@ng-org/alien-deepsignals 0.1.2-alpha.2 → 0.1.2-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/deepSignal.d.ts.map +1 -1
  2. package/dist/deepSignal.js +244 -100
  3. package/dist/hooks/react/useDeepSignal.d.ts +4 -4
  4. package/dist/hooks/react/useDeepSignal.d.ts.map +1 -1
  5. package/dist/hooks/react/useDeepSignal.js +22 -15
  6. package/dist/hooks/svelte/useDeepSignal.svelte.d.ts +3 -7
  7. package/dist/hooks/svelte/useDeepSignal.svelte.d.ts.map +1 -1
  8. package/dist/hooks/svelte/useDeepSignal.svelte.js +34 -18
  9. package/dist/hooks/vue/useDeepSignal.d.ts +4 -3
  10. package/dist/hooks/vue/useDeepSignal.d.ts.map +1 -1
  11. package/dist/hooks/vue/useDeepSignal.js +52 -24
  12. package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts +4 -0
  13. package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts.map +1 -0
  14. package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.js +225 -0
  15. package/dist/test/frontend/astro-app/src/components/ReactPanel.d.ts +4 -0
  16. package/dist/test/frontend/astro-app/src/components/ReactPanel.d.ts.map +1 -0
  17. package/dist/test/frontend/astro-app/src/components/ReactPanel.js +227 -0
  18. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts +4 -0
  19. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts.map +1 -0
  20. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.js +150 -0
  21. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts +4 -0
  22. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts.map +1 -0
  23. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.js +184 -0
  24. package/dist/test/frontend/playwright/crossFrameworkHooks.spec.d.ts +2 -0
  25. package/dist/test/frontend/playwright/crossFrameworkHooks.spec.d.ts.map +1 -0
  26. package/dist/test/frontend/playwright/crossFrameworkHooks.spec.js +171 -0
  27. package/dist/test/frontend/playwright/perfSuite.spec.d.ts +2 -0
  28. package/dist/test/frontend/playwright/perfSuite.spec.d.ts.map +1 -0
  29. package/dist/test/frontend/playwright/perfSuite.spec.js +128 -0
  30. package/dist/test/frontend/utils/mockData.d.ts +53 -0
  31. package/dist/test/frontend/utils/mockData.d.ts.map +1 -0
  32. package/dist/test/frontend/utils/mockData.js +78 -0
  33. package/dist/test/frontend/utils/paths.d.ts +4 -0
  34. package/dist/test/frontend/utils/paths.d.ts.map +1 -0
  35. package/dist/test/frontend/utils/paths.js +28 -0
  36. package/dist/test/frontend/utils/perfScenarios.d.ts +15 -0
  37. package/dist/test/frontend/utils/perfScenarios.d.ts.map +1 -0
  38. package/dist/test/frontend/utils/perfScenarios.js +287 -0
  39. package/dist/test/frontend/utils/renderMetrics.d.ts +13 -0
  40. package/dist/test/frontend/utils/renderMetrics.d.ts.map +1 -0
  41. package/dist/test/frontend/utils/renderMetrics.js +45 -0
  42. package/dist/test/frontend/utils/state.d.ts +57 -0
  43. package/dist/test/frontend/utils/state.d.ts.map +1 -0
  44. package/dist/test/frontend/utils/state.js +79 -0
  45. package/dist/test/lib/core.test.d.ts +2 -0
  46. package/dist/test/lib/core.test.d.ts.map +1 -0
  47. package/dist/test/lib/core.test.js +53 -0
  48. package/dist/test/lib/deepSignalOptions.test.d.ts +2 -0
  49. package/dist/test/lib/deepSignalOptions.test.d.ts.map +1 -0
  50. package/dist/test/lib/deepSignalOptions.test.js +230 -0
  51. package/dist/test/lib/index.test.d.ts +2 -0
  52. package/dist/test/lib/index.test.d.ts.map +1 -0
  53. package/dist/test/lib/index.test.js +807 -0
  54. package/dist/test/lib/misc.test.d.ts +2 -0
  55. package/dist/test/lib/misc.test.d.ts.map +1 -0
  56. package/dist/test/lib/misc.test.js +140 -0
  57. package/dist/test/lib/watch.test.d.ts +2 -0
  58. package/dist/test/lib/watch.test.d.ts.map +1 -0
  59. package/dist/test/lib/watch.test.js +1280 -0
  60. package/dist/types.d.ts +1 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +36 -19
  63. package/src/index.ts +5 -0
