Skip to content

Commit e018aef

Browse files
committed
refactor(core): extract shared pure helper modules
1 parent 002405c commit e018aef

10 files changed

Lines changed: 547 additions & 277 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ContainerType } from "loro-crdt";
2+
import type { InferContainerOptions } from "../schema/types.js";
3+
4+
export type SchemaContainerTypeName =
5+
| "loro-map"
6+
| "loro-list"
7+
| "loro-text"
8+
| "loro-movable-list";
9+
10+
type InferableContainerType = Exclude<ContainerType, "Tree" | "Counter">;
11+
type ContainerValueKind = "map" | "array" | "string" | "other";
12+
13+
export function isPlainObjectValue(
14+
value: unknown,
15+
): value is Record<string, unknown> {
16+
return (
17+
typeof value === "object" &&
18+
value !== null &&
19+
!Array.isArray(value) &&
20+
!(value instanceof Date) &&
21+
!(value instanceof RegExp) &&
22+
typeof value !== "function"
23+
);
24+
}
25+
26+
export function inferContainerTypeFromValue(
27+
value: unknown,
28+
defaults?: InferContainerOptions,
29+
): InferableContainerType | undefined {
30+
switch (getContainerValueKind(value)) {
31+
case "map":
32+
return "Map";
33+
case "array":
34+
return defaults?.defaultMovableList ? "MovableList" : "List";
35+
case "string":
36+
return defaults?.defaultLoroText ? "Text" : undefined;
37+
default:
38+
return undefined;
39+
}
40+
}
41+
42+
export function inferSchemaContainerTypeFromValue(
43+
value: unknown,
44+
defaults?: InferContainerOptions,
45+
): SchemaContainerTypeName | undefined {
46+
const containerType = inferContainerTypeFromValue(value, defaults);
47+
switch (containerType) {
48+
case "Map":
49+
return "loro-map";
50+
case "List":
51+
return "loro-list";
52+
case "MovableList":
53+
return "loro-movable-list";
54+
case "Text":
55+
return "loro-text";
56+
default:
57+
return undefined;
58+
}
59+
}
60+
61+
export function matchesContainerType(
62+
containerType: ContainerType,
63+
value: unknown,
64+
): boolean {
65+
const kind = getContainerValueKind(value);
66+
switch (containerType) {
67+
case "Map":
68+
return kind === "map";
69+
case "List":
70+
case "MovableList":
71+
case "Tree":
72+
return kind === "array";
73+
case "Text":
74+
return kind === "string";
75+
default:
76+
return false;
77+
}
78+
}
79+
80+
function getContainerValueKind(value: unknown): ContainerValueKind {
81+
if (isPlainObjectValue(value)) {
82+
return "map";
83+
}
84+
if (Array.isArray(value)) {
85+
return "array";
86+
}
87+
if (typeof value === "string") {
88+
return "string";
89+
}
90+
return "other";
91+
}

packages/core/src/core/diff.ts

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
isRootSchemaType,
1717
LoroListSchema,
1818
LoroMapSchema,
19-
LoroMapSchemaWithCatchall,
2019
LoroMovableListSchema,
2120
LoroTreeSchema,
2221
RootSchemaType,
@@ -43,6 +42,7 @@ import {
4342
isTreeID,
4443
defineCidProperty,
4544
} from "./utils.js";
45+
import { getMapFieldSchema } from "../schema/resolver.js";
4646

