jazz-tools 0.18.3 → 0.18.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 (32) hide show
  1. package/.turbo/turbo-build.log +35 -35
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{chunk-IERUTUXB.js → chunk-LHQQZH7I.js} +121 -36
  4. package/dist/chunk-LHQQZH7I.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/react-core/index.js +120 -35
  7. package/dist/react-core/index.js.map +1 -1
  8. package/dist/testing.js +1 -1
  9. package/dist/tools/coValues/account.d.ts.map +1 -1
  10. package/dist/tools/coValues/coFeed.d.ts +12 -0
  11. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  12. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  13. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +19 -0
  14. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  15. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +61 -11
  16. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  17. package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts +2 -0
  18. package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts.map +1 -0
  19. package/dist/tools/testing.d.ts.map +1 -1
  20. package/package.json +4 -4
  21. package/src/tools/coValues/account.ts +3 -1
  22. package/src/tools/coValues/coFeed.ts +5 -0
  23. package/src/tools/coValues/coMap.ts +3 -1
  24. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +19 -0
  25. package/src/tools/subscribe/CoValueCoreSubscription.test.ts +1000 -0
  26. package/src/tools/subscribe/CoValueCoreSubscription.ts +179 -43
  27. package/src/tools/tests/account.test.ts +12 -0
  28. package/src/tools/tests/coFeed.test.ts +25 -0
  29. package/src/tools/tests/coList.test.ts +20 -0
  30. package/src/tools/tests/coMap.record.test.ts +1 -0
  31. package/src/tools/tests/coMap.test.ts +12 -2
  32. package/dist/chunk-IERUTUXB.js.map +0 -1
