Skip to content

Commit 9050513

Browse files
committed
fix(core): restore injection for map record entries
1 parent 2bf1942 commit 9050513

File tree

3 files changed

+143
-19
lines changed

3 files changed

+143
-19
lines changed

packages/core/src/core/diff.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isRootSchemaType,
1717
LoroListSchema,
1818
LoroMapSchema,
19+
LoroMapSchemaWithCatchall,
1920
LoroMovableListSchema,
2021
LoroTextSchemaType,
2122
LoroTreeSchema,
@@ -94,6 +95,33 @@ type CommonListItemInfo = {
9495

9596
type IdSelector<T> = (item: T) => string | undefined;
9697

98+
function getMapChildSchema(
99+
schema:
100+
| LoroMapSchema<Record<string, SchemaType>>
101+
| LoroMapSchemaWithCatchall<Record<string, SchemaType>, SchemaType>
102+
| RootSchemaType<Record<string, ContainerSchemaType>>
103+
| undefined,
104+
key: string,
105+
): SchemaType | ContainerSchemaType | undefined {
106+
if (!schema) return undefined;
107+
if (schema.type === "schema") {
108+
return schema.definition[key];
109+
}
110+
if (schema.type === "loro-map") {
111+
if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
112+
return schema.definition[key];
113+
}
114+
const withCatchall = schema as LoroMapSchemaWithCatchall<
115+
Record<string, SchemaType>,
116+
SchemaType
117+
> & { catchallType?: SchemaType };
118+
if (withCatchall.catchallType) {
119+
return withCatchall.catchallType;
120+
}
121+
}
122+
return undefined;
123+
}
124+
97125
/**
98126
* Diffs a container between two states
99127
*
@@ -980,9 +1008,17 @@ export function diffMap<S extends ObjectLike>(
9801008
continue;
9811009
}
9821010
// Skip ignored fields defined in schema
983-
const childSchemaForDelete = (
984-
schema as LoroMapSchema<Record<string, SchemaType>> | undefined
985-
)?.definition?.[key];
1011+
const childSchemaForDelete = getMapChildSchema(
1012+
schema as
1013+
| LoroMapSchema<Record<string, SchemaType>>
1014+
| LoroMapSchemaWithCatchall<
1015+
Record<string, SchemaType>,
1016+
SchemaType
1017+
>
1018+
| RootSchemaType<Record<string, ContainerSchemaType>>
1019+
| undefined,
1020+
key,
1021+
);
9861022
if (childSchemaForDelete && childSchemaForDelete.type === "ignore") {
9871023
continue;
9881024
}
@@ -1006,9 +1042,17 @@ export function diffMap<S extends ObjectLike>(
10061042
const newItem = newStateObj[key];
10071043

10081044
// Figure out if the modified new value is a container
1009-
const childSchema = (
1010-
schema as LoroMapSchema<Record<string, SchemaType>> | undefined
1011-
)?.definition?.[key];
1045+
const childSchema = getMapChildSchema(
1046+
schema as
1047+
| LoroMapSchema<Record<string, SchemaType>>
1048+
| LoroMapSchemaWithCatchall<
1049+
Record<string, SchemaType>,
1050+
SchemaType
1051+
>
1052+
| RootSchemaType<Record<string, ContainerSchemaType>>
1053+
| undefined,
1054+
key,
1055+
);
10121056

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

packages/core/src/core/mirror.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
isLoroTreeSchema,
3333
LoroListSchema,
3434
LoroMapSchema,
35+
LoroMapSchemaWithCatchall,
3536
RootSchemaType,
3637
SchemaType,
3738
validateSchema,
@@ -504,9 +505,18 @@ export class Mirror<S extends SchemaType> {
504505
if (isContainer(value)) {
505506
let nestedSchema: ContainerSchemaType | undefined;
506507
if (parentSchema && isLoroMapSchema(parentSchema)) {
507-
nestedSchema = parentSchema.definition[
508-
key
509-
] as ContainerSchemaType;
508+
const candidate = this.getSchemaForMapKey(
509+
parentSchema as
510+
| LoroMapSchema<Record<string, SchemaType>>
511+
| LoroMapSchemaWithCatchall<
512+
Record<string, SchemaType>,
513+
SchemaType
514+
>,
515+
key,
516+
);
517+
if (candidate && isContainerSchema(candidate)) {
518+
nestedSchema = candidate;
519+
}
510520
}
511521
this.registerContainer(value.id, nestedSchema);
512522
}
@@ -1419,16 +1429,19 @@ export class Mirror<S extends SchemaType> {
14191429
if (!isObject(value)) {
14201430
return;
14211431
}
1432+
const mapSchema = schema as
1433+
| LoroMapSchema<Record<string, SchemaType>>
1434+
| LoroMapSchemaWithCatchall<
1435+
Record<string, SchemaType>,
1436+
SchemaType
1437+
>
1438+
| undefined;
14221439
for (const [key, val] of Object.entries(value)) {
14231440
// Skip injected CID field
14241441
if (key === CID_KEY) continue;
1425-
const fieldSchema = (
1426-
schema as
1427-
| LoroMapSchema<Record<string, SchemaType>>
1428-
| undefined
1429-
)?.definition[key];
1442+
const fieldSchema = this.getSchemaForMapKey(mapSchema, key);
14301443

1431-
if (isContainerSchema(fieldSchema)) {
1444+
if (fieldSchema && isContainerSchema(fieldSchema)) {
14321445
const ct = schemaToContainerType(fieldSchema);
14331446
if (ct && isValueOfContainerType(ct, val)) {
14341447
this.insertContainerIntoMap(map, fieldSchema, key, val);
@@ -1660,8 +1673,14 @@ export class Mirror<S extends SchemaType> {
16601673
) {
16611674
if (key === CID_KEY) return; // Ignore CID in writes
16621675
// Check if this field should be a container according to schema
1663-
if (schema && schema.type === "loro-map" && schema.definition) {
1664-
const fieldSchema = schema.definition[key];
1676+
if (schema && schema.type === "loro-map") {
1677+
const mapSchema = schema as
1678+
| LoroMapSchema<Record<string, SchemaType>>
1679+
| LoroMapSchemaWithCatchall<
1680+
Record<string, SchemaType>,
1681+
SchemaType
1682+
>;
1683+
const fieldSchema = this.getSchemaForMapKey(mapSchema, key);
16651684
if (fieldSchema && fieldSchema.type === "ignore") {
16661685
// Skip ignore fields: they live only in mirrored state
16671686
return;
@@ -1926,6 +1945,27 @@ export class Mirror<S extends SchemaType> {
19261945
});
19271946
}
19281947

1948+
private getSchemaForMapKey(
1949+
schema:
1950+
| LoroMapSchema<Record<string, SchemaType>>
1951+
| LoroMapSchemaWithCatchall<Record<string, SchemaType>, SchemaType>
1952+
| undefined,
1953+
key: string,
1954+
): SchemaType | undefined {
1955+
if (!schema) return undefined;
1956+
if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
1957+
return schema.definition[key];
1958+
}
1959+
const withCatchall = schema as LoroMapSchemaWithCatchall<
1960+
Record<string, SchemaType>,
1961+
SchemaType
1962+
> & { catchallType?: SchemaType };
1963+
if (withCatchall.catchallType) {
1964+
return withCatchall.catchallType;
1965+
}
1966+
return undefined;
1967+
}
1968+
19291969
private getContainerSchema(
19301970
containerId: ContainerID,
19311971
): ContainerSchemaType | undefined {
@@ -1956,7 +1996,15 @@ export class Mirror<S extends SchemaType> {
19561996
}
19571997

19581998
if (isLoroMapSchema(containerSchema)) {
1959-
return containerSchema.definition[childKey];
1999+
return this.getSchemaForMapKey(
2000+
containerSchema as
2001+
| LoroMapSchema<Record<string, SchemaType>>
2002+
| LoroMapSchemaWithCatchall<
2003+
Record<string, SchemaType>,
2004+
SchemaType
2005+
>,
2006+
String(childKey),
2007+
);
19602008
} else if (
19612009
isLoroListSchema(containerSchema) ||
19622010
isLoroMovableListSchema(containerSchema)

packages/core/tests/issue.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { it, expect } from "vitest";
2-
import { LoroDoc } from "loro-crdt";
2+
import { LoroDoc, LoroMap } from "loro-crdt";
33
import { Mirror, schema } from "../src/";
44

55
it("Applying remote event then calling setState immediately may cause an event apply order issue", async () => {
@@ -54,3 +54,35 @@ it("Reproduces 'Item ID cannot be null' when adding a new todo item using $cid-b
5454
todos: [{ text: "Buy milk", status: "todo" }],
5555
});
5656
});
57+
58+
it("doc with map record containers misses $cid in initial getState (repro)", () => {
59+
const doc = new LoroDoc();
60+
const root = doc.getMap("root");
61+
const todo = new LoroMap();
62+
todo.set("text", "Buy eggs");
63+
root.setContainer("todo-1", todo);
64+
const list = doc.getList("list");
65+
list.pushContainer(new LoroMap());
66+
doc.commit();
67+
68+
const todoSchema = schema({
69+
root: schema.LoroMapRecord(
70+
schema.LoroMap({
71+
text: schema.String(),
72+
}),
73+
),
74+
list: schema.LoroList(schema.LoroMap({})),
75+
});
76+
77+
const m = new Mirror({
78+
doc,
79+
schema: todoSchema,
80+
});
81+
82+
const state = m.getState() as any;
83+
expect(state.root).toBeDefined();
84+
expect(state.root["todo-1"]).toBeDefined();
85+
// Bug: $cid is currently missing even though the container exists in the doc
86+
expect(typeof state.root["todo-1"].$cid).toBe("string");
87+
expect(typeof state.list[0].$cid).toBe("string");
88+
});

0 commit comments

Comments
 (0)