4747
/**
4848
* Finds the longest increasing subsequence of a sequence of numbers
@@ -96,33 +96,6 @@ type CommonListItemInfo = {
9696

9797
type IdSelector<T> = (item: T) => string | undefined;
9898

99-
function getMapChildSchema(
100-
schema:
101-
| LoroMapSchema<Record<string, SchemaType>>
102-
| LoroMapSchemaWithCatchall<Record<string, SchemaType>, SchemaType>
103-
| RootSchemaType<Record<string, ContainerSchemaType>>
104-
| undefined,
105-
key: string,
106-
): SchemaType | ContainerSchemaType | undefined {
107-
if (!schema) return undefined;
108-
if (schema.type === "schema") {
109-
return schema.definition[key];
110-
}
111-
if (schema.type === "loro-map") {
112-
if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
113-
return schema.definition[key];
114-
}
115-
const withCatchall = schema as LoroMapSchemaWithCatchall<
116-
Record<string, SchemaType>,
117-
SchemaType
118-
> & { catchallType?: SchemaType };
119-
if (withCatchall.catchallType) {
120-
return withCatchall.catchallType;
121-
}
122-
}
123-
return undefined;
124-
}
125-
12699
/**
127100
* Diffs a container between two states
128101
*
@@ -1271,17 +1244,7 @@ export function diffMap<S extends ObjectLike>(
12711244
continue;
12721245
}
12731246
// Skip ignored fields defined in schema
1274-
const childSchemaForDelete = getMapChildSchema(
1275-
schema as
1276-
| LoroMapSchema<Record<string, SchemaType>>
1277-
| LoroMapSchemaWithCatchall<
1278-
Record<string, SchemaType>,
1279-
SchemaType
1280-
>
1281-
| RootSchemaType<Record<string, ContainerSchemaType>>
1282-
| undefined,
1283-
key,
1284-
);
1247+
const childSchemaForDelete = getMapFieldSchema(schema, key);
12851248
if (childSchemaForDelete && childSchemaForDelete.type === "ignore") {
12861249
continue;
12871250
}
@@ -1309,17 +1272,7 @@ export function diffMap<S extends ObjectLike>(
13091272
if (newItem === undefined) {
13101273
// If old item exists, we need to delete it
13111274
if (key in oldStateObj && oldItem !== undefined) {
1312-
const childSchemaForDelete = getMapChildSchema(
1313-
schema as
1314-
| LoroMapSchema<Record<string, SchemaType>>
1315-
| LoroMapSchemaWithCatchall<
1316-
Record<string, SchemaType>,
1317-
SchemaType
1318-
>
1319-
| RootSchemaType<Record<string, ContainerSchemaType>>
1320-
| undefined,
1321-
key,
1322-
);
1275+
const childSchemaForDelete = getMapFieldSchema(schema, key);
13231276
if (!(childSchemaForDelete && childSchemaForDelete.type === "ignore")) {
13241277
changes.push({
13251278
container: containerId,
@@ -1333,17 +1286,7 @@ export function diffMap<S extends ObjectLike>(
13331286
}
13341287

13351288
// Figure out if the modified new value is a container
1336-
const childSchema = getMapChildSchema(
1337-
schema as
1338-
| LoroMapSchema<Record<string, SchemaType>>
1339-
| LoroMapSchemaWithCatchall<
1340-
Record<string, SchemaType>,
1341-
SchemaType
1342-
>
1343-
| RootSchemaType<Record<string, ContainerSchemaType>>
1344-
| undefined,
1345-
key,
1346-
);
1289+
const childSchema = getMapFieldSchema(schema, key);
13471290

13481291
// Skip ignored fields defined in schema
13491292
if (childSchema && childSchema.type === "ignore") {

packages/core/src/core/loroEventApply.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
LoroEventBatch,
99
TreeID,
1010
} from "loro-crdt";
11+
import {
12+
normalizeTreeJson,
13+
type NormalizedTreeNode,
14+
} from "./tree-utils.js";
1115
import { defineCidProperty, isTreeID } from "./utils.js";
1216

1317
// Plain JSON-like value held in Mirror state (no `any`)
@@ -18,11 +22,7 @@ interface JSONObject {
1822
}
1923

2024
// State representation for a tree node in mirror state
21-
interface StateTreeNode {
22-
id: string;
23-
data: JSONObject;
24-
children: StateTreeNode[];
25-
}
25+
type StateTreeNode = NormalizedTreeNode<JSONObject>;
2626

2727
function isJSONObject(v: unknown): v is JSONObject {
2828
return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -357,24 +357,6 @@ function getOrInitNodeData(node: JSONObject): JSONObject {
357357
return fresh;
358358
}
359359

360-
// Normalize LoroTree JSON (with `meta`) to Mirror tree node shape `{ id, data, children }`.
361-
function normalizeTreeJson(input: unknown[]): StateTreeNode[] {
362-
if (!Array.isArray(input)) return [];
363-
return input.map(mapRawTreeNode);
364-
}
365-
366-
function mapRawTreeNode(n: unknown): StateTreeNode {
367-
const rawId = (n as { id?: unknown })?.id;
368-
const id = typeof rawId === "string" ? rawId : "";
369-
const meta = (n as { meta?: unknown })?.meta;
370-
const data = isJSONObject(meta) ? meta : {};
371-
const rawChildren = (n as { children?: unknown })?.children;
372-
const children = Array.isArray(rawChildren)
373-
? rawChildren.map(mapRawTreeNode)
374-
: [];
375-
return { id, data, children };
376-
}
377-
378360
/**
379361
* Apply Map updates to a plain object
380362
*/
@@ -638,7 +620,10 @@ function containerToMirrorJson(c: Container): JSONValue {
638620
const raw = (
639621
c as Exclude<Container, LoroCounter>
640622
).toJSON() as unknown[];
641-
return normalizeTreeJson(raw) as unknown as JSONValue;
623+
return normalizeTreeJson(raw, {
624+
isTreeData: isJSONObject,
625+
createEmptyData: (): JSONObject => ({}),
626+
}) as unknown as JSONValue;
642627
}
643628
return (
644629
c as Exclude<Container, LoroCounter>

0 commit comments

Comments
 (0)