@@ -0,0 +1,1000 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { Group, co, z } from "../exports.js";
3
+ import { CoValueCoreSubscription } from "./CoValueCoreSubscription.js";
4
+ import {
5
+ createJazzTestAccount,
6
+ getPeerConnectedToTestSyncServer,
7
+ setupJazzTestSync,
8
+ } from "../testing.js";
9
+ import { waitFor } from "../tests/utils.js";
10
+
11
+ beforeEach(async () => {
12
+ await setupJazzTestSync();
13
+
14
+ // Create a test account for each test
15
+ await createJazzTestAccount({
16
+ isCurrentActiveAccount: true,
17
+ creationProps: { name: "Hermes Puggington" },
18
+ });
19
+ });
20
+
21
+ describe("CoValueCoreSubscription", async () => {
22
+ /**
23
+ * Tests scenarios where the CoValue is immediately available
24
+ * (already loaded in memory, no async loading required)
25
+ */
26
+ describe("immediate availability scenarios", () => {
27
+ test("should emit immediately when CoValue is available and no branch requested", async () => {
28
+ const Person = co.map({
29
+ name: z.string(),
30
+ age: z.number(),
31
+ });
32
+ type Person = co.loaded<typeof Person>;
33
+
34
+ // Create a person that's immediately available
35
+ const person = Person.create({ name: "John", age: 30 });
36
+ let lastResult: any = null;
37
+ const listener = vi.fn();
38
+
39
+ // Subscribe to the person without requesting a specific branch
40
+ const subscription = new CoValueCoreSubscription(
41
+ person.$jazz.localNode,
42
+ person.$jazz.id,
43
+ (result) => {
44
+ lastResult = result;
45
+ listener(result);
46
+ },
47
+ );
48
+
49
+ // Should immediately call the listener since CoValue is available
50
+ expect(listener).toHaveBeenCalledTimes(1);
51
+ expect(lastResult.get("name")).toEqual("John");
52
+
53
+ subscription.unsubscribe();
54
+ });
55
+
56
+ test("should subscribe to branch when CoValue is available and branch is requested and available", async () => {
57
+ const Person = co.map({
58
+ name: z.string(),
59
+ age: z.number(),
60
+ });
61
+
62
+ // Create a person that's immediately available
63
+ const person = Person.create({ name: "John", age: 30 });
64
+ let lastResult: any = null;
65
+ const listener = vi.fn();
66
+
67
+ // Create a branch on the person with modified data
68
+ const branch = person.$jazz.raw.core.createBranch(
69
+ "main",
70
+ person.$jazz.owner.$jazz.raw.id,
71
+ );
72
+
73
+ // @ts-ignore Update the person name in the branch
74
+ branch.getCurrentContent().set("name", "Jane");
75
+
76
+ // Subscribe to the specific branch
77
+ const subscription = new CoValueCoreSubscription(
78
+ person.$jazz.localNode,
79
+ person.$jazz.id,
80
+ (result) => {
81
+ lastResult = result;
82
+ listener(result);
83
+ },
84
+ false,
85
+ { name: "main", owner: person.$jazz.owner },
86
+ );
87
+
88
+ // Should immediately call the listener with branch data
89
+ expect(listener).toHaveBeenCalledTimes(1);
90
+ expect(lastResult.get("name")).toEqual("Jane");
91
+
92
+ subscription.unsubscribe();
93
+ });
94
+
95
+ test("should fall through to loading when CoValue is available but branch is not available", async () => {
96
+ const Person = co.map({
97
+ name: z.string(),
98
+ age: z.number(),
99
+ });
100
+
101
+ // Create a person that's immediately available
102
+ const person = Person.create({ name: "John", age: 30 });
103
+ let lastResult: any = null;
104
+ const listener = vi.fn();
105
+
106
+ // Request a branch that doesn't exist yet
107
+ const subscription = new CoValueCoreSubscription(
108
+ person.$jazz.localNode,
109
+ person.$jazz.id,
110
+ (result) => {
111
+ lastResult = result;
112
+ listener(result);
113
+ },
114
+ false,
115
+ { name: "main" },
116
+ );
117
+
118
+ // Should not call listener immediately since branch isn't available
119
+ expect(listener).not.toHaveBeenCalled();
120
+
121
+ // Wait for the branch to be created and loaded
122
+ await waitFor(() => expect(listener).toHaveBeenCalled());
123
+
124
+ expect(listener).toHaveBeenCalledTimes(1);
125
+
126
+ // Should return the branch, that contains the source data
127
+ expect(lastResult.get("name")).toEqual("John");
128
+ expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
129
+
130
+ subscription.unsubscribe();
131
+ });
132
+ });
133
+
134
+ /**
135
+ * Tests scenarios where the CoValue needs to be loaded asynchronously
136
+ * (not currently in memory, requires network/sync operations)
137
+ */
138
+ describe("loading scenarios", () => {
139
+ test("should emit in async when CoValue is available and no branch requested", async () => {
140
+ const Person = co.map({
141
+ name: z.string(),
142
+ age: z.number(),
143
+ });
144
+ const bob = await createJazzTestAccount();
145
+
146
+ // Create a person on a different account that bob doesn't have access to yet
147
+ // The sync is delayed by a queueMicrotask, making the load async
148
+ const person = Person.create(
149
+ { name: "John", age: 30 },
150
+ Group.create().makePublic("writer"),
151
+ );
152
+
153
+ let lastResult: any = null;
154
+ const listener = vi.fn();
155
+
156
+ // Subscribe to a CoValue that needs to be loaded
157
+ const subscription = new CoValueCoreSubscription(
158
+ bob.$jazz.localNode,
159
+ person.$jazz.id,
160
+ (result) => {
161
+ lastResult = result;
162
+ listener(result);
163
+ },
164
+ );
165
+
166
+ // Should not call listener immediately since CoValue needs to be loaded
167
+ expect(listener).not.toHaveBeenCalled();
168
+
169
+ // Wait for the async loading to complete
170
+ await waitFor(() => expect(listener).toHaveBeenCalled());
171
+
172
+ // Should call listener with the loaded value
173
+ expect(lastResult.get("name")).toEqual("John");
174
+ expect(lastResult.id).toBe(person.$jazz.id);
175
+
176
+ subscription.unsubscribe();
177
+ });
178
+
179
+ test("should handle loading when CoValue is not available and branch is requested", async () => {
180
+ const Person = co.map({
181
+ name: z.string(),
182
+ age: z.number(),
183
+ });
184
+ const bob = await createJazzTestAccount();
185
+
186
+ // Create a person on a different account that bob doesn't have access to yet
187
+ const person = Person.create(
188
+ { name: "John", age: 30 },
189
+ Group.create().makePublic("writer"),
190
+ );
191
+
192
+ let lastResult: any = null;
193
+ const listener = vi.fn();
194
+
195
+ // Request both the CoValue and a specific branch
196
+ const subscription = new CoValueCoreSubscription(
197
+ bob.$jazz.localNode,
198
+ person.$jazz.id,
199
+ (result) => {
200
+ lastResult = result;
201
+ listener(result);
202
+ },
203
+ false,
204
+ { name: "main" },
205
+ );
206
+
207
+ // Should not call listener immediately since both CoValue and branch need to be loaded
208
+ expect(listener).not.toHaveBeenCalled();
209
+
210
+ // Wait for the async loading to complete
211
+ await waitFor(() => expect(listener).toHaveBeenCalled());
212
+
213
+ // Should return the branch, that contains the source data
214
+ expect(lastResult.get("name")).toEqual("John");
215
+ expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
216
+
217
+ subscription.unsubscribe();
218
+ });
219
+ });
220
+
221
+ /**
222
+ * Tests scenarios involving branch checkout operations
223
+ * (creating, accessing, and working with different branches of CoValues)
224
+ */
225
+ describe("branch checkout scenarios", () => {
226
+ test("should handle successful branch checkout when source is not available", async () => {
227
+ const Person = co.map({
228
+ name: z.string(),
229
+ age: z.number(),
230
+ });
231
+ const bob = await createJazzTestAccount();
232
+
233
+ // Create a person on a different account that bob doesn't have access to yet
234
+ const person = Person.create(
235
+ { name: "John", age: 30 },
236
+ Group.create().makePublic("writer"),
237
+ );
238
+
239
+ // Create a branch on the person with modified data
240
+ const branch = person.$jazz.raw.core.createBranch(
241
+ "main",
242
+ person.$jazz.owner.$jazz.raw.id,
243
+ );
244
+
245
+ // @ts-ignore Update the person name in the branch
246
+ branch.getCurrentContent().set("name", "Jane");
247
+
248
+ let lastResult: any = null;
249
+ const listener = vi.fn();
250
+
251
+ // Subscribe to the specific branch
252
+ const subscription = new CoValueCoreSubscription(
253
+ bob.$jazz.localNode,
254
+ person.$jazz.id,
255
+ (result) => {
256
+ lastResult = result;
257
+ listener(result);
258
+ },
259
+ false,
260
+ { name: "main", owner: person.$jazz.owner },
261
+ );
262
+
263
+ // Should not call listener immediately since source isn't available
264
+ expect(listener).not.toHaveBeenCalled();
265
+
266
+ // Wait for the branch checkout to complete
267
+ await waitFor(() => expect(listener).toHaveBeenCalled());
268
+
269
+ // Should return the branch data
270
+ await waitFor(() => expect(lastResult.get("name")).toEqual("Jane"));
271
+ expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
272
+
273
+ subscription.unsubscribe();
274
+ });
275
+
276
+ test("should create a private branch when a custom owner id is provided", async () => {
277
+ const Person = co.map({
278
+ name: z.string(),
279
+ age: z.number(),
280
+ });
281
+ const bob = await createJazzTestAccount();
282
+
283
+ // Create a person on a different account that bob doesn't have access to yet
284
+ const person = Person.create(
285
+ { name: "John", age: 30 },
286
+ Group.create().makePublic(), // Only read access
287
+ );
288
+
289
+ // Create a branch on the person using the current owner id
290
+ const branch = person.$jazz.raw.core.createBranch(
291
+ "main",
292
+ person.$jazz.owner.$jazz.raw.id,
293
+ );
294
+
295
+ // @ts-ignore Update the person name in the branch
296
+ branch.getCurrentContent().set("name", "Jane");
297
+
298
+ let lastResult: any = null;
299
+ const listener = vi.fn();
300
+
301
+ // Wait for the branch to sync before subscribing
302
+ await branch.waitForSync();
303
+
304
+ // Subscribe with bob's ID as the owner, creating a private branch
305
+ const subscription = new CoValueCoreSubscription(
306
+ bob.$jazz.localNode,
307
+ person.$jazz.id,
308
+ (result) => {
309
+ lastResult = result;
310
+ listener(result);
311
+ },
312
+ false,
313
+ { name: "main", owner: bob },
314
+ );
315
+
316
+ // Should not call listener immediately since private branch needs to be created
317
+ expect(listener).not.toHaveBeenCalled();
318
+
319
+ // Wait for the private branch creation to complete
320
+ await waitFor(() => expect(listener).toHaveBeenCalled());
321
+
322
+ // Should return the source data (not branch data) since it's a private branch
323
+ expect(lastResult.get("name")).toEqual("John");
324
+ expect(lastResult.core.getGroup().id).toBe(bob.$jazz.id); // Should be owned by bob
325
+ expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
326
+ expect(lastResult.id).not.toBe(branch.id); // Should not be the original branch
327
+
328
+ // Should have write access to the private branch, even though source gives only read access
329
+ lastResult.set("name", "Guido");
330
+ expect(lastResult.get("name")).toEqual("Guido");
331
+
332
+ subscription.unsubscribe();
333
+ });
334
+ });
335
+
336
+ describe("error handling scenarios", () => {
337
+ test("should handle return unavailable when the id is invalid", async () => {
338
+ const bob = await createJazzTestAccount();
339
+ const invalidId = "invalid-co-value-id";
340
+
341
+ let lastResult: any = null;
342
+ const listener = vi.fn();
343
+
344
+ // Try to subscribe to an invalid CoValue ID
345
+ const subscription = new CoValueCoreSubscription(
346
+ bob.$jazz.localNode,
347
+ invalidId,
348
+ (result) => {
349
+ lastResult = result;
350
+ listener(result);
351
+ },
352
+ );
353
+
354
+ // Should not call listener immediately since ID is invalid
355
+ expect(listener).not.toHaveBeenCalled();
356
+
357
+ // Wait for the error handling to complete
358
+ await waitFor(() => expect(listener).toHaveBeenCalled());
359
+
360
+ // Should report unavailable when loading fails
361
+ expect(lastResult).toBe("unavailable");
362
+
363
+ subscription.unsubscribe();
364
+ });
365
+
366
+ test("should handle return unavailable when the id is invalid and a branch is requested", async () => {
367
+ const bob = await createJazzTestAccount();
368
+ const invalidId = "invalid-co-value-id";
369
+
370
+ let lastResult: any = null;
371
+ const listener = vi.fn();
372
+
373
+ // Try to subscribe to an invalid CoValue ID with branch request
374
+ const subscription = new CoValueCoreSubscription(
375
+ bob.$jazz.localNode,
376
+ invalidId,
377
+ (result) => {
378
+ lastResult = result;
379
+ listener(result);
380
+ },
381
+ false,
382
+ { name: "main", owner: bob },
383
+ );
384
+
385
+ // Should not call listener immediately since ID is invalid
386
+ expect(listener).not.toHaveBeenCalled();
387
+
388
+ // Wait for the error handling to complete
389
+ await waitFor(() => expect(listener).toHaveBeenCalled());
390
+
391
+ // Should report unavailable when loading fails
392
+ expect(lastResult).toBe("unavailable");
393
+
394
+ subscription.unsubscribe();
395
+ });
396
+
397
+ test("should handle return unavailable when the owner is unavailable", async () => {
398
+ const Person = co.map({
399
+ name: z.string(),
400
+ age: z.number(),
401
+ });
402
+
403
+ const alice = await createJazzTestAccount();
404
+
405
+ // Disconnect all peers to not sync the unavailable group
406
+ alice.$jazz.localNode.syncManager
407
+ .getServerPeers(alice.$jazz.raw.id)
408
+ .forEach((peer) => peer.gracefulShutdown());
409
+
410
+ const unavailableGroup = Group.create(alice).makePublic("writer");
411
+
412
+ const bob = await createJazzTestAccount();
413
+
414
+ // Create a person that bob can access
415
+ const person = Person.create(
416
+ { name: "John", age: 30 },
417
+ Group.create().makePublic("writer"),
418
+ );
419
+ let lastResult: any = null;
420
+ const listener = vi.fn();
421
+
422
+ // Try to subscribe with an invalid owner ID
423
+ const subscription = new CoValueCoreSubscription(
424
+ bob.$jazz.localNode,
425
+ person.$jazz.id,
426
+ (result) => {
427
+ lastResult = result;
428
+ listener(result);
429
+ },
430
+ true,
431
+ { name: "main", owner: unavailableGroup },
432
+ );
433
+
434
+ // Should not call listener immediately since owner is unavailable
435
+ expect(listener).not.toHaveBeenCalled();
436
+
437
+ // Wait for the error handling to complete
438
+ await waitFor(() => expect(listener).toHaveBeenCalled());
439
+
440
+ // Should report unavailable when loading fails
441
+ expect(lastResult).toBe("unavailable");
442
+
443
+ subscription.unsubscribe();
444
+ });
445
+ });
446
+
447
+ /**
448
+ * Tests scenarios where CoValues transition from unavailable to available
449
+ */
450
+ describe("resolving an unavailable covalue", () => {
451
+ test("should handle state changes when source becomes available", async () => {
452
+ const Person = co.map({
453
+ name: z.string(),
454
+ age: z.number(),
455
+ });
456
+ const bob = await createJazzTestAccount();
457
+
458
+ // Create a person that bob can access
459
+ const person = Person.create(
460
+ { name: "John", age: 30 },
461
+ Group.create().makePublic("writer"),
462
+ );
463
+
464
+ // Disconnect all peers to make the CoValue unavailable
465
+ bob.$jazz.localNode.syncManager
466
+ .getServerPeers(person.$jazz.raw.id)
467
+ .forEach((peer) => peer.gracefulShutdown());
468
+
469
+ let lastResult: any = null;
470
+ const listener = vi.fn();
471
+
472
+ // Subscribe to the now-unavailable CoValue with branch request
473
+ const subscription = new CoValueCoreSubscription(
474
+ bob.$jazz.localNode,
475
+ person.$jazz.id,
476
+ (result) => {
477
+ lastResult = result;
478
+ listener(result);
479
+ },
480
+ false,
481
+ { name: "main" },
482
+ );
483
+
484
+ // Wait for the initial unavailable state
485
+ await waitFor(() => expect(listener).toHaveBeenCalled());
486
+
487
+ // Clear the listener to track new calls
488
+ listener.mockClear();
489
+
490
+ // Reconnect to make the CoValue available again
491
+ bob.$jazz.localNode.syncManager.addPeer(
492
+ getPeerConnectedToTestSyncServer(),
493
+ );
494
+
495
+ // Wait for the CoValue to become available
496
+ await waitFor(() => expect(listener).toHaveBeenCalled());
497
+
498
+ // Should return the source data when branch isn't available
499
+ expect(lastResult.get("name")).toEqual("John");
500
+ expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
501
+
502
+ subscription.unsubscribe();
503
+ });
504
+
505
+ test("should handle state changes when source becomes available and no branch requested", async () => {
506
+ const Person = co.map({
507
+ name: z.string(),
508
+ age: z.number(),
509
+ });
510
+ const bob = await createJazzTestAccount();
511
+
512
+ // Create a person that bob can access
513
+ const person = Person.create(
514
+ { name: "John", age: 30 },
515
+ Group.create().makePublic("writer"),
516
+ );
517
+
518
+ // Disconnect all peers to make the CoValue unavailable
519
+ bob.$jazz.localNode.syncManager
520
+ .getServerPeers(person.$jazz.raw.id)
521
+ .forEach((peer) => peer.gracefulShutdown());
522
+
523
+ let lastResult: any = null;
524
+ const listener = vi.fn();
525
+
526
+ // Subscribe to the now-unavailable CoValue without branch request
527
+ const subscription = new CoValueCoreSubscription(
528
+ bob.$jazz.localNode,
529
+ person.$jazz.id,
530
+ (result) => {
531
+ lastResult = result;
532
+ listener(result);
533
+ },
534
+ false,
535
+ );
536
+
537
+ // Wait for the initial unavailable state
538
+ await waitFor(() => expect(listener).toHaveBeenCalled());
539
+
540
+ // Clear the listener to track new calls
541
+ listener.mockClear();
542
+
543
+ // Reconnect to make the CoValue available again
544
+ bob.$jazz.localNode.syncManager.addPeer(
545
+ getPeerConnectedToTestSyncServer(),
546
+ );
547
+
548
+ // Wait for the CoValue to become available
549
+ await waitFor(() => expect(listener).toHaveBeenCalled());
550
+
551
+ // Should return the original CoValue when no branch is requested
552
+ expect(lastResult.get("name")).toEqual("John");
553
+ expect(lastResult.id).toBe(person.$jazz.id);
554
+
555
+ subscription.unsubscribe();
556
+ });
557
+ });
558
+
559
+ /**
560
+ * Tests unsubscribe behavior in various scenarios
561
+ * (immediate unsubscribe, multiple calls, during async operations)
562
+ */
563
+ describe("unsubscribe scenarios", () => {
564
+ test("should properly unsubscribe when called", async () => {
565
+ const Person = co.map({
566
+ name: z.string(),
567
+ age: z.number(),
568
+ });
569
+
570
+ // Create a person that's immediately available
571
+ const person = Person.create({ name: "John", age: 30 });
572
+ const listener = vi.fn();
573
+
574
+ // Subscribe to the person
575
+ const subscription = new CoValueCoreSubscription(
576
+ person.$jazz.localNode,
577
+ person.$jazz.id,
578
+ (value) => {
579
+ listener(value);
580
+ },
581
+ );
582
+
583
+ // Should call listener once for initial value
584
+ expect(listener).toHaveBeenCalledTimes(1);
585
+
586
+ // Unsubscribe from updates
587
+ subscription.unsubscribe();
588
+
589
+ // Update the person to trigger subscription callback
590
+ person.$jazz.set("name", "Jane");
591
+
592
+ // Listener should not be called after unsubscribe
593
+ expect(listener).toHaveBeenCalledTimes(1);
594
+ });
595
+
596
+ test("should handle multiple unsubscribe calls gracefully", async () => {
597
+ const Person = co.map({
598
+ name: z.string(),
599
+ age: z.number(),
600
+ });
601
+
602
+ // Create a person that's immediately available
603
+ const person = Person.create({ name: "John", age: 30 });
604
+ let lastResult: any = null;
605
+ const listener = vi.fn();
606
+
607
+ // Subscribe to the person
608
+ const subscription = new CoValueCoreSubscription(
609
+ person.$jazz.localNode,
610
+ person.$jazz.id,
611
+ (result) => {
612
+ lastResult = result;
613
+ listener(result);
614
+ },
615
+ );
616
+
617
+ // Should call listener once for initial value
618
+ expect(listener).toHaveBeenCalledTimes(1);
619
+
620
+ // Call unsubscribe multiple times
621
+ subscription.unsubscribe();
622
+ subscription.unsubscribe(); // Second call should not cause issues
623
+
624
+ // Update the person to trigger subscription callback
625
+ person.$jazz.set("name", "Jane");
626
+
627
+ // Listener should not be called after unsubscribe
628
+ expect(listener).toHaveBeenCalledTimes(1);
629
+ });
630
+
631
+ test("should unsubscribe during async operations", async () => {
632
+ const Person = co.map({
633
+ name: z.string(),
634
+ age: z.number(),
635
+ });
636
+ const bob = await createJazzTestAccount();
637
+
638
+ // Create a person on a different account that bob doesn't have access to yet
639
+ const person = Person.create(
640
+ { name: "John", age: 30 },
641
+ Group.create().makePublic("writer"),
642
+ );
643
+
644
+ let lastResult: any = null;
645
+ const listener = vi.fn();
646
+
647
+ // Subscribe to a CoValue that needs to be loaded
648
+ const subscription = new CoValueCoreSubscription(
649
+ bob.$jazz.localNode,
650
+ person.$jazz.id,
651
+ (result) => {
652
+ lastResult = result;
653
+ listener(result);
654
+ },
655
+ );
656
+
657
+ // Unsubscribe immediately before the async operation completes
658
+ subscription.unsubscribe();
659
+
660
+ // Wait a bit to ensure async operations would have completed
661
+ await new Promise((resolve) => setTimeout(resolve, 100));
662
+
663
+ // Listener should not be called after unsubscribe
664
+ expect(listener).toHaveBeenCalledTimes(0);
665
+ });
666
+
667
+ test("should unsubscribe during checkout operations", async () => {
668
+ const Person = co.map({
669
+ name: z.string(),
670
+ age: z.number(),
671
+ });
672
+ const bob = await createJazzTestAccount();
673
+
674
+ // Create a person on a different account that bob doesn't have access to yet
675
+ const person = Person.create(
676
+ { name: "John", age: 30 },
677
+ Group.create().makePublic("writer"),
678
+ );
679
+
680
+ let lastResult: any = null;
681
+ const listener = vi.fn();
682
+
683
+ // Subscribe to a CoValue with branch request that needs to be loaded
684
+ const subscription = new CoValueCoreSubscription(
685
+ bob.$jazz.localNode,
686
+ person.$jazz.id,
687
+ (result) => {
688
+ lastResult = result;
689
+ listener(result);
690
+ },
691
+ false,
692
+ { name: "main" },
693
+ );
694
+
695
+ // Unsubscribe immediately before the async operation completes
696
+ subscription.unsubscribe();
697
+
698
+ // Wait a bit to ensure async operations would have completed
699
+ await new Promise((resolve) => setTimeout(resolve, 100));
700
+
701
+ // Listener should not be called after unsubscribe
702
+ expect(listener).toHaveBeenCalledTimes(0);
703
+ });
704
+ });
705
+
706
+ /**
707
+ * Tests concurrent operations and multiple subscriptions
708
+ * (multiple subscribers to same CoValue, same branch, etc.)
709
+ */
710
+ describe("concurrent operations", () => {
711
+ test("should handle multiple subscriptions to the same CoValue", async () => {
712
+ const Person = co.map({
713
+ name: z.string(),
714
+ age: z.number(),
715
+ });
716
+ const bob = await createJazzTestAccount();
717
+
718
+ // Create a person on a different account that bob doesn't have access to yet
719
+ const person = Person.create(
720
+ { name: "John", age: 30 },
721
+ Group.create().makePublic("writer"),
722
+ );
723
+
724
+ let lastResultSubscription1: any = null;
725
+ let lastResultSubscription2: any = null;
726
+ const listener = vi.fn();
727
+
728
+ // Subscribe to the CoValue with branch request
729
+ const subscription1 = new CoValueCoreSubscription(
730
+ bob.$jazz.localNode,
731
+ person.$jazz.id,
732
+ (result) => {
733
+ lastResultSubscription1 = result;
734
+ listener(result);
735
+ },
736
+ );
737
+ const subscription2 = new CoValueCoreSubscription(
738
+ bob.$jazz.localNode,
739
+ person.$jazz.id,
740
+ (result) => {
741
+ lastResultSubscription2 = result;
742
+ listener(result);
743
+ },
744
+ );
745
+
746
+ await waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
747
+
748
+ // Should return the same instance of the RawCoValue
749
+ expect(lastResultSubscription1).toBe(lastResultSubscription2);
750
+ expect(lastResultSubscription1.get("name")).toEqual("John");
751
+ expect(lastResultSubscription1.id).toBe(person.$jazz.id);
752
+
753
+ subscription1.unsubscribe();
754
+ subscription2.unsubscribe();
755
+ });
756
+
757
+ test("should handle multiple subscriptions to the same branch", async () => {
758
+ const Person = co.map({
759
+ name: z.string(),
760
+ age: z.number(),
761
+ });
762
+ const bob = await createJazzTestAccount();
763
+
764
+ // Create a person on a different account that bob doesn't have access to yet
765
+ const person = Person.create(
766
+ { name: "John", age: 30 },
767
+ Group.create().makePublic("writer"),
768
+ );
769
+
770
+ let lastResultSubscription1: any = null;
771
+ let lastResultSubscription2: any = null;
772
+ const listener = vi.fn();
773
+
774
+ // Subscribe to the CoValue with branch request
775
+ const subscription1 = new CoValueCoreSubscription(
776
+ bob.$jazz.localNode,
777
+ person.$jazz.id,
778
+ (result) => {
779
+ lastResultSubscription1 = result;
780
+ listener(result);
781
+ },
782
+ false,
783
+ { name: "main" },
784
+ );
785
+ const subscription2 = new CoValueCoreSubscription(
786
+ bob.$jazz.localNode,
787
+ person.$jazz.id,
788
+ (result) => {
789
+ lastResultSubscription2 = result;
790
+ listener(result);
791
+ },
792
+ false,
793
+ { name: "main" },
794
+ );
795
+
796
+ // Wait for the async loading to complete
797
+ await waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
798
+
799
+ // Should return the same instance of the RawCoValue
800
+ expect(lastResultSubscription1).toBe(lastResultSubscription2);
801
+ expect(lastResultSubscription1.get("name")).toEqual("John");
802
+ expect(lastResultSubscription1.id).not.toBe(person.$jazz.id);
803
+
804
+ subscription1.unsubscribe();
805
+ subscription2.unsubscribe();
806
+ });
807
+ });
808
+
809
+ /**
810
+ * Tests real-time update scenarios
811
+ * (property changes, branch updates, rapid successive changes)
812
+ */
813
+ describe("updates", () => {
814
+ test("should receive updates when CoValue properties change", async () => {
815
+ // Define a Person schema with optional email field
816
+ const Person = co.map({
817
+ name: z.string(),
818
+ age: z.number(),
819
+ email: z.string().optional(),
820
+ });
821
+
822
+ // Create a person that's immediately available
823
+ const person = Person.create({ name: "John", age: 30 });
824
+ let lastResult: any = null;
825
+ const listener = vi.fn();
826
+
827
+ // Subscribe to the person
828
+ const subscription = new CoValueCoreSubscription(
829
+ person.$jazz.localNode,
830
+ person.$jazz.id,
831
+ (result) => {
832
+ lastResult = result;
833
+ listener(result);
834
+ },
835
+ );
836
+
837
+ // Initial call with default values
838
+ expect(listener).toHaveBeenCalledTimes(1);
839
+ expect(lastResult.get("name")).toEqual("John");
840
+
841
+ // Update properties to trigger subscription callbacks
842
+ person.$jazz.set("age", 31);
843
+ person.$jazz.set("email", "john@example.com");
844
+
845
+ // Wait for all updates to be processed
846
+ await waitFor(() => expect(listener).toHaveBeenCalledTimes(3));
847
+
848
+ // Check that we received updates for each change
849
+ expect(lastResult.get("age")).toEqual(31);
850
+ expect(lastResult.get("email")).toEqual("john@example.com");
851
+ expect(lastResult.get("name")).toEqual("John"); // Other properties should remain
852
+ expect(lastResult.get("age")).toEqual(31);
853
+
854
+ subscription.unsubscribe();
855
+ });
856
+
857
+ test("should receive updates when CoValue properties change in branch", async () => {
858
+ // Define a Person schema with optional email field
859
+ const Person = co.map({
860
+ name: z.string(),
861
+ age: z.number(),
862
+ email: z.string().optional(),
863
+ });
864
+
865
+ // Create a person that's immediately available
866
+ const person = Person.create({ name: "John", age: 30 });
867
+
868
+ // Create a branch on the person
869
+ const branch = person.$jazz.raw.core.createBranch(
870
+ "main",
871
+ person.$jazz.owner.$jazz.raw.id,
872
+ );
873
+
874
+ // @ts-ignore Update the person age in the branch
875
+ branch.getCurrentContent().set("age", 25);
876
+
877
+ let lastResult: any = null;
878
+ const listener = vi.fn();
879
+
880
+ // Subscribe to the specific branch
881
+ const subscription = new CoValueCoreSubscription(
882
+ person.$jazz.localNode,
883
+ person.$jazz.id,
884
+ (result) => {
885
+ lastResult = result;
886
+ listener(result);
887
+ },
888
+ false,
889
+ { name: "main", owner: person.$jazz.owner },
890
+ );
891
+
892
+ // Initial call with branch value
893
+ expect(listener).toHaveBeenCalledTimes(1);
894
+ expect(lastResult.get("age")).toEqual(25);
895
+
896
+ // @ts-ignore Update the person name in the branch
897
+ branch.getCurrentContent().set("name", "Jane");
898
+
899
+ // Wait for the update to be processed
900
+ await waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
901
+
902
+ // Check that we received updates for each change
903
+ expect(lastResult.get("name")).toEqual("Jane");
904
+ expect(lastResult.get("age")).toEqual(25); // Should remain from branch
905
+
906
+ subscription.unsubscribe();
907
+ });
908
+
909
+ test("should not receive updates when CoValue properties change in source", async () => {
910
+ // Define a Person schema with optional email field
911
+ const Person = co.map({
912
+ name: z.string(),
913
+ age: z.number(),
914
+ email: z.string().optional(),
915
+ });
916
+
917
+ // Create a person that's immediately available
918
+ const person = Person.create({ name: "John", age: 30 });
919
+
920
+ // Create a branch on the person
921
+ const branch = person.$jazz.raw.core.createBranch(
922
+ "main",
923
+ person.$jazz.owner.$jazz.raw.id,
924
+ );
925
+
926
+ // @ts-ignore Update the person age in the branch
927
+ branch.getCurrentContent().set("age", 25);
928
+
929
+ let lastResult: any = null;
930
+ const listener = vi.fn();
931
+
932
+ // Subscribe to the specific branch
933
+ const subscription = new CoValueCoreSubscription(
934
+ person.$jazz.localNode,
935
+ person.$jazz.id,
936
+ (result) => {
937
+ lastResult = result;
938
+ listener(result);
939
+ },
940
+ false,
941
+ { name: "main", owner: person.$jazz.owner },
942
+ );
943
+
944
+ // Initial call with branch value
945
+ expect(listener).toHaveBeenCalledTimes(1);
946
+ expect(lastResult.get("age")).toEqual(25);
947
+
948
+ // Update properties in the source (not the branch)
949
+ person.$jazz.set("age", 31);
950
+ person.$jazz.set("email", "john@example.com");
951
+
952
+ // Listener should not be called since we're subscribed to the branch, not the source
953
+ expect(listener).toHaveBeenCalledTimes(1);
954
+
955
+ subscription.unsubscribe();
956
+ });
957
+
958
+ test("should handle rapid successive updates correctly", async () => {
959
+ // Define a Person schema with score field
960
+ const Person = co.map({
961
+ name: z.string(),
962
+ age: z.number(),
963
+ score: z.number(),
964
+ });
965
+
966
+ // Create a person that's immediately available
967
+ const person = Person.create({ name: "John", age: 30, score: 100 });
968
+ let lastResult: any = null;
969
+ const listener = vi.fn();
970
+
971
+ // Subscribe to the person
972
+ const subscription = new CoValueCoreSubscription(
973
+ person.$jazz.localNode,
974
+ person.$jazz.id,
975
+ (result) => {
976
+ lastResult = result;
977
+ listener(result);
978
+ },
979
+ );
980
+
981
+ // Initial call with default values
982
+ expect(listener).toHaveBeenCalledTimes(1);
983
+
984
+ // Make rapid successive updates to test update handling
985
+ person.$jazz.set("age", 31);
986
+ person.$jazz.set("score", 150);
987
+ person.$jazz.set("name", "Jane");
988
+ person.$jazz.set("age", 32);
989
+
990
+ expect(listener).toHaveBeenCalledTimes(5);
991
+
992
+ // Check final state after all updates
993
+ expect(lastResult.get("name")).toEqual("Jane");
994
+ expect(lastResult.get("age")).toEqual(32);
995
+ expect(lastResult.get("score")).toEqual(150);
996
+
997
+ subscription.unsubscribe();
998
+ });
999
+ });
1000
+ });