@@ -0,0 +1,1280 @@
1
+ "use strict";
2
+ // Copyright (c) 2025 Laurin Weger, Par le Peuple, NextGraph.org developers
3
+ // All rights reserved.
4
+ // Licensed under the Apache License, Version 2.0
5
+ // <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
6
+ // or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
7
+ // at your option. All files in the project carrying such
8
+ // notice may not be copied, modified, or distributed except
9
+ // according to those terms.
10
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const vitest_1 = require("vitest");
13
+ const __1 = require("../../");
14
+ const watch_1 = require("../../watch");
15
+ const effect_1 = require("../../effect");
16
+ (0, vitest_1.describe)("watch", () => {
17
+ (0, vitest_1.it)("watch immediate", () => {
18
+ const store = (0, __1.deepSignal)({
19
+ userinfo: {
20
+ name: "tom",
21
+ },
22
+ });
23
+ let val;
24
+ (0, watch_1.watch)(store, ({ newValue }) => {
25
+ val = newValue.userinfo.name;
26
+ }, { immediate: true });
27
+ (0, vitest_1.expect)(val).toEqual("tom");
28
+ });
29
+ (0, vitest_1.it)("watch deep", () => {
30
+ const store = (0, __1.deepSignal)({
31
+ userinfo: {
32
+ name: "tom",
33
+ },
34
+ });
35
+ let val;
36
+ (0, watch_1.watch)(store, ({ newValue }) => {
37
+ val = newValue.userinfo.name;
38
+ }, { immediate: true });
39
+ let value2;
40
+ (0, watch_1.watch)(store, ({ newValue }) => {
41
+ value2 = newValue.userinfo.name;
42
+ }, { immediate: true });
43
+ (0, vitest_1.expect)(val).toEqual("tom");
44
+ store.userinfo.name = "jon";
45
+ // patch delivery async (microtask)
46
+ return Promise.resolve().then(() => {
47
+ (0, vitest_1.expect)(val).toEqual("jon");
48
+ // With refactored watch using native effect, shallow watcher now also updates root reference
49
+ (0, vitest_1.expect)(value2).toEqual("jon");
50
+ });
51
+ });
52
+ (0, vitest_1.it)("watch once", () => {
53
+ const store = (0, __1.deepSignal)({
54
+ userinfo: {
55
+ name: "tom",
56
+ },
57
+ });
58
+ let val;
59
+ (0, watch_1.watch)(store, ({ newValue }) => {
60
+ val = newValue.userinfo.name;
61
+ }, { immediate: true, once: true });
62
+ (0, vitest_1.expect)(val).toEqual("tom");
63
+ store.userinfo.name = "jon";
64
+ // once watcher shouldn't update after first run
65
+ (0, vitest_1.expect)(val).toEqual("tom");
66
+ });
67
+ (0, vitest_1.it)("delivers immediate snapshot", () => {
68
+ const state = (0, __1.deepSignal)({ user: { name: "tom" } });
69
+ let observed = "";
70
+ (0, watch_1.watch)(state, ({ newValue }) => {
71
+ observed = newValue.user.name;
72
+ }, { immediate: true });
73
+ (0, vitest_1.expect)(observed).toEqual("tom");
74
+ });
75
+ (0, vitest_1.it)("emits patches with version", async () => {
76
+ const state = (0, __1.deepSignal)({ count: 0 });
77
+ const versions = [];
78
+ (0, watch_1.watch)(state, ({ version }) => {
79
+ versions.push(version);
80
+ });
81
+ state.count = 1;
82
+ await Promise.resolve();
83
+ (0, vitest_1.expect)(versions.length).toBe(1);
84
+ (0, vitest_1.expect)(versions[0]).toBeGreaterThan(0);
85
+ });
86
+ (0, vitest_1.it)("effect runs and cleans up", () => {
87
+ const calls = [];
88
+ const dispose = (0, effect_1.effect)((registerCleanup) => {
89
+ calls.push("run");
90
+ registerCleanup?.(() => calls.push("cleanup"));
91
+ });
92
+ dispose();
93
+ (0, vitest_1.expect)(calls).toEqual(["run", "cleanup"]);
94
+ });
95
+ });
96
+ (0, vitest_1.describe)("watch (patch mode)", () => {
97
+ (0, vitest_1.it)("emits set patches with correct paths and batching", async () => {
98
+ const state = (0, __1.deepSignal)({ a: { b: 1 }, arr: [1, { x: 2 }] });
99
+ const received = [];
100
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => {
101
+ received.push(patches);
102
+ });
103
+ state.a.b = 2;
104
+ state.arr[1].x = 3;
105
+ state.arr.push(5);
106
+ await Promise.resolve();
107
+ (0, vitest_1.expect)(received).toHaveLength(1);
108
+ const batch = received[0];
109
+ (0, vitest_1.expect)(batch).toEqual([
110
+ { op: "add", path: ["a", "b"], value: 2 },
111
+ { op: "add", path: ["arr", "1", "x"], value: 3 },
112
+ { op: "add", path: ["arr", "2"], value: 5 },
113
+ ]);
114
+ stop();
115
+ });
116
+ (0, vitest_1.it)("emits delete patches without value", async () => {
117
+ const state = (0, __1.deepSignal)({
118
+ a: { b: 1 },
119
+ c: 2,
120
+ });
121
+ const out = [];
122
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => out.push(patches));
123
+ delete state.a.b;
124
+ delete state.c;
125
+ await Promise.resolve();
126
+ (0, vitest_1.expect)(out).toHaveLength(1);
127
+ (0, vitest_1.expect)(out[0]).toEqual([
128
+ { op: "remove", path: ["a", "b"] },
129
+ { op: "remove", path: ["c"] },
130
+ ]);
131
+ stop();
132
+ });
133
+ (0, vitest_1.it)("filters out patches from other roots", async () => {
134
+ const a = (0, __1.deepSignal)({ x: 1 });
135
+ const b = (0, __1.deepSignal)({ y: 2 });
136
+ const out = [];
137
+ const { stopListening: stop } = (0, watch_1.watch)(a, ({ patches }) => out.push(patches));
138
+ b.y = 3;
139
+ a.x = 2;
140
+ await Promise.resolve();
141
+ (0, vitest_1.expect)(out).toHaveLength(1);
142
+ (0, vitest_1.expect)(out[0]).toEqual([{ op: "add", path: ["x"], value: 2 }]);
143
+ stop();
144
+ });
145
+ (0, vitest_1.it)("emits patches for Set structural mutations (add/delete)", async () => {
146
+ const state = (0, __1.deepSignal)({ s: new Set([1, 2]) });
147
+ const batches = [];
148
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => batches.push(patches));
149
+ state.s.add(3);
150
+ state.s.delete(1);
151
+ await Promise.resolve();
152
+ const flattened = batches.flat();
153
+ (0, vitest_1.expect)(flattened).toEqual([
154
+ { op: "add", path: ["s"], type: "set", value: [3] },
155
+ { op: "remove", path: ["s"], type: "set", value: 1 },
156
+ ]);
157
+ stop();
158
+ });
159
+ (0, vitest_1.it)("emits patches for nested objects added after initialization", async () => {
160
+ const state = (0, __1.deepSignal)({ root: {} });
161
+ const patches = [];
162
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(batch));
163
+ state.root.child = { level: { value: 1 }, l1: "val" };
164
+ await Promise.resolve();
165
+ const flattened = patches.flat();
166
+ (0, vitest_1.expect)(flattened).toEqual([
167
+ { op: "add", path: ["root", "child"], value: {} },
168
+ { op: "add", path: ["root", "child", "level"], value: {} },
169
+ {
170
+ op: "add",
171
+ path: ["root", "child", "level", "value"],
172
+ value: 1,
173
+ },
174
+ { op: "add", path: ["root", "child", "l1"], value: "val" },
175
+ ]);
176
+ stop();
177
+ });
178
+ (0, vitest_1.it)("emits patches for Set with primitives added as one operation", async () => {
179
+ const state = (0, __1.deepSignal)({ container: {} }, {
180
+ syntheticIdPropertyName: "id",
181
+ });
182
+ const patches = [];
183
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(...batch));
184
+ state.container.items = new Set(["item1"]);
185
+ state.container.items.add("item2");
186
+ await Promise.resolve();
187
+ (0, vitest_1.expect)(patches).toHaveLength(3);
188
+ (0, vitest_1.expect)(patches[0]).toMatchObject({
189
+ op: "add",
190
+ path: ["container", "items"],
191
+ type: "set",
192
+ });
193
+ (0, vitest_1.expect)(patches[1]).toMatchObject({
194
+ op: "add",
195
+ path: ["container", "items"],
196
+ type: "set",
197
+ value: ["item1"],
198
+ });
199
+ (0, vitest_1.expect)(patches[2]).toMatchObject({
200
+ op: "add",
201
+ path: ["container", "items"],
202
+ type: "set",
203
+ value: ["item2"],
204
+ });
205
+ stop();
206
+ });
207
+ (0, vitest_1.it)("emits patches for deeply nested arrays and objects", async () => {
208
+ const state = (0, __1.deepSignal)({ data: null });
209
+ const patches = [];
210
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(batch));
211
+ state.data = {
212
+ users: [
213
+ {
214
+ id: 1,
215
+ profile: { name: "Alice", settings: { theme: "dark" } },
216
+ },
217
+ {
218
+ id: 2,
219
+ profile: { name: "Bob", settings: { theme: "light" } },
220
+ },
221
+ ],
222
+ meta: { count: 2, active: true },
223
+ };
224
+ await Promise.resolve();
225
+ const flattened = patches.flat();
226
+ (0, vitest_1.expect)(flattened).toEqual([
227
+ { op: "add", path: ["data"], value: {}, type: undefined },
228
+ { op: "add", path: ["data", "users"], value: [], type: undefined },
229
+ {
230
+ op: "add",
231
+ path: ["data", "users", 0],
232
+ value: {},
233
+ type: undefined,
234
+ },
235
+ {
236
+ op: "add",
237
+ path: ["data", "users", 0, "id"],
238
+ value: 1,
239
+ },
240
+ {
241
+ op: "add",
242
+ path: ["data", "users", 0, "profile"],
243
+ value: {},
244
+ type: undefined,
245
+ },
246
+ {
247
+ op: "add",
248
+ path: ["data", "users", 0, "profile", "name"],
249
+ value: "Alice",
250
+ },
251
+ {
252
+ op: "add",
253
+ path: ["data", "users", 0, "profile", "settings"],
254
+ value: {},
255
+ type: undefined,
256
+ },
257
+ {
258
+ op: "add",
259
+ path: ["data", "users", 0, "profile", "settings", "theme"],
260
+ value: "dark",
261
+ },
262
+ {
263
+ op: "add",
264
+ path: ["data", "users", 1],
265
+ value: {},
266
+ type: undefined,
267
+ },
268
+ { op: "add", path: ["data", "users", 1, "id"], value: 2 },
269
+ {
270
+ op: "add",
271
+ path: ["data", "users", 1, "profile"],
272
+ value: {},
273
+ type: undefined,
274
+ },
275
+ {
276
+ op: "add",
277
+ path: ["data", "users", 1, "profile", "name"],
278
+ value: "Bob",
279
+ },
280
+ {
281
+ op: "add",
282
+ path: ["data", "users", 1, "profile", "settings"],
283
+ value: {},
284
+ type: undefined,
285
+ },
286
+ {
287
+ op: "add",
288
+ path: ["data", "users", 1, "profile", "settings", "theme"],
289
+ value: "light",
290
+ },
291
+ { op: "add", path: ["data", "meta"], value: {}, type: undefined },
292
+ { op: "add", path: ["data", "meta", "count"], value: 2 },
293
+ { op: "add", path: ["data", "meta", "active"], value: true },
294
+ ]);
295
+ stop();
296
+ });
297
+ (0, vitest_1.it)("emits patches for Set with nested objects added as one operation", async () => {
298
+ const state = (0, __1.deepSignal)({ container: {} }, {
299
+ syntheticIdPropertyName: "id",
300
+ propGenerator: ({ object }) => ({ syntheticId: object.id }),
301
+ });
302
+ const patches = [];
303
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(batch));
304
+ state.container.items = new Set([
305
+ { id: "a", data: { nested: { value: 1 } } },
306
+ { id: "b", data: { nested: { value: 2 } } },
307
+ ]);
308
+ await Promise.resolve();
309
+ const flattened = patches.flat();
310
+ (0, vitest_1.expect)(flattened).toEqual([
311
+ { op: "add", path: ["container", "items"], type: "set", value: [] },
312
+ { op: "add", path: ["container", "items", "a"], value: {} },
313
+ { op: "add", path: ["container", "items", "a", "id"], value: "a" },
314
+ {
315
+ op: "add",
316
+ path: ["container", "items", "a", "data"],
317
+ value: {},
318
+ },
319
+ {
320
+ op: "add",
321
+ path: ["container", "items", "a", "data", "nested"],
322
+ value: {},
323
+ },
324
+ {
325
+ op: "add",
326
+ path: ["container", "items", "a", "data", "nested", "value"],
327
+ value: 1,
328
+ },
329
+ { op: "add", path: ["container", "items", "b"], value: {} },
330
+ { op: "add", path: ["container", "items", "b", "id"], value: "b" },
331
+ {
332
+ op: "add",
333
+ path: ["container", "items", "b", "data"],
334
+ value: {},
335
+ },
336
+ {
337
+ op: "add",
338
+ path: ["container", "items", "b", "data", "nested"],
339
+ value: {},
340
+ },
341
+ {
342
+ op: "add",
343
+ path: ["container", "items", "b", "data", "nested", "value"],
344
+ value: 2,
345
+ },
346
+ ]);
347
+ stop();
348
+ });
349
+ (0, vitest_1.it)("emits structural patches for sets of sets", async () => {
350
+ const innerA = new Set([{ id: "node1", x: 1 }]);
351
+ const s = new Set([innerA]);
352
+ const state = (0, __1.deepSignal)({ graph: s });
353
+ const batches = [];
354
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => batches.push(patches));
355
+ const innerB = new Set([{ id: "node2", x: 5 }]);
356
+ state.graph.add(innerB);
357
+ [...innerA][0].x = 2;
358
+ await Promise.resolve();
359
+ const pathStrings = batches.flat().map((p) => p.path.join("."));
360
+ (0, vitest_1.expect)(pathStrings.some((p) => p.startsWith("graph."))).toBe(true);
361
+ stop();
362
+ });
363
+ (0, vitest_1.it)("tracks deep nested object mutation inside a Set entry after iteration", async () => {
364
+ const rawEntry = { id: "n1", data: { val: 1 } };
365
+ const st = (0, __1.deepSignal)({ bag: new Set([rawEntry]) }, {
366
+ syntheticIdPropertyName: "id",
367
+ propGenerator: ({ object }) => ({ syntheticId: object.id }),
368
+ });
369
+ const collected = [];
370
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => collected.push(patches));
371
+ let proxied;
372
+ for (const e of st.bag.values()) {
373
+ proxied = e;
374
+ e.data.val;
375
+ }
376
+ proxied.data.val = 2;
377
+ await Promise.resolve();
378
+ const flat = collected.flat().map((p) => p.path.join("."));
379
+ (0, vitest_1.expect)(flat.some((p) => p.endsWith("n1.data.val"))).toBe(true);
380
+ stop();
381
+ });
382
+ (0, vitest_1.it)("allows custom synthetic id for Set entry", async () => {
383
+ const node = { name: "x" };
384
+ const state = (0, __1.deepSignal)({ s: new Set() });
385
+ const collected2 = [];
386
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => collected2.push(patches));
387
+ (0, __1.addWithId)(state.s, node, "custom123");
388
+ await Promise.resolve();
389
+ const flat = collected2.flat().map((p) => p.path.join("."));
390
+ (0, vitest_1.expect)(flat.some((p) => p === "s.custom123")).toBe(true);
391
+ stop();
392
+ });
393
+ (0, vitest_1.it)("should apply extraProps returned from propGenerator when adding objects to Set", async () => {
394
+ const options = {
395
+ propGenerator: ({ path, object }) => {
396
+ // Generate @graph and @id if not present or empty
397
+ const graphIri = !object["@graph"] || object["@graph"] === ""
398
+ ? "did:ng:test-graph"
399
+ : object["@graph"];
400
+ const subjectIri = !object["@id"] || object["@id"] === ""
401
+ ? `did:ng:test-subject-${Math.random()}`
402
+ : object["@id"];
403
+ return {
404
+ extraProps: { "@id": subjectIri, "@graph": graphIri },
405
+ syntheticId: graphIri + "|" + subjectIri,
406
+ };
407
+ },
408
+ };
409
+ const state = (0, __1.deepSignal)(new Set(), options);
410
+ const patches = [];
411
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(batch));
412
+ // Add object with empty @graph and @id (should be replaced)
413
+ const newObj = { "@graph": "", "@id": "", name: "Test Object" };
414
+ state.add(newObj);
415
+ await Promise.resolve();
416
+ // Check that extraProps were applied to the object
417
+ (0, vitest_1.expect)(newObj["@id"]).toBeDefined();
418
+ (0, vitest_1.expect)(newObj["@graph"]).toBeDefined();
419
+ (0, vitest_1.expect)(newObj["@graph"]).toBe("did:ng:test-graph");
420
+ // Check that patches were emitted with the generated values
421
+ const allPatches = patches.flat();
422
+ const idPatch = allPatches.find((p) => p.path.length === 2 && p.path[1] === "@id");
423
+ const graphPatch = allPatches.find((p) => p.path.length === 2 && p.path[1] === "@graph");
424
+ (0, vitest_1.expect)(idPatch).toBeDefined();
425
+ (0, vitest_1.expect)(idPatch?.op).toBe("add");
426
+ (0, vitest_1.expect)(idPatch?.value).toBeDefined();
427
+ (0, vitest_1.expect)(idPatch?.value?.startsWith("did:ng:test-subject")).toBeTruthy();
428
+ (0, vitest_1.expect)(graphPatch).toBeDefined();
429
+ (0, vitest_1.expect)(graphPatch?.op).toBe("add");
430
+ (0, vitest_1.expect)(graphPatch?.value).toBe("did:ng:test-graph");
431
+ stop();
432
+ });
433
+ (0, vitest_1.describe)("Set", () => {
434
+ (0, vitest_1.it)("emits patches for primitive adds", async () => {
435
+ const st = (0, __1.deepSignal)({ s: new Set() });
436
+ const batches = [];
437
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
438
+ st.s.add(true);
439
+ st.s.add(2);
440
+ st.s.add("3");
441
+ await Promise.resolve();
442
+ (0, vitest_1.expect)(batches.length).toBe(1);
443
+ const patches = batches[0];
444
+ (0, vitest_1.expect)(patches.length).toBe(3);
445
+ // All patches should have the same path (the Set itself)
446
+ patches.forEach((p) => {
447
+ (0, vitest_1.expect)(p.path.join(".")).toBe("s");
448
+ (0, vitest_1.expect)(p.op).toBe("add");
449
+ (0, vitest_1.expect)(p.type).toBe("set");
450
+ });
451
+ // Check that values are in the value field, not in path
452
+ const values = patches.map((p) => p.value[0]);
453
+ (0, vitest_1.expect)(values).toContain(true);
454
+ (0, vitest_1.expect)(values).toContain(2);
455
+ (0, vitest_1.expect)(values).toContain("3");
456
+ stop();
457
+ });
458
+ (0, vitest_1.it)("emits patches for primitive deletes", async () => {
459
+ const st = (0, __1.deepSignal)({ s: new Set([true, 2, "3"]) });
460
+ const batches = [];
461
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
462
+ st.s.delete(true);
463
+ st.s.delete(2);
464
+ await Promise.resolve();
465
+ (0, vitest_1.expect)(batches.length).toBe(1);
466
+ const patches = batches[0];
467
+ (0, vitest_1.expect)(patches.length).toBe(2);
468
+ // All patches should have the same path (the Set itself)
469
+ patches.forEach((p) => {
470
+ (0, vitest_1.expect)(p.path.join(".")).toBe("s");
471
+ (0, vitest_1.expect)(p.op).toBe("remove");
472
+ (0, vitest_1.expect)(p.type).toBe("set");
473
+ });
474
+ // Check that values are in the value field
475
+ const values = patches.map((p) => p.value);
476
+ (0, vitest_1.expect)(values).toContain(true);
477
+ (0, vitest_1.expect)(values).toContain(2);
478
+ stop();
479
+ });
480
+ (0, vitest_1.it)("does not emit patches for non-existent primitives", async () => {
481
+ const st = (0, __1.deepSignal)({ s: new Set([1, 2]) });
482
+ const batches = [];
483
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
484
+ st.s.delete("nonexistent");
485
+ st.s.delete(999);
486
+ await Promise.resolve();
487
+ (0, vitest_1.expect)(batches.length).toBe(0);
488
+ stop();
489
+ });
490
+ (0, vitest_1.it)("does not emit patches for already added primitive", async () => {
491
+ const st = (0, __1.deepSignal)({ s: new Set([1, "test", true]) });
492
+ const batches = [];
493
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
494
+ st.s.add(1);
495
+ st.s.add("test");
496
+ st.s.add(true);
497
+ await Promise.resolve();
498
+ (0, vitest_1.expect)(batches.length).toBe(0);
499
+ stop();
500
+ });
501
+ (0, vitest_1.it)("emits single structural patch on Set.clear()", async () => {
502
+ const st = (0, __1.deepSignal)({ s: new Set() });
503
+ (0, __1.addWithId)(st.s, { id: "a", x: 1 }, "a");
504
+ (0, __1.addWithId)(st.s, { id: "b", x: 2 }, "b");
505
+ const batches = [];
506
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
507
+ st.s.clear();
508
+ await Promise.resolve();
509
+ // clear() emits a single structural patch for the Set itself (op: "add", value: [])
510
+ const structuralPatches = batches
511
+ .flat()
512
+ .filter((p) => p.path.length === 1 && p.path[0] === "s");
513
+ (0, vitest_1.expect)(structuralPatches.length).toBe(1);
514
+ (0, vitest_1.expect)(structuralPatches[0].op).toBe("add");
515
+ (0, vitest_1.expect)(structuralPatches[0].value).toEqual([]);
516
+ stop();
517
+ });
518
+ (0, vitest_1.it)("emits delete patch for object entry", async () => {
519
+ const st = (0, __1.deepSignal)({ s: new Set() }, {
520
+ syntheticIdPropertyName: "id",
521
+ propGenerator: ({ object }) => ({ syntheticId: object.id }),
522
+ });
523
+ const obj = { id: "n1", x: 1 };
524
+ const patches = [];
525
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches: batch }) => patches.push(batch));
526
+ st.s.add(obj);
527
+ st.s.delete(obj);
528
+ await Promise.resolve();
529
+ const all = patches
530
+ .flat()
531
+ .filter((p) => p.op === "remove")
532
+ .map((p) => p.path.join("."));
533
+ (0, vitest_1.expect)(all).toContain("s.n1");
534
+ stop();
535
+ });
536
+ (0, vitest_1.it)("does not emit patch for duplicate add", async () => {
537
+ const st = (0, __1.deepSignal)({ s: new Set([1]) });
538
+ const patches = [];
539
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches: batch }) => patches.push(batch));
540
+ st.s.add(1);
541
+ await Promise.resolve();
542
+ (0, vitest_1.expect)(patches.length).toBe(0);
543
+ stop();
544
+ });
545
+ (0, vitest_1.it)("does not emit patch deleting non-existent entry", async () => {
546
+ const st = (0, __1.deepSignal)({ s: new Set([1]) });
547
+ const patches = [];
548
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches: batch }) => patches.push(batch));
549
+ st.s.delete(2);
550
+ await Promise.resolve();
551
+ (0, vitest_1.expect)(patches.length).toBe(0);
552
+ stop();
553
+ });
554
+ (0, vitest_1.it)("addWithId primitive returns primitive and emits patch with primitive key", async () => {
555
+ const st = (0, __1.deepSignal)({ s: new Set() });
556
+ const patches = [];
557
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches: batch }) => patches.push(batch));
558
+ const ret = (0, __1.addWithId)(st.s, 5, "ignored");
559
+ (0, vitest_1.expect)(ret).toBe(5);
560
+ await Promise.resolve();
561
+ // For primitives, path should be just "s" and value should be in the value field
562
+ const paths = patches.flat().map((p) => p.path.join("."));
563
+ (0, vitest_1.expect)(paths).toContain("s");
564
+ const values = patches.flat().map((p) => p.value?.[0]);
565
+ (0, vitest_1.expect)(values).toContain(5);
566
+ stop();
567
+ });
568
+ (0, vitest_1.it)("setSetEntrySyntheticId applies custom id without helper", async () => {
569
+ const st = (0, __1.deepSignal)({ s: new Set() });
570
+ const obj = { name: "x" };
571
+ (0, __1.setSetEntrySyntheticId)(obj, "customX");
572
+ const patches = [];
573
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches: batch }) => patches.push(batch));
574
+ st.s.add(obj);
575
+ await Promise.resolve();
576
+ const paths = patches.flat().map((p) => p.path.join("."));
577
+ (0, vitest_1.expect)(paths).toContain("s.customX");
578
+ stop();
579
+ });
580
+ (0, vitest_1.it)("values/entries/forEach proxy nested mutation", async () => {
581
+ const st = (0, __1.deepSignal)({ s: new Set() });
582
+ const entry = (0, __1.addWithId)(st.s, { id: "e1", inner: { v: 1 } }, "e1");
583
+ const batches = [];
584
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
585
+ for (const e of st.s.values()) {
586
+ e.inner.v;
587
+ }
588
+ entry.inner.v = 2;
589
+ await Promise.resolve();
590
+ const vPaths = batches.flat().map((p) => p.path.join("."));
591
+ (0, vitest_1.expect)(vPaths.some((p) => p.endsWith("e1.inner.v"))).toBe(true);
592
+ stop();
593
+ });
594
+ (0, vitest_1.it)("raw reference mutation produces no deep patch while proxied does", async () => {
595
+ const raw = { id: "id1", data: { x: 1 } };
596
+ const st = (0, __1.deepSignal)({ s: new Set([raw]) }, {
597
+ syntheticIdPropertyName: "id",
598
+ propGenerator: ({ object }) => ({ syntheticId: object.id }),
599
+ });
600
+ const batches = [];
601
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
602
+ raw.data.x = 2;
603
+ await Promise.resolve();
604
+ const afterRaw = batches.flat().map((p) => p.path.join("."));
605
+ (0, vitest_1.expect)(afterRaw.some((p) => p.endsWith("id1.data.x"))).toBe(false);
606
+ let proxied;
607
+ for (const e of st.s.values())
608
+ proxied = e;
609
+ proxied.data.x = 3;
610
+ await Promise.resolve();
611
+ const afterProxied = batches.flat().map((p) => p.path.join("."));
612
+ (0, vitest_1.expect)(afterProxied.some((p) => p.endsWith("id1.data.x"))).toBe(true);
613
+ stop();
614
+ });
615
+ (0, vitest_1.it)("synthetic id collision assigns unique blank node id", async () => {
616
+ const st = (0, __1.deepSignal)({ s: new Set() });
617
+ const a1 = { id: "dup", v: 1 };
618
+ const a2 = { id: "dup", v: 2 };
619
+ const patches = [];
620
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches: batch }) => patches.push(batch));
621
+ st.s.add(a1);
622
+ st.s.add(a2);
623
+ await Promise.resolve();
624
+ // Filter for Set structural patches only (path length 2: ['s', syntheticId])
625
+ const setAddPatches = patches
626
+ .flat()
627
+ .filter((p) => p.op === "add" &&
628
+ p.path.length === 2 &&
629
+ p.path[0] === "s");
630
+ const keys = setAddPatches.map((p) => p.path.slice(-1)[0]);
631
+ // Both objects should have unique synthetic IDs despite id collision
632
+ (0, vitest_1.expect)(new Set(keys).size).toBe(2);
633
+ stop();
634
+ });
635
+ (0, vitest_1.it)("allows Array.from() and spread on Set without brand errors and tracks nested mutation", async () => {
636
+ const st = (0, __1.deepSignal)({
637
+ s: new Set([{ id: "eIter", inner: { v: 1 } }]),
638
+ }, {
639
+ syntheticIdPropertyName: "id",
640
+ propGenerator: ({ object }) => ({ syntheticId: object.id }),
641
+ });
642
+ // Regression: previously 'values method called on incompatible Proxy' was thrown here.
643
+ const arr = Array.from(st.s);
644
+ (0, vitest_1.expect)(arr.length).toBe(1);
645
+ (0, vitest_1.expect)(arr[0].inner.v).toBe(1);
646
+ const spread = [...st.s];
647
+ (0, vitest_1.expect)(spread[0].inner.v).toBe(1);
648
+ const batches = [];
649
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
650
+ spread[0].inner.v = 2; // mutate nested field of iterated (proxied) entry
651
+ await Promise.resolve();
652
+ const flat = batches.flat().map((p) => p.path.join("."));
653
+ (0, vitest_1.expect)(flat.some((p) => p.endsWith("eIter.inner.v"))).toBe(true);
654
+ stop();
655
+ });
656
+ (0, vitest_1.it)("generates correct patches when root is a Set (primitive entries)", async () => {
657
+ const rootSet = (0, __1.deepSignal)(new Set());
658
+ const batches = [];
659
+ const { stopListening: stop } = (0, watch_1.watch)(rootSet, ({ patches }) => batches.push(patches));
660
+ rootSet.add(1);
661
+ rootSet.add("test");
662
+ rootSet.add(true);
663
+ await Promise.resolve();
664
+ (0, vitest_1.expect)(batches.length).toBe(1);
665
+ const patches = batches[0];
666
+ (0, vitest_1.expect)(patches.length).toBe(3);
667
+ // When root is a Set, path should be empty array for primitive adds
668
+ patches.forEach((p) => {
669
+ (0, vitest_1.expect)(p.path).toEqual([]);
670
+ (0, vitest_1.expect)(p.op).toBe("add");
671
+ (0, vitest_1.expect)(p.type).toBe("set");
672
+ });
673
+ const values = patches.map((p) => p.value[0]);
674
+ (0, vitest_1.expect)(values).toContain(1);
675
+ (0, vitest_1.expect)(values).toContain("test");
676
+ (0, vitest_1.expect)(values).toContain(true);
677
+ stop();
678
+ });
679
+ (0, vitest_1.it)("generates correct patches when root is a Set (object entries)", async () => {
680
+ const rootSet = (0, __1.deepSignal)(new Set(), {
681
+ propGenerator: ({ object }) => ({
682
+ syntheticId: object["@id"] || `fallback-${Math.random()}`,
683
+ }),
684
+ syntheticIdPropertyName: "@id",
685
+ });
686
+ const batches = [];
687
+ const { stopListening: stop } = (0, watch_1.watch)(rootSet, ({ patches }) => batches.push(patches));
688
+ const obj1 = { "@id": "obj1", value: 1 };
689
+ const obj2 = { "@id": "obj2", value: 2 };
690
+ rootSet.add(obj1);
691
+ rootSet.add(obj2);
692
+ await Promise.resolve();
693
+ const flat = batches.flat().map((p) => p.path.join("."));
694
+ // When root is a Set, first element of path should be synthetic id
695
+ (0, vitest_1.expect)(flat).toContain("obj1");
696
+ (0, vitest_1.expect)(flat).toContain("obj1.@id");
697
+ (0, vitest_1.expect)(flat).toContain("obj1.value");
698
+ (0, vitest_1.expect)(flat).toContain("obj2");
699
+ (0, vitest_1.expect)(flat).toContain("obj2.@id");
700
+ (0, vitest_1.expect)(flat).toContain("obj2.value");
701
+ stop();
702
+ });
703
+ (0, vitest_1.it)("tracks nested mutations when root is a Set", async () => {
704
+ const rootSet = (0, __1.deepSignal)(new Set(), {
705
+ syntheticIdPropertyName: "id",
706
+ propGenerator: ({ object }) => ({ syntheticId: object.id }),
707
+ });
708
+ const obj = { id: "nested", data: { x: 1 } };
709
+ rootSet.add(obj);
710
+ const batches = [];
711
+ const { stopListening: stop } = (0, watch_1.watch)(rootSet, ({ patches }) => batches.push(patches));
712
+ // Get the proxied entry
713
+ let proxied;
714
+ for (const e of rootSet.values()) {
715
+ proxied = e;
716
+ }
717
+ proxied.data.x = 2;
718
+ await Promise.resolve();
719
+ const flat = batches.flat().map((p) => p.path.join("."));
720
+ (0, vitest_1.expect)(flat.some((p) => p === "nested.data.x")).toBe(true);
721
+ stop();
722
+ });
723
+ });
724
+ (0, vitest_1.describe)("Arrays & mixed batch", () => {
725
+ (0, vitest_1.it)("emits patches for splice/unshift/shift in single batch", async () => {
726
+ const st = (0, __1.deepSignal)({ arr: [1, 2, 3] });
727
+ const batches = [];
728
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => {
729
+ batches.push(patches);
730
+ });
731
+ st.arr.splice(1, 1, 99, 100);
732
+ st.arr.unshift(0);
733
+ st.arr.shift();
734
+ await Promise.resolve();
735
+ (0, vitest_1.expect)(st.arr).toEqual([1, 99, 100, 3]);
736
+ (0, vitest_1.expect)(batches[0]).toEqual([
737
+ { path: ["arr", "1"], op: "remove" },
738
+ { path: ["arr", "1"], op: "add", value: 100 },
739
+ { path: ["arr", "1"], op: "add", value: 99 },
740
+ { path: ["arr", "0"], op: "add", value: 0 },
741
+ { path: ["arr", "0"], op: "remove" },
742
+ ]);
743
+ stop();
744
+ });
745
+ (0, vitest_1.it)("emits patches adding new array", async () => {
746
+ const obj = (0, __1.deepSignal)({});
747
+ const batches = [];
748
+ const { stopListening: stop } = (0, watch_1.watch)(obj, ({ patches }) => {
749
+ batches.push(patches);
750
+ });
751
+ obj.arr = [1, 2, 3];
752
+ await Promise.resolve();
753
+ (0, vitest_1.expect)(batches[0]).toEqual([
754
+ { path: ["arr"], op: "add", value: [] },
755
+ { path: ["arr", 0], op: "add", value: 1 },
756
+ { path: ["arr", 1], op: "add", value: 2 },
757
+ { path: ["arr", 2], op: "add", value: 3 },
758
+ ]);
759
+ stop();
760
+ });
761
+ (0, vitest_1.it)("emits remove patches when array length shrinks", async () => {
762
+ const st = (0, __1.deepSignal)([1, 0, 3, -1]);
763
+ const batches = [];
764
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => {
765
+ batches.push(patches);
766
+ });
767
+ st.length = 2;
768
+ await Promise.resolve();
769
+ (0, vitest_1.expect)(st).toEqual([1, 0]);
770
+ (0, vitest_1.expect)(batches[0]).toEqual([
771
+ { path: ["3"], op: "remove" },
772
+ { path: ["2"], op: "remove" },
773
+ ]);
774
+ stop();
775
+ });
776
+ (0, vitest_1.it)("mixed object/array/Set mutations batch together", async () => {
777
+ const st = (0, __1.deepSignal)({ o: { a: 1 }, arr: [1], s: new Set() });
778
+ const batches = [];
779
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
780
+ st.o.a = 2;
781
+ st.arr.push(2);
782
+ (0, __1.addWithId)(st.s, { id: "z", v: 1 }, "z");
783
+ await Promise.resolve();
784
+ (0, vitest_1.expect)(batches.length).toBe(1);
785
+ const paths = batches[0].map((p) => p.path.join("."));
786
+ (0, vitest_1.expect)(paths).toContain("o.a");
787
+ (0, vitest_1.expect)(paths).toContain("arr.1");
788
+ (0, vitest_1.expect)(paths.some((p) => p.startsWith("s."))).toBe(true);
789
+ stop();
790
+ });
791
+ (0, vitest_1.it)("mutating existing Set property emits correct patches", async () => {
792
+ const obj2 = {
793
+ "@id": "urn:test:obj2|did:ng:o:xypN3xdPs4ozIXaCFOQ5tDmq86q0szbqtCQmcqTcMTcA:v:QvO7rDIp1MCUs3Xec-yQBl0kHjMmLzoUOU4m6qy6b4EA",
794
+ setProperty: new Set([1, 2, 3]),
795
+ };
796
+ const st = (0, __1.deepSignal)({ items: new Set([obj2]) }, {
797
+ syntheticIdPropertyName: "@id",
798
+ propGenerator: ({ object }) => ({
799
+ syntheticId: object["@id"],
800
+ }),
801
+ });
802
+ // Get the proxied object from the set
803
+ let proxiedObj;
804
+ for (const item of st.items) {
805
+ proxiedObj = item;
806
+ }
807
+ const batches = [];
808
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
809
+ // Modify the Set property: remove one value and add another
810
+ proxiedObj.setProperty.delete(3);
811
+ proxiedObj.setProperty.add(4);
812
+ proxiedObj.setProperty.add(5);
813
+ await Promise.resolve();
814
+ const patches = batches.flat();
815
+ // Should NOT have an object patch for the Set itself when modifying existing Set
816
+ const objectPatches = patches.filter((p) => p.type === "object");
817
+ (0, vitest_1.expect)(objectPatches.length).toBe(0);
818
+ // Should have remove patch for deleted value
819
+ const removePatches = patches.filter((p) => p.op === "remove");
820
+ (0, vitest_1.expect)(removePatches.length).toBe(1);
821
+ (0, vitest_1.expect)(removePatches[0].value).toBe(3);
822
+ // Should have add patches for new values (primitives in Set)
823
+ const addPatches = patches.filter((p) => p.op === "add");
824
+ (0, vitest_1.expect)(addPatches.length).toBe(2);
825
+ const addedValues = addPatches.map((p) => p.value[0]);
826
+ (0, vitest_1.expect)(addedValues).toContain(4);
827
+ (0, vitest_1.expect)(addedValues).toContain(5);
828
+ stop();
829
+ });
830
+ (0, vitest_1.it)("reassigning Set property should not emit deep patches for all elements", async () => {
831
+ const obj2 = {
832
+ "@id": "urn:test:obj2|did:ng:o:xypN3xdPs4ozIXaCFOQ5tDmq86q0szbqtCQmcqTcMTcA:v:QvO7rDIp1MCUs3Xec-yQBl0kHjMmLzoUOU4m6qy6b4EA",
833
+ setProperty: new Set([1, 2, 3]),
834
+ };
835
+ const st = (0, __1.deepSignal)({ items: new Set([obj2]) }, {
836
+ syntheticIdPropertyName: "@id",
837
+ propGenerator: ({ object }) => ({
838
+ syntheticId: object["@id"],
839
+ }),
840
+ });
841
+ // Get the proxied object from the set
842
+ let proxiedObj;
843
+ for (const item of st.items) {
844
+ proxiedObj = item;
845
+ }
846
+ const batches = [];
847
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
848
+ // Reassign the Set property to a new Set with different values
849
+ proxiedObj.setProperty = new Set([4, 5]);
850
+ await Promise.resolve();
851
+ const patches = batches.flat();
852
+ // When reassigning a complex object property (Set/Array/Object) to a new value,
853
+ // we should NOT emit:
854
+ // 1. An "object" type patch for the container
855
+ // 2. Individual "add" patches for each element
856
+ //
857
+ // This was the bug reported: replacing a Set emitted patches like:
858
+ // [{ path: [..., "setProperty"], op: "add", value: {} },
859
+ // { path: [..., "setProperty", 4], op: "add", value: 4 },
860
+ // { path: [..., "setProperty", 5], op: "add", value: 5 }]
861
+ //
862
+ // The correct behavior: no deep patches for the reassignment.
863
+ // Subsequent mutations (add/delete) on the new Set will emit proper patches.
864
+ const objectPatches = patches.filter((p) => p.type === "object");
865
+ (0, vitest_1.expect)(objectPatches.length).toBe(0);
866
+ // No individual element patches should be emitted for the initial elements
867
+ const elementPatches = patches.filter((p) => (p.path.length > 2 && p.path[p.path.length - 1] === 4) ||
868
+ p.path[p.path.length - 1] === 5);
869
+ (0, vitest_1.expect)(elementPatches.length).toBe(0);
870
+ // Verify that subsequent operations on the new Set DO emit patches
871
+ proxiedObj.setProperty.add(6);
872
+ await Promise.resolve();
873
+ const laterPatches = batches.flat();
874
+ const patch6 = laterPatches.find((p) => p.value?.[0] === 6 || p.value === 6);
875
+ (0, vitest_1.expect)(patch6).toBeDefined();
876
+ stop();
877
+ });
878
+ (0, vitest_1.it)("calls propGenerator for objects with nested Set structures (Set inside Set)", async () => {
879
+ const propGeneratorCalls = [];
880
+ const st = (0, __1.deepSignal)({ items: new Set() }, {
881
+ syntheticIdPropertyName: "@id",
882
+ propGenerator: ({ object, path, inSet }) => {
883
+ propGeneratorCalls.push({ object, path, inSet });
884
+ return {
885
+ syntheticId: object["@id"] || `fallback-${Math.random()}`,
886
+ };
887
+ },
888
+ });
889
+ const batches = [];
890
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
891
+ // Create nested objects that will be in the inner Set
892
+ const innerObj1 = { "@id": "inner1", value: "nested1" };
893
+ const innerObj2 = { "@id": "inner2", value: "nested2" };
894
+ const nestedSet = new Set([innerObj1, innerObj2]);
895
+ // Create outer object with nested Set
896
+ const outerObj = {
897
+ "@id": "outer-with-nested-set",
898
+ nestedSet: nestedSet,
899
+ };
900
+ st.items.add(outerObj);
901
+ await Promise.resolve();
902
+ // Verify propGenerator was called for the outer object
903
+ const outerCall = propGeneratorCalls.find((call) => call.object === outerObj);
904
+ (0, vitest_1.expect)(outerCall).toBeDefined();
905
+ (0, vitest_1.expect)(outerCall.object["@id"]).toBe("outer-with-nested-set");
906
+ // Verify propGenerator was called for inner objects
907
+ const inner1Call = propGeneratorCalls.find((call) => call.object === innerObj1);
908
+ (0, vitest_1.expect)(inner1Call).toBeDefined();
909
+ (0, vitest_1.expect)(inner1Call.object["@id"]).toBe("inner1");
910
+ const inner2Call = propGeneratorCalls.find((call) => call.object === innerObj2);
911
+ (0, vitest_1.expect)(inner2Call).toBeDefined();
912
+ (0, vitest_1.expect)(inner2Call.object["@id"]).toBe("inner2");
913
+ // Verify patches were emitted correctly
914
+ const patches = batches.flat();
915
+ const flat = patches.map((p) => p.path.join("."));
916
+ // Should have patch for the outer object
917
+ (0, vitest_1.expect)(flat.some((p) => p.startsWith("items.outer-with-nested-set"))).toBe(true);
918
+ // Should have patches for the nested Set and its objects
919
+ (0, vitest_1.expect)(flat.some((p) => p.includes("nestedSet"))).toBe(true);
920
+ (0, vitest_1.expect)(flat.some((p) => p.includes("inner1"))).toBe(true);
921
+ (0, vitest_1.expect)(flat.some((p) => p.includes("inner2"))).toBe(true);
922
+ stop();
923
+ });
924
+ (0, vitest_1.it)("calls propGenerator for deeply nested Sets (multiple levels)", async () => {
925
+ const propGeneratorCalls = [];
926
+ const st = (0, __1.deepSignal)(new Set(), {
927
+ syntheticIdPropertyName: "@id",
928
+ propGenerator: ({ object, path, inSet }) => {
929
+ propGeneratorCalls.push({ object, path, inSet });
930
+ return {
931
+ syntheticId: object["@id"] + "-withCustomString",
932
+ };
933
+ },
934
+ });
935
+ const batches = [];
936
+ const { stopListening: stop } = (0, watch_1.watch)(st, ({ patches }) => batches.push(patches));
937
+ // Create structure matching real use case:
938
+ // Root Set contains objects with did:ng:o: IDs
939
+ // Those objects have properties that are Sets of more objects
940
+ const innerObj = { "@id": "innerId", prop2: "initial value" };
941
+ const anotherObjectSet = new Set([innerObj]);
942
+ const outerObj = {
943
+ "@id": "outerId",
944
+ anotherObject: anotherObjectSet,
945
+ };
946
+ st.add(outerObj);
947
+ await Promise.resolve();
948
+ // Verify propGenerator was called for all objects
949
+ (0, vitest_1.expect)(propGeneratorCalls.some((call) => call.object === outerObj)).toBe(true);
950
+ (0, vitest_1.expect)(propGeneratorCalls.some((call) => call.object === innerObj)).toBe(true);
951
+ // Clear batches for mutation test
952
+ batches.length = 0;
953
+ // Get proxied objects through iteration
954
+ let proxiedOuter;
955
+ for (const obj of st.values()) {
956
+ proxiedOuter = obj;
957
+ }
958
+ let proxiedInner;
959
+ for (const obj of proxiedOuter.anotherObject.values()) {
960
+ proxiedInner = obj;
961
+ }
962
+ // Modify the deeply nested property
963
+ proxiedInner.prop2 = "modified value";
964
+ await Promise.resolve();
965
+ const mutationPatches = batches.flat();
966
+ const mutationPaths = mutationPatches.map((p) => p.path.join("."));
967
+ // The path should match: ["rootId", "anotherObject", "innerId", "prop2"]
968
+ const prop2Path = mutationPaths.find((p) => p.endsWith("prop2"));
969
+ (0, vitest_1.expect)(prop2Path).toBeDefined();
970
+ // Verify the path segments
971
+ const pathSegments = prop2Path?.split(".");
972
+ (0, vitest_1.expect)(pathSegments?.length).toBe(4);
973
+ (0, vitest_1.expect)(pathSegments?.[0]).toBe("outerId-withCustomString");
974
+ (0, vitest_1.expect)(pathSegments?.[1]).toBe("anotherObject");
975
+ (0, vitest_1.expect)(pathSegments?.[2]).toBe("innerId-withCustomString");
976
+ (0, vitest_1.expect)(pathSegments?.[3]).toBe("prop2");
977
+ stop();
978
+ });
979
+ (0, vitest_1.it)("Does not mutate calling .sort() and .reverse()", async () => {
980
+ const signalObject = (0, __1.deepSignal)([1, 3, 2]);
981
+ const batches = [];
982
+ const { stopListening: stop } = (0, watch_1.watch)(signalObject, ({ patches }) => batches.push(patches));
983
+ signalObject.sort();
984
+ signalObject.reverse();
985
+ (0, vitest_1.expect)(signalObject).toEqual([1, 3, 2]);
986
+ await Promise.resolve();
987
+ (0, vitest_1.expect)(batches).toHaveLength(0);
988
+ stop();
989
+ });
990
+ (0, vitest_1.it)("Emits patches on .shift()", async () => {
991
+ const signalObject = (0, __1.deepSignal)([1, 3, 2]);
992
+ const batches = [];
993
+ const { stopListening: stop } = (0, watch_1.watch)(signalObject, ({ patches }) => batches.push(patches));
994
+ signalObject.shift();
995
+ (0, vitest_1.expect)(signalObject).toEqual([3, 2]);
996
+ await Promise.resolve();
997
+ (0, vitest_1.expect)(batches[0]).toEqual([
998
+ {
999
+ op: "remove",
1000
+ path: ["0"],
1001
+ },
1002
+ ]);
1003
+ });
1004
+ (0, vitest_1.it)("Emits patches on .splice()", async () => {
1005
+ const signalObject = (0, __1.deepSignal)([1, 3, 2]);
1006
+ const batches = [];
1007
+ const { stopListening: stop } = (0, watch_1.watch)(signalObject, ({ patches }) => batches.push(patches));
1008
+ signalObject.splice(1, 1, 10);
1009
+ await Promise.resolve();
1010
+ (0, vitest_1.expect)(signalObject).toEqual([1, 10, 2]);
1011
+ (0, vitest_1.expect)(batches[0]).toEqual([
1012
+ {
1013
+ op: "remove",
1014
+ path: ["1"],
1015
+ },
1016
+ {
1017
+ op: "add",
1018
+ path: ["1"],
1019
+ value: 10,
1020
+ },
1021
+ ]);
1022
+ });
1023
+ (0, vitest_1.it)("Does not mutate calling .unshift()", async () => {
1024
+ const signalObject = (0, __1.deepSignal)([1, 3, 2]);
1025
+ const batches = [];
1026
+ const { stopListening: stop } = (0, watch_1.watch)(signalObject, ({ patches }) => batches.push(patches));
1027
+ signalObject.unshift(-1, 0);
1028
+ (0, vitest_1.expect)(signalObject).toEqual([-1, 0, 1, 3, 2]);
1029
+ await Promise.resolve();
1030
+ (0, vitest_1.expect)(batches[0]).toEqual([
1031
+ {
1032
+ op: "add",
1033
+ path: ["0"],
1034
+ value: 0,
1035
+ },
1036
+ {
1037
+ op: "add",
1038
+ path: ["0"],
1039
+ value: -1,
1040
+ },
1041
+ ]);
1042
+ });
1043
+ });
1044
+ (0, vitest_1.describe)("delete patches", () => {
1045
+ (0, vitest_1.it)("emits delete patch when removing objects with @id from Sets", async () => {
1046
+ const options = {
1047
+ propGenerator: ({ object }) => ({
1048
+ syntheticId: object["@id"] || `fallback-${Math.random()}`,
1049
+ }),
1050
+ syntheticIdPropertyName: "@id",
1051
+ };
1052
+ const state = (0, __1.deepSignal)({ s: new Set() }, options);
1053
+ const patches = [];
1054
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(batch));
1055
+ // Add objects with @id
1056
+ const obj1 = { "@id": "obj-1", value: 1 };
1057
+ const obj2 = { "@id": "obj-2", value: 2 };
1058
+ const obj3 = { "@id": "obj-3", value: 3 };
1059
+ state.s.add(obj1);
1060
+ state.s.add(obj2);
1061
+ state.s.add(obj3);
1062
+ await Promise.resolve();
1063
+ // Get the proxied objects from the Set
1064
+ const proxiedObjs = Array.from(state.s);
1065
+ const proxiedObj2 = proxiedObjs.find((o) => o["@id"] === "obj-2");
1066
+ // Clear patches from additions
1067
+ patches.length = 0;
1068
+ // Delete one object using the proxied object
1069
+ state.s.delete(proxiedObj2);
1070
+ await Promise.resolve();
1071
+ // Check that delete patch was emitted with correct path
1072
+ const deletePaths = patches
1073
+ .flat()
1074
+ .filter((p) => p.op === "remove")
1075
+ .map((p) => p.path.join("."));
1076
+ (0, vitest_1.expect)(deletePaths).toContain("s.obj-2");
1077
+ (0, vitest_1.expect)(deletePaths).not.toContain("s.obj-1");
1078
+ (0, vitest_1.expect)(deletePaths).not.toContain("s.obj-3");
1079
+ stop();
1080
+ });
1081
+ (0, vitest_1.it)("emits delete patches when removing objects without explicit @id from Sets", async () => {
1082
+ const options = {
1083
+ propGenerator: () => ({
1084
+ syntheticId: `gen-${Math.random().toString(36).substr(2, 9)}`,
1085
+ }),
1086
+ syntheticIdPropertyName: "@id",
1087
+ };
1088
+ const state = (0, __1.deepSignal)({ s: new Set() }, options);
1089
+ // Add objects without @id - they should get generated IDs
1090
+ const obj1 = { value: 1 };
1091
+ const obj2 = { value: 2 };
1092
+ state.s.add(obj1);
1093
+ state.s.add(obj2);
1094
+ // Get the proxied objects and their generated IDs
1095
+ const proxiedObjs = Array.from(state.s);
1096
+ const proxiedObj1 = proxiedObjs[0];
1097
+ const proxiedObj2 = proxiedObjs[1];
1098
+ const id1 = proxiedObj1["@id"];
1099
+ const id2 = proxiedObj2["@id"];
1100
+ (0, vitest_1.expect)(id1).toBeDefined();
1101
+ (0, vitest_1.expect)(id2).toBeDefined();
1102
+ const patches = [];
1103
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: batch }) => patches.push(batch));
1104
+ // Delete one object using the proxied object
1105
+ state.s.delete(proxiedObj1);
1106
+ await Promise.resolve();
1107
+ // Check that delete patch was emitted with the generated ID
1108
+ const deletePaths = patches
1109
+ .flat()
1110
+ .filter((p) => p.op === "remove")
1111
+ .map((p) => p.path.join("."));
1112
+ (0, vitest_1.expect)(deletePaths).toContain(`s.${id1}`);
1113
+ (0, vitest_1.expect)(deletePaths).not.toContain(`s.${id2}`);
1114
+ stop();
1115
+ });
1116
+ });
1117
+ });
1118
+ (0, vitest_1.describe)("watch (triggerInstantly / JIT listeners)", () => {
1119
+ (0, vitest_1.it)("triggers callback synchronously when triggerInstantly is true", () => {
1120
+ const state = (0, __1.deepSignal)({ count: 0 });
1121
+ const callTimes = [];
1122
+ let syncMarker = 0;
1123
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => {
1124
+ callTimes.push(syncMarker);
1125
+ }, { triggerInstantly: true });
1126
+ syncMarker = 1;
1127
+ state.count = 1;
1128
+ syncMarker = 2;
1129
+ // Callback should have been called synchronously (at syncMarker = 1)
1130
+ (0, vitest_1.expect)(callTimes).toEqual([1]);
1131
+ stop();
1132
+ });
1133
+ (0, vitest_1.it)("triggers callback asynchronously when triggerInstantly is false", async () => {
1134
+ const state = (0, __1.deepSignal)({ count: 0 });
1135
+ const callTimes = [];
1136
+ let syncMarker = 0;
1137
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => {
1138
+ callTimes.push(syncMarker);
1139
+ });
1140
+ syncMarker = 1;
1141
+ state.count = 1;
1142
+ syncMarker = 2;
1143
+ // Callback should NOT have been called yet (batched in microtask)
1144
+ (0, vitest_1.expect)(callTimes).toEqual([]);
1145
+ await Promise.resolve();
1146
+ // Now callback should have been called (at syncMarker = 2)
1147
+ (0, vitest_1.expect)(callTimes).toEqual([2]);
1148
+ stop();
1149
+ });
1150
+ (0, vitest_1.it)("JIT listener receives patches without version", () => {
1151
+ const state = (0, __1.deepSignal)({ value: "initial" });
1152
+ let receivedVersion = 999; // sentinel value
1153
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches, version }) => {
1154
+ receivedVersion = version;
1155
+ }, { triggerInstantly: true });
1156
+ state.value = "changed";
1157
+ // JIT listeners should not receive a version (it's undefined)
1158
+ (0, vitest_1.expect)(receivedVersion).toBeUndefined();
1159
+ stop();
1160
+ });
1161
+ (0, vitest_1.it)("batched listener receives patches with version", async () => {
1162
+ const state = (0, __1.deepSignal)({ value: "initial" });
1163
+ let receivedVersion;
1164
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches, version }) => {
1165
+ receivedVersion = version;
1166
+ });
1167
+ state.value = "changed";
1168
+ await Promise.resolve();
1169
+ // Batched listeners should receive a version number
1170
+ (0, vitest_1.expect)(receivedVersion).toBeDefined();
1171
+ (0, vitest_1.expect)(typeof receivedVersion).toBe("number");
1172
+ (0, vitest_1.expect)(receivedVersion).toBeGreaterThan(0);
1173
+ stop();
1174
+ });
1175
+ (0, vitest_1.it)("JIT listener is called for each mutation, not batched", () => {
1176
+ const state = (0, __1.deepSignal)({ a: 1, b: 2 });
1177
+ const patchCalls = [];
1178
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => {
1179
+ patchCalls.push([...patches]);
1180
+ }, { triggerInstantly: true });
1181
+ state.a = 10;
1182
+ state.b = 20;
1183
+ // Should be called twice (once per mutation), not once with batched patches
1184
+ (0, vitest_1.expect)(patchCalls).toHaveLength(2);
1185
+ (0, vitest_1.expect)(patchCalls[0]).toEqual([{ op: "add", path: ["a"], value: 10 }]);
1186
+ (0, vitest_1.expect)(patchCalls[1]).toEqual([{ op: "add", path: ["b"], value: 20 }]);
1187
+ stop();
1188
+ });
1189
+ (0, vitest_1.it)("batched listener receives all mutations in one batch", async () => {
1190
+ const state = (0, __1.deepSignal)({ a: 1, b: 2 });
1191
+ const patchCalls = [];
1192
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches }) => {
1193
+ patchCalls.push([...patches]);
1194
+ });
1195
+ state.a = 10;
1196
+ state.b = 20;
1197
+ await Promise.resolve();
1198
+ // Should be called once with all patches batched
1199
+ (0, vitest_1.expect)(patchCalls).toHaveLength(1);
1200
+ (0, vitest_1.expect)(patchCalls[0]).toEqual([
1201
+ { op: "add", path: ["a"], value: 10 },
1202
+ { op: "add", path: ["b"], value: 20 },
1203
+ ]);
1204
+ stop();
1205
+ });
1206
+ (0, vitest_1.it)("both JIT and batched listeners can coexist on the same root", async () => {
1207
+ const state = (0, __1.deepSignal)({ count: 0 });
1208
+ const jitPatches = [];
1209
+ const batchedPatches = [];
1210
+ const { stopListening: stopJit } = (0, watch_1.watch)(state, ({ patches }) => {
1211
+ jitPatches.push([...patches]);
1212
+ }, { triggerInstantly: true });
1213
+ const { stopListening: stopBatched } = (0, watch_1.watch)(state, ({ patches }) => {
1214
+ batchedPatches.push([...patches]);
1215
+ });
1216
+ state.count = 1;
1217
+ state.count = 2;
1218
+ // JIT should have received 2 calls immediately
1219
+ (0, vitest_1.expect)(jitPatches).toHaveLength(2);
1220
+ // Batched should have received 0 calls so far
1221
+ (0, vitest_1.expect)(batchedPatches).toHaveLength(0);
1222
+ await Promise.resolve();
1223
+ // Now batched should have received 1 call with all patches
1224
+ (0, vitest_1.expect)(batchedPatches).toHaveLength(1);
1225
+ (0, vitest_1.expect)(batchedPatches[0]).toEqual([
1226
+ { op: "add", path: ["count"], value: 1 },
1227
+ { op: "add", path: ["count"], value: 2 },
1228
+ ]);
1229
+ stopJit();
1230
+ stopBatched();
1231
+ });
1232
+ (0, vitest_1.it)("JIT listener works with nested object mutations", () => {
1233
+ const state = (0, __1.deepSignal)({ user: { name: "Alice", age: 30 } });
1234
+ const patches = [];
1235
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: p }) => {
1236
+ patches.push([...p]);
1237
+ }, { triggerInstantly: true });
1238
+ state.user.name = "Bob";
1239
+ state.user.age = 31;
1240
+ (0, vitest_1.expect)(patches).toHaveLength(2);
1241
+ (0, vitest_1.expect)(patches[0]).toEqual([
1242
+ { op: "add", path: ["user", "name"], value: "Bob" },
1243
+ ]);
1244
+ (0, vitest_1.expect)(patches[1]).toEqual([
1245
+ { op: "add", path: ["user", "age"], value: 31 },
1246
+ ]);
1247
+ stop();
1248
+ });
1249
+ (0, vitest_1.it)("JIT listener works with Set mutations", () => {
1250
+ const state = (0, __1.deepSignal)({ items: new Set([1, 2]) });
1251
+ const patches = [];
1252
+ const { stopListening: stop } = (0, watch_1.watch)(state, ({ patches: p }) => {
1253
+ patches.push([...p]);
1254
+ }, { triggerInstantly: true });
1255
+ state.items.add(3);
1256
+ state.items.delete(1);
1257
+ (0, vitest_1.expect)(patches).toHaveLength(2);
1258
+ (0, vitest_1.expect)(patches[0]).toEqual([
1259
+ { op: "add", path: ["items"], type: "set", value: [3] },
1260
+ ]);
1261
+ (0, vitest_1.expect)(patches[1]).toEqual([
1262
+ { op: "remove", path: ["items"], type: "set", value: 1 },
1263
+ ]);
1264
+ stop();
1265
+ });
1266
+ (0, vitest_1.it)("stopping JIT listener prevents further callbacks", () => {
1267
+ const state = (0, __1.deepSignal)({ value: 0 });
1268
+ let callCount = 0;
1269
+ const { stopListening: stop } = (0, watch_1.watch)(state, () => {
1270
+ callCount++;
1271
+ }, { triggerInstantly: true });
1272
+ state.value = 1;
1273
+ (0, vitest_1.expect)(callCount).toBe(1);
1274
+ stop();
1275
+ state.value = 2;
1276
+ state.value = 3;
1277
+ // Should still be 1, no more calls after stop
1278
+ (0, vitest_1.expect)(callCount).toBe(1);
1279
+ });
1280
+ });