diff --git a/sdk/js/alien-deepsignals/src/deepSignal.ts b/sdk/js/alien-deepsignals/src/deepSignal.ts index 48490bc..cd71dba 100644 --- a/sdk/js/alien-deepsignals/src/deepSignal.ts +++ b/sdk/js/alien-deepsignals/src/deepSignal.ts @@ -490,8 +490,9 @@ function getFromSet( objToProxy.set(entryVal, childProxy); entryVal = childProxy; } - // Set entry add: emit object vs literal variant. + // Set entry add: emit object vs primitive variant. if (entryVal && typeof entryVal === "object") { + // Object entry: path includes synthetic id queuePatch({ root: metaNow.root, path: [...containerPath, synthetic], @@ -499,21 +500,36 @@ function getFromSet( type: "object", }); } else { + // Primitive entry: path is just the Set, value contains the primitive queuePatch({ root: metaNow.root, - path: [...containerPath, synthetic], + path: containerPath, op: "add", - value: entryVal, + type: "set", + value: [entryVal], }); } } else if (key === "delete") { const entry = args[0]; const synthetic = getSetEntryKey(entry); - queuePatch({ - root: metaNow.root, - path: [...containerPath, synthetic], - op: "remove", - }); + // Check if entry is primitive or object + if (entry && typeof entry === "object") { + // Object entry: path includes synthetic id + queuePatch({ + root: metaNow.root, + path: [...containerPath, synthetic], + op: "remove", + }); + } else { + // Primitive entry: path is just the Set, value contains the primitive + queuePatch({ + root: metaNow.root, + path: containerPath, + op: "remove", + type: "set", + value: entry, + }); + } } else if (key === "clear") { // Structural clear: remove prior entry-level patches for this Set this tick. if (pendingPatches) { diff --git a/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts b/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts index e15603c..4a30469 100644 --- a/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts +++ b/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts @@ -95,7 +95,16 @@ describe("watch (patch mode)", () => { await Promise.resolve(); expect(batches.length >= 1).toBe(true); const allPaths = batches.flatMap((b) => b.map((p) => p.path.join("."))); - expect(allPaths.some((p) => p.startsWith("s."))).toBe(true); + // For primitives, the path should be just "s" (the Set itself) + expect(allPaths.every((p) => p === "s")).toBe(true); + // Check the values + const patches = batches.flat(); + const addPatches = patches.filter((p) => p.op === "add"); + const deletePatches = patches.filter((p) => p.op === "remove"); + expect(addPatches.length).toBe(1); + expect(deletePatches.length).toBe(1); + expect((addPatches[0] as any).value[0]).toBe(3); + expect((deletePatches[0] as any).value).toBe(1); stop(); }); @@ -226,6 +235,89 @@ describe("watch (patch mode)", () => { }); describe("Set", () => { + it("emits patches for primitive adds", async () => { + const st = deepSignal({ s: new Set() }); + const batches: DeepPatch[][] = []; + const { stopListening: stop } = watch(st, ({ patches }) => + batches.push(patches) + ); + st.s.add(true); + st.s.add(2); + st.s.add("3"); + await Promise.resolve(); + + expect(batches.length).toBe(1); + const patches = batches[0]; + expect(patches.length).toBe(3); + + // All patches should have the same path (the Set itself) + patches.forEach((p) => { + expect(p.path.join(".")).toBe("s"); + expect(p.op).toBe("add"); + expect((p as any).type).toBe("set"); + }); + + // Check that values are in the value field, not in path + const values = patches.map((p: any) => p.value[0]); + expect(values).toContain(true); + expect(values).toContain(2); + expect(values).toContain("3"); + stop(); + }); + it("emits patches for primitive deletes", async () => { + const st = deepSignal({ s: new Set([true, 2, "3"]) }); + const batches: DeepPatch[][] = []; + const { stopListening: stop } = watch(st, ({ patches }) => + batches.push(patches) + ); + st.s.delete(true); + st.s.delete(2); + await Promise.resolve(); + + expect(batches.length).toBe(1); + const patches = batches[0]; + expect(patches.length).toBe(2); + + // All patches should have the same path (the Set itself) + patches.forEach((p) => { + expect(p.path.join(".")).toBe("s"); + expect(p.op).toBe("remove"); + expect((p as any).type).toBe("set"); + }); + + // Check that values are in the value field + const values = patches.map((p: any) => p.value); + expect(values).toContain(true); + expect(values).toContain(2); + stop(); + }); + it("does not emit patches for non-existent primitives", async () => { + const st = deepSignal({ s: new Set([1, 2]) }); + const batches: DeepPatch[][] = []; + const { stopListening: stop } = watch(st, ({ patches }) => + batches.push(patches) + ); + st.s.delete("nonexistent"); + st.s.delete(999); + await Promise.resolve(); + + expect(batches.length).toBe(0); + stop(); + }); + it("does not emit patches for already added primitive", async () => { + const st = deepSignal({ s: new Set([1, "test", true]) }); + const batches: DeepPatch[][] = []; + const { stopListening: stop } = watch(st, ({ patches }) => + batches.push(patches) + ); + st.s.add(1); + st.s.add("test"); + st.s.add(true); + await Promise.resolve(); + + expect(batches.length).toBe(0); + stop(); + }); it("emits single structural patch on Set.clear()", async () => { const st = deepSignal({ s: new Set() }); addWithId(st.s as any, { id: "a", x: 1 }, "a"); @@ -288,8 +380,11 @@ describe("watch (patch mode)", () => { const ret = addWithId(st.s as any, 5, "ignored"); expect(ret).toBe(5); await Promise.resolve(); + // For primitives, path should be just "s" and value should be in the value field const paths = patches.flat().map((p) => p.path.join(".")); - expect(paths).toContain("s.5"); + expect(paths).toContain("s"); + const values = patches.flat().map((p: any) => p.value?.[0]); + expect(values).toContain(5); stop(); }); it("setSetEntrySyntheticId applies custom id without helper", async () => {