Skip to content

Commit 280fcfa

Browse files
authored
Fix retry on push with objects (#907)
instead of modifying the doc, just do the bare minimum re-applying of the push & return that value.
1 parent cb9251c commit 280fcfa

File tree

2 files changed

+86
-10
lines changed

2 files changed

+86
-10
lines changed

runner/src/cell.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,13 +400,6 @@ function createRegularCell<T>(
400400
}
401401
});
402402

403-
// Hacky retry logic for push only. See storage.ts for details on this
404-
// retry approach and what we should really be doing instead.
405-
if (!ref.cell.retry) ref.cell.retry = [];
406-
ref.cell.retry.push((newBaseValue: any[]) =>
407-
diffAndUpdate(ref, [...newBaseValue, ...valuesToWrite], log, cause)
408-
);
409-
410403
// If there is no array yet, create it first. We have to do this as a
411404
// separate operation, so that in the next steps [ID] is properly anchored
412405
// in the array.
@@ -417,6 +410,27 @@ function createRegularCell<T>(
417410

418411
// Append the new values to the array.
419412
diffAndUpdate(ref, [...array, ...valuesToWrite], log, cause);
413+
414+
const appended = ref.cell.getAtPath(ref.path).slice(
415+
-valuesToWrite.length,
416+
);
417+
418+
// Hacky retry logic for push only. See storage.ts for details on this
419+
// retry approach and what we should really be doing instead.
420+
if (!ref.cell.retry) ref.cell.retry = [];
421+
ref.cell.retry.push((newBaseValue: any[]) => {
422+
// Unlikely, but maybe the conflict reset to undefined?
423+
if (newBaseValue === undefined) {
424+
newBaseValue = Array.isArray(schema?.default) ? schema.default : [];
425+
}
426+
427+
// Serialize cell links that were appended during the push. This works
428+
// because of the .toJSON() method on Cell.
429+
const newValues = JSON.parse(JSON.stringify(appended));
430+
431+
// Reappend the new values.
432+
return [...newBaseValue, ...newValues];
433+
});
420434
},
421435
equals: (other: Cell<any>) =>
422436
JSON.stringify(self) === JSON.stringify(other),

runner/test/push-conflict.test.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { describe, it } from "@std/testing/bdd";
22
import { expect } from "@std/expect";
3+
import { ID } from "@commontools/builder";
4+
import { Identity } from "@commontools/identity";
35
import { storage } from "../src/storage.ts";
4-
import { getDoc } from "@commontools/runner";
6+
import { getDoc } from "../src/doc.ts";
7+
import { isCellLink } from "../src/cell.ts";
58
import { VolatileStorageProvider } from "../src/storage/volatile.ts";
6-
import { Identity } from "@commontools/identity";
79

810
storage.setRemoteStorage(new URL(`volatile:`));
911
storage.setSigner(await Identity.fromPassphrase("test operator"));
@@ -51,7 +53,7 @@ describe("Push conflict", () => {
5153
"name",
5254
"push and set",
5355
);
54-
const listDoc = getDoc<any[]>([], "list", "push and set");
56+
const listDoc = getDoc<any[]>([], "list 2", "push and set");
5557

5658
const name = nameDoc.asCell();
5759
const list = listDoc.asCell();
@@ -96,4 +98,64 @@ describe("Push conflict", () => {
9698
// Retry list should be empty now, since the change was applied.
9799
expect(!!listDoc.retry?.length).toBe(false);
98100
});
101+
102+
it("should resolve push conflicts with ID among other conflicts", async () => {
103+
const nameDoc = getDoc<string | undefined>(
104+
undefined,
105+
"name 2",
106+
"push and set",
107+
);
108+
const listDoc = getDoc<any[]>([], "list 3", "push and set");
109+
110+
const name = nameDoc.asCell();
111+
const list = listDoc.asCell();
112+
113+
await storage.syncCell(name);
114+
await storage.syncCell(list);
115+
116+
const memory = new VolatileStorageProvider("push and set");
117+
118+
// Update memory without notifying main storage
119+
await memory.sync(nameDoc.entityId, true); // Get current value
120+
await memory.sync(listDoc.entityId, true); // Get current value
121+
await memory.send<any>([{
122+
entityId: nameDoc.entityId,
123+
value: { value: "foo" },
124+
}, {
125+
entityId: listDoc.entityId,
126+
value: { value: [{ n: 1 }, { n: 2 }, { n: 3 }] },
127+
}], true); // true = do not notify main storage
128+
129+
let retryCalled = 0;
130+
listDoc.retry = [(value) => {
131+
retryCalled++;
132+
return value;
133+
}];
134+
135+
name.set("bar");
136+
list.push({ n: 4, [ID]: "4" });
137+
138+
// This is locally ahead of the db, and retry wasn't called yet.
139+
expect(name.get()).toEqual("bar");
140+
expect(list.get()).toEqual([{ n: 4 }]);
141+
expect(isCellLink(listDoc.get()?.[0])).toBe(true);
142+
const entry = listDoc.get()[0].cell?.asCell();
143+
expect(retryCalled).toEqual(0);
144+
145+
await storage.synced();
146+
147+
// We successfully replayed the change on top of the db:
148+
expect(name.get()).toEqual("foo");
149+
expect(
150+
list.asSchema({
151+
type: "array",
152+
items: { type: "object", properties: { n: { type: "number" } } },
153+
}).get(),
154+
).toEqual([{ n: 1 }, { n: 2 }, { n: 3 }, { n: 4 }]);
155+
expect(retryCalled).toEqual(1);
156+
expect(!!listDoc.retry?.length).toBe(false);
157+
158+
// Check that the ID is still there
159+
expect(entry.equals(listDoc.get()[3])).toBe(true);
160+
});
99161
});

0 commit comments

Comments
 (0)