jazz-tools 0.19.7 → 0.19.10

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 (167) hide show
  1. package/.turbo/turbo-build.log +65 -59
  2. package/CHANGELOG.md +34 -3
  3. package/dist/{chunk-CUS6O5NE.js → chunk-FFEEPZEG.js} +454 -122
  4. package/dist/chunk-FFEEPZEG.js.map +1 -0
  5. package/dist/expo/polyfills.js +22 -0
  6. package/dist/expo/polyfills.js.map +1 -0
  7. package/dist/index.js +26 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/react/hooks.d.ts +1 -1
  10. package/dist/react/hooks.d.ts.map +1 -1
  11. package/dist/react/index.d.ts +1 -1
  12. package/dist/react/index.d.ts.map +1 -1
  13. package/dist/react/index.js +5 -1
  14. package/dist/react/index.js.map +1 -1
  15. package/dist/react-core/hooks.d.ts +59 -0
  16. package/dist/react-core/hooks.d.ts.map +1 -1
  17. package/dist/react-core/index.js +133 -34
  18. package/dist/react-core/index.js.map +1 -1
  19. package/dist/react-core/tests/testUtils.d.ts +1 -0
  20. package/dist/react-core/tests/testUtils.d.ts.map +1 -1
  21. package/dist/react-core/tests/useSuspenseAccount.test.d.ts +2 -0
  22. package/dist/react-core/tests/useSuspenseAccount.test.d.ts.map +1 -0
  23. package/dist/react-core/tests/useSuspenseCoState.test.d.ts +2 -0
  24. package/dist/react-core/tests/useSuspenseCoState.test.d.ts.map +1 -0
  25. package/dist/react-core/use.d.ts +3 -0
  26. package/dist/react-core/use.d.ts.map +1 -0
  27. package/dist/react-native/index.d.ts +1 -1
  28. package/dist/react-native/index.d.ts.map +1 -1
  29. package/dist/react-native/index.js +717 -9
  30. package/dist/react-native/index.js.map +1 -1
  31. package/dist/react-native/polyfills.js +22 -0
  32. package/dist/react-native/polyfills.js.map +1 -0
  33. package/dist/react-native-core/crypto/RNCrypto.d.ts +2 -0
  34. package/dist/react-native-core/crypto/RNCrypto.d.ts.map +1 -0
  35. package/dist/react-native-core/crypto/RNCrypto.js +3 -0
  36. package/dist/react-native-core/crypto/RNCrypto.js.map +1 -0
  37. package/dist/react-native-core/hooks.d.ts +1 -1
  38. package/dist/react-native-core/hooks.d.ts.map +1 -1
  39. package/dist/react-native-core/index.d.ts.map +1 -1
  40. package/dist/react-native-core/index.js +5 -1
  41. package/dist/react-native-core/index.js.map +1 -1
  42. package/dist/react-native-core/platform.d.ts +2 -1
  43. package/dist/react-native-core/platform.d.ts.map +1 -1
  44. package/dist/testing.js +1 -1
  45. package/dist/testing.js.map +1 -1
  46. package/dist/tools/coValues/account.d.ts +3 -3
  47. package/dist/tools/coValues/account.d.ts.map +1 -1
  48. package/dist/tools/coValues/coFeed.d.ts +3 -3
  49. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  50. package/dist/tools/coValues/coList.d.ts +4 -4
  51. package/dist/tools/coValues/coList.d.ts.map +1 -1
  52. package/dist/tools/coValues/coMap.d.ts +7 -7
  53. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  54. package/dist/tools/coValues/coPlainText.d.ts +2 -2
  55. package/dist/tools/coValues/coPlainText.d.ts.map +1 -1
  56. package/dist/tools/coValues/coVector.d.ts +2 -2
  57. package/dist/tools/coValues/coVector.d.ts.map +1 -1
  58. package/dist/tools/coValues/deepLoading.d.ts +24 -0
  59. package/dist/tools/coValues/deepLoading.d.ts.map +1 -1
  60. package/dist/tools/coValues/group.d.ts +2 -2
  61. package/dist/tools/coValues/group.d.ts.map +1 -1
  62. package/dist/tools/coValues/interfaces.d.ts +7 -7
  63. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  64. package/dist/tools/coValues/schemaUnion.d.ts +2 -2
  65. package/dist/tools/coValues/schemaUnion.d.ts.map +1 -1
  66. package/dist/tools/config.d.ts +3 -0
  67. package/dist/tools/config.d.ts.map +1 -0
  68. package/dist/tools/exports.d.ts +2 -0
  69. package/dist/tools/exports.d.ts.map +1 -1
  70. package/dist/tools/implementation/ContextManager.d.ts +3 -0
  71. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  72. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +2 -2
  73. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
  74. package/dist/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.d.ts +2 -2
  75. package/dist/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.d.ts.map +1 -1
  76. package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts +2 -2
  77. package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts.map +1 -1
  78. package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts +4 -4
  79. package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts.map +1 -1
  80. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +4 -4
  81. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  82. package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts +4 -4
  83. package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts.map +1 -1
  84. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +2 -2
  85. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
  86. package/dist/tools/implementation/zodSchema/schemaTypes/GroupSchema.d.ts +2 -2
  87. package/dist/tools/implementation/zodSchema/schemaTypes/GroupSchema.d.ts.map +1 -1
  88. package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts +2 -2
  89. package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts.map +1 -1
  90. package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts +2 -2
  91. package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts.map +1 -1
  92. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  93. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +8 -22
  94. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  95. package/dist/tools/subscribe/JazzError.d.ts.map +1 -1
  96. package/dist/tools/subscribe/SubscriptionCache.d.ts +51 -0
  97. package/dist/tools/subscribe/SubscriptionCache.d.ts.map +1 -0
  98. package/dist/tools/subscribe/SubscriptionScope.d.ts +27 -2
  99. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  100. package/dist/tools/subscribe/errorReporting.d.ts +31 -0
  101. package/dist/tools/subscribe/errorReporting.d.ts.map +1 -0
  102. package/dist/tools/subscribe/utils.d.ts +9 -1
  103. package/dist/tools/subscribe/utils.d.ts.map +1 -1
  104. package/dist/tools/testing.d.ts +2 -2
  105. package/dist/tools/testing.d.ts.map +1 -1
  106. package/dist/tools/tests/SubscriptionCache.test.d.ts +2 -0
  107. package/dist/tools/tests/SubscriptionCache.test.d.ts.map +1 -0
  108. package/dist/tools/tests/errorReporting.test.d.ts +2 -0
  109. package/dist/tools/tests/errorReporting.test.d.ts.map +1 -0
  110. package/package.json +22 -7
  111. package/src/react/hooks.tsx +2 -0
  112. package/src/react/index.ts +1 -14
  113. package/src/react-core/hooks.ts +181 -16
  114. package/src/react-core/tests/createCoValueSubscriptionContext.test.tsx +18 -8
  115. package/src/react-core/tests/testUtils.tsx +67 -5
  116. package/src/react-core/tests/useCoState.test.ts +3 -7
  117. package/src/react-core/tests/useSubscriptionSelector.test.ts +3 -7
  118. package/src/react-core/tests/useSuspenseAccount.test.tsx +343 -0
  119. package/src/react-core/tests/useSuspenseCoState.test.tsx +1182 -0
  120. package/src/react-core/use.ts +46 -0
  121. package/src/react-native/index.ts +1 -1
  122. package/src/react-native-core/crypto/RNCrypto.ts +1 -0
  123. package/src/react-native-core/hooks.tsx +2 -0
  124. package/src/react-native-core/index.ts +2 -0
  125. package/src/react-native-core/platform.ts +2 -1
  126. package/src/react-native-core/polyfills/index.js +28 -0
  127. package/src/tools/coValues/account.ts +3 -4
  128. package/src/tools/coValues/coFeed.ts +3 -2
  129. package/src/tools/coValues/coList.ts +4 -4
  130. package/src/tools/coValues/coMap.ts +4 -4
  131. package/src/tools/coValues/coPlainText.ts +2 -2
  132. package/src/tools/coValues/coVector.ts +2 -2
  133. package/src/tools/coValues/deepLoading.ts +31 -0
  134. package/src/tools/coValues/group.ts +2 -2
  135. package/src/tools/coValues/interfaces.ts +21 -26
  136. package/src/tools/coValues/schemaUnion.ts +2 -2
  137. package/src/tools/config.ts +9 -0
  138. package/src/tools/exports.ts +4 -0
  139. package/src/tools/implementation/ContextManager.ts +13 -0
  140. package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +2 -2
  141. package/src/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.ts +2 -2
  142. package/src/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.ts +2 -2
  143. package/src/tools/implementation/zodSchema/schemaTypes/CoListSchema.ts +4 -4
  144. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +4 -4
  145. package/src/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.ts +4 -10
  146. package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +2 -2
  147. package/src/tools/implementation/zodSchema/schemaTypes/GroupSchema.ts +2 -2
  148. package/src/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.ts +2 -2
  149. package/src/tools/implementation/zodSchema/schemaTypes/RichTextSchema.ts +2 -2
  150. package/src/tools/subscribe/CoValueCoreSubscription.ts +71 -100
  151. package/src/tools/subscribe/JazzError.ts +9 -6
  152. package/src/tools/subscribe/SubscriptionCache.ts +272 -0
  153. package/src/tools/subscribe/SubscriptionScope.ts +218 -29
  154. package/src/tools/subscribe/errorReporting.ts +67 -0
  155. package/src/tools/subscribe/utils.ts +77 -0
  156. package/src/tools/testing.ts +0 -3
  157. package/src/tools/tests/CoValueCoreSubscription.test.ts +46 -12
  158. package/src/tools/tests/ContextManager.test.ts +85 -0
  159. package/src/tools/tests/SubscriptionCache.test.ts +237 -0
  160. package/src/tools/tests/coMap.test.ts +5 -7
  161. package/src/tools/tests/deepLoading.test.ts +47 -47
  162. package/src/tools/tests/errorReporting.test.ts +103 -0
  163. package/src/tools/tests/load.test.ts +21 -1
  164. package/src/tools/tests/request.test.ts +2 -1
  165. package/src/tools/tests/subscribe.test.ts +44 -0
  166. package/tsup.config.ts +17 -0
  167. package/dist/chunk-CUS6O5NE.js.map +0 -1
@@ -3,6 +3,7 @@ import { RegisteredSchemas } from "../coValues/registeredSchemas.js";
3
3
  import {
4
4
  CoValue,
5
5
  RefEncoded,
6
+ RefsToResolve,
6
7
  accountOrGroupToGroup,
7
8
  instantiateRefEncodedFromRaw,
8
9
  } from "../internal.js";
@@ -42,3 +43,79 @@ export function createCoValue<D extends CoValue>(
42
43
  id: subscriptionScope.id,
43
44
  };
44
45
  }
46
+
47
+ export type PromiseWithStatus<T> = PromiseLike<T> & {
48
+ status?: "pending" | "fulfilled" | "rejected";
49
+ value?: T;
50
+ reason?: unknown;
51
+ };
52
+
53
+ export function resolvedPromise<T>(value: T): PromiseWithStatus<T> {
54
+ const promise = Promise.resolve(value) as PromiseWithStatus<T>;
55
+ promise.status = "fulfilled";
56
+ promise.value = value;
57
+ return promise;
58
+ }
59
+
60
+ export function rejectedPromise<T>(reason: unknown): PromiseWithStatus<T> {
61
+ const promise = Promise.reject(reason) as PromiseWithStatus<T>;
62
+ promise.status = "rejected";
63
+ promise.reason = reason;
64
+ return promise;
65
+ }
66
+
67
+ export function isEqualRefsToResolve(
68
+ a: RefsToResolve<any>,
69
+ b: RefsToResolve<any>,
70
+ ) {
71
+ // Fast path: same reference
72
+ if (a === b) {
73
+ return true;
74
+ }
75
+
76
+ // Fast path: both are boolean
77
+ if (typeof a === "boolean" && typeof b === "boolean") {
78
+ return a === b;
79
+ }
80
+
81
+ // One is boolean, the other is not
82
+ if (typeof a === "boolean" || typeof b === "boolean") {
83
+ return false;
84
+ }
85
+
86
+ // Both must be objects at this point
87
+ if (
88
+ typeof a !== "object" ||
89
+ typeof b !== "object" ||
90
+ a === null ||
91
+ b === null
92
+ ) {
93
+ return false;
94
+ }
95
+
96
+ // Get all keys from both objects
97
+ const keysA = Object.keys(a);
98
+ const keysB = Object.keys(b);
99
+
100
+ // Different number of keys means not equal
101
+ if (keysA.length !== keysB.length) {
102
+ return false;
103
+ }
104
+
105
+ // Check each key
106
+ for (const key of keysA) {
107
+ if (!(key in b)) {
108
+ return false;
109
+ }
110
+
111
+ const valueA = (a as any)[key];
112
+ const valueB = (b as any)[key];
113
+
114
+ // Recursively compare nested RefsToResolve values
115
+ if (!isEqualRefsToResolve(valueA, valueB)) {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ return true;
121
+ }
@@ -16,9 +16,6 @@ import {
16
16
  coValueClassFromCoValueClassOrSchema,
17
17
  createAnonymousJazzContext,
18
18
  createJazzContext,
19
- CoValue,
20
- LoadedAndRequired,
21
- MaybeLoaded,
22
19
  randomSessionProvider,
23
20
  } from "./internal.js";
24
21
 
@@ -14,6 +14,7 @@ import {
14
14
  setupJazzTestSync,
15
15
  } from "../testing.js";
16
16
  import { waitFor } from "../tests/utils.js";
17
+ import { cojsonInternals } from "cojson";
17
18
 
18
19
  beforeEach(async () => {
19
20
  await setupJazzTestSync();
@@ -25,6 +26,8 @@ beforeEach(async () => {
25
26
  });
26
27
  });
27
28
 
29
+ cojsonInternals.setCoValueLoadingRetryDelay(10);
30
+
28
31
  describe("CoValueCoreSubscription", async () => {
29
32
  /**
30
33
  * Tests scenarios where the CoValue is immediately available
@@ -444,7 +447,7 @@ describe("CoValueCoreSubscription", async () => {
444
447
  });
445
448
 
446
449
  describe("error handling scenarios", () => {
447
- test("should handle return unavailable when the id is invalid", async () => {
450
+ test("should synchronously emit unavailable when an invalid id is provided", async () => {
448
451
  const bob = await createJazzTestAccount();
449
452
  const invalidId = "invalid-co-value-id";
450
453
 
@@ -461,13 +464,50 @@ describe("CoValueCoreSubscription", async () => {
461
464
  },
462
465
  );
463
466
 
464
- // Should not call listener immediately since ID is invalid
465
- expect(listener).not.toHaveBeenCalled();
467
+ // Should call listener synchronously since ID is invalid (doesn't start with "co_z")
468
+ expect(listener).toHaveBeenCalledTimes(1);
469
+ expect(lastResult).toBe(CoValueLoadingState.UNAVAILABLE);
470
+
471
+ subscription.unsubscribe();
472
+ });
473
+
474
+ test("should emit unavailable when subscribing to a CoValue that has already been marked as unavailable", async () => {
475
+ const Person = co.map({
476
+ name: z.string(),
477
+ age: z.number(),
478
+ });
479
+
480
+ // Create a person that bob can access
481
+ const person = Person.create(
482
+ { name: "John", age: 30 },
483
+ Group.create().makePublic("writer"),
484
+ );
485
+
486
+ // Create a new sync server, this way the CoValue is not available for new accounts
487
+ await setupJazzTestSync();
488
+ const bob = await createJazzTestAccount();
489
+
490
+ // Try to load it first, to mark the value as unavailable
491
+ await Person.load(person.$jazz.id, {
492
+ loadAs: bob,
493
+ });
494
+
495
+ let lastResult: any = null;
496
+ const listener = vi.fn();
497
+
498
+ // Subscribe to the unavailable CoValue
499
+ const subscription = new CoValueCoreSubscription(
500
+ bob.$jazz.localNode,
501
+ person.$jazz.id,
502
+ (result) => {
503
+ lastResult = result;
504
+ listener(result);
505
+ },
506
+ );
466
507
 
467
- // Wait for the error handling to complete
468
508
  await waitFor(() => expect(listener).toHaveBeenCalled());
469
509
 
470
- // Should report unavailable when loading fails
510
+ expect(listener).toHaveBeenCalledTimes(1);
471
511
  expect(lastResult).toBe(CoValueLoadingState.UNAVAILABLE);
472
512
 
473
513
  subscription.unsubscribe();
@@ -492,13 +532,7 @@ describe("CoValueCoreSubscription", async () => {
492
532
  { name: "main", owner: bob },
493
533
  );
494
534
 
495
- // Should not call listener immediately since ID is invalid
496
- expect(listener).not.toHaveBeenCalled();
497
-
498
- // Wait for the error handling to complete
499
- await waitFor(() => expect(listener).toHaveBeenCalled());
500
-
501
- // Should report unavailable when loading fails
535
+ expect(listener).toHaveBeenCalledTimes(1);
502
536
  expect(lastResult).toBe(CoValueLoadingState.UNAVAILABLE);
503
537
 
504
538
  subscription.unsubscribe();
@@ -33,6 +33,7 @@ import {
33
33
  getPeerConnectedToTestSyncServer,
34
34
  setupJazzTestSync,
35
35
  } from "../testing";
36
+ import { SubscriptionCache } from "../subscribe/SubscriptionCache";
36
37
  import { createAsyncStorage, getDbPath } from "./testStorage";
37
38
 
38
39
  const Crypto = await WasmCrypto.create();
@@ -809,4 +810,88 @@ describe("ContextManager", () => {
809
810
  );
810
811
  });
811
812
  });
813
+
814
+ describe("SubscriptionCache integration", () => {
815
+ test("initializes cache on construction", () => {
816
+ const newManager = new TestJazzContextManager<Account>();
817
+ const cache = newManager.getSubscriptionScopeCache();
818
+
819
+ expect(cache).toBeDefined();
820
+ expect(cache).toBeInstanceOf(SubscriptionCache);
821
+ });
822
+
823
+ test("getSubscriptionScopeCache returns the cache instance", () => {
824
+ const cache1 = manager.getSubscriptionScopeCache();
825
+ const cache2 = manager.getSubscriptionScopeCache();
826
+
827
+ expect(cache1).toBe(cache2);
828
+ expect(cache1).toBeDefined();
829
+ });
830
+
831
+ test("updateContext clears the cache", async () => {
832
+ await manager.createContext({});
833
+
834
+ const cache = manager.getSubscriptionScopeCache();
835
+ const node = getCurrentValue().node;
836
+ const Person = co.map({ name: z.string() });
837
+ const person = Person.create({ name: "Test" });
838
+
839
+ // Create a subscription in the cache
840
+ const scope = cache.getOrCreate(
841
+ node,
842
+ Person,
843
+ person.$jazz.id,
844
+ true,
845
+ false,
846
+ false,
847
+ );
848
+
849
+ // Verify it's in the cache
850
+ const idSet = (cache as any).cache.get(person.$jazz.id);
851
+ expect(idSet).toBeDefined();
852
+ expect(idSet).toBeInstanceOf(Set);
853
+ expect(idSet.size).toBeGreaterThan(0);
854
+
855
+ // Update context - should clear cache
856
+ await manager.createContext({});
857
+
858
+ // Verify cache was cleared
859
+ const idSetAfter = (cache as any).cache.get(person.$jazz.id);
860
+ expect(idSetAfter).toBeUndefined();
861
+ expect(scope.closed).toBe(true);
862
+ });
863
+
864
+ test("logOut clears the cache", async () => {
865
+ await manager.createContext({});
866
+
867
+ const cache = manager.getSubscriptionScopeCache();
868
+ const node = getCurrentValue().node;
869
+ const Person = co.map({ name: z.string() });
870
+ const person = Person.create({ name: "Test" });
871
+
872
+ // Create a subscription in the cache
873
+ const scope = cache.getOrCreate(
874
+ node,
875
+ Person,
876
+ person.$jazz.id,
877
+ true,
878
+ false,
879
+ false,
880
+ );
881
+
882
+ // Verify it's in the cache
883
+ const scopeSet = (cache as any).cache.get(person.$jazz.id);
884
+ expect(scopeSet).toBeDefined();
885
+ expect(scopeSet).toBeInstanceOf(Set);
886
+ expect(scopeSet.size).toBeGreaterThan(0);
887
+
888
+ // Logout - should clear cache
889
+ await manager.logOut();
890
+
891
+ // Verify cache was cleared
892
+ const scopeSetAfter = (cache as any).cache.get(person.$jazz.id);
893
+ expect(scopeSetAfter).toBeUndefined();
894
+ expect(scope.closed).toBe(true);
895
+ });
896
+ });
812
897
  });
@@ -0,0 +1,237 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { Account, Group, co, z } from "../exports.js";
3
+ import { SubscriptionCache } from "../subscribe/SubscriptionCache.js";
4
+ import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
5
+
6
+ describe("SubscriptionCache", () => {
7
+ const Person = co.map({
8
+ name: co.plainText(),
9
+ });
10
+
11
+ beforeEach(async () => {
12
+ await setupJazzTestSync();
13
+ await createJazzTestAccount({
14
+ isCurrentActiveAccount: true,
15
+ creationProps: { name: "Hermes Puggington" },
16
+ });
17
+ });
18
+
19
+ describe("cache key comparison logic", () => {
20
+ it("matches entries with identical schema, id, resolve, and branch", () => {
21
+ const person = Person.create({ name: "John" });
22
+ const node = person.$jazz.raw.core.node;
23
+ const id = person.$jazz.id;
24
+ const cache = new SubscriptionCache();
25
+
26
+ const scope1 = cache.getOrCreate(node, Person, id, true, false, false);
27
+ const scope2 = cache.getOrCreate(node, Person, id, true, false, false);
28
+
29
+ expect(scope1).toBe(scope2);
30
+ cache.clear();
31
+ });
32
+
33
+ it("creates different entries for different resolve queries", () => {
34
+ const person = Person.create({ name: "John" });
35
+ const node = person.$jazz.raw.core.node;
36
+ const id = person.$jazz.id;
37
+ const cache = new SubscriptionCache();
38
+
39
+ const scope1 = cache.getOrCreate(node, Person, id, true, false, false);
40
+ const scope2 = cache.getOrCreate(
41
+ node,
42
+ Person,
43
+ id,
44
+ { name: true },
45
+ false,
46
+ false,
47
+ );
48
+
49
+ expect(scope1).not.toBe(scope2);
50
+ cache.clear();
51
+ });
52
+
53
+ it("creates different entries for different branch definitions", () => {
54
+ const person = Person.create({ name: "John" });
55
+ const group = Group.create();
56
+ const node = person.$jazz.raw.core.node;
57
+ const id = person.$jazz.id;
58
+ const cache = new SubscriptionCache();
59
+
60
+ const scope1 = cache.getOrCreate(node, Person, id, true, false, false, {
61
+ name: "branch1",
62
+ owner: group,
63
+ });
64
+ const scope2 = cache.getOrCreate(node, Person, id, true, false, false, {
65
+ name: "branch2",
66
+ owner: group,
67
+ });
68
+
69
+ expect(scope1).not.toBe(scope2);
70
+ cache.clear();
71
+ });
72
+
73
+ it("matches entries with same branch name but different owner references", () => {
74
+ const person = Person.create({ name: "John" });
75
+ const group1 = Group.create();
76
+ const group2 = Group.create();
77
+ const node = person.$jazz.raw.core.node;
78
+ const id = person.$jazz.id;
79
+ const cache = new SubscriptionCache();
80
+
81
+ const scope1 = cache.getOrCreate(node, Person, id, true, false, false, {
82
+ name: "branch",
83
+ owner: group1,
84
+ });
85
+ const scope2 = cache.getOrCreate(node, Person, id, true, false, false, {
86
+ name: "branch",
87
+ owner: group2,
88
+ });
89
+
90
+ expect(scope1).not.toBe(scope2);
91
+ cache.clear();
92
+ });
93
+ });
94
+
95
+ describe("subscriber count tracking", () => {
96
+ it("tracks subscriber count changes via onSubscriberChange", () => {
97
+ const person = Person.create({ name: "John" });
98
+ const node = person.$jazz.raw.core.node;
99
+ const id = person.$jazz.id;
100
+ const cache = new SubscriptionCache();
101
+
102
+ const scope = cache.getOrCreate(node, Person, id, true, false, false);
103
+
104
+ expect(scope.subscribers.size).toBe(0);
105
+
106
+ const unsubscribe1 = scope.subscribe(() => {});
107
+ expect(scope.subscribers.size).toBe(1);
108
+
109
+ const unsubscribe2 = scope.subscribe(() => {});
110
+ expect(scope.subscribers.size).toBe(2);
111
+
112
+ unsubscribe1();
113
+ expect(scope.subscribers.size).toBe(1);
114
+
115
+ unsubscribe2();
116
+ expect(scope.subscribers.size).toBe(0);
117
+
118
+ cache.clear();
119
+ });
120
+ });
121
+
122
+ describe("cleanup lifecycle", () => {
123
+ it("schedules cleanup when subscriber count reaches zero", async () => {
124
+ vi.useFakeTimers();
125
+ const person = Person.create({ name: "John" });
126
+ const node = person.$jazz.raw.core.node;
127
+ const id = person.$jazz.id;
128
+ const cache = new SubscriptionCache(100); // 100ms timeout
129
+
130
+ const scope = cache.getOrCreate(node, Person, id, true, false, false);
131
+
132
+ const unsubscribe = scope.subscribe(() => {});
133
+ unsubscribe();
134
+
135
+ await vi.advanceTimersByTimeAsync(110);
136
+
137
+ expect(scope.closed).toBe(true);
138
+
139
+ cache.clear();
140
+ vi.useRealTimers();
141
+ });
142
+
143
+ it("cancels cleanup when new subscription arrives during pending cleanup", async () => {
144
+ vi.useFakeTimers();
145
+ const person = Person.create({ name: "John" });
146
+ const node = person.$jazz.raw.core.node;
147
+ const id = person.$jazz.id;
148
+ const cache = new SubscriptionCache(100); // 100ms timeout
149
+
150
+ const scope1 = cache.getOrCreate(node, Person, id, true, false, false);
151
+
152
+ const unsubscribe = scope1.subscribe(() => {});
153
+ unsubscribe();
154
+
155
+ await vi.advanceTimersByTimeAsync(50);
156
+
157
+ // Request again before cleanup
158
+ const scope2 = cache.getOrCreate(node, Person, id, true, false, false);
159
+
160
+ expect(scope2).toBe(scope1);
161
+ await vi.advanceTimersByTimeAsync(60);
162
+
163
+ cache.clear();
164
+ vi.useRealTimers();
165
+ });
166
+
167
+ it("executes cleanup after timeout", async () => {
168
+ vi.useFakeTimers();
169
+ const person = Person.create({ name: "John" });
170
+ const node = person.$jazz.raw.core.node;
171
+ const id = person.$jazz.id;
172
+ const cache = new SubscriptionCache(100); // 100ms timeout
173
+
174
+ const scope = cache.getOrCreate(node, Person, id, true, false, false);
175
+
176
+ const unsubscribe = scope.subscribe(() => {});
177
+ unsubscribe();
178
+
179
+ await vi.advanceTimersByTimeAsync(150);
180
+
181
+ const idSet = (cache as any).cache.get(id);
182
+ expect(idSet).toBeUndefined();
183
+
184
+ cache.clear();
185
+ vi.useRealTimers();
186
+ });
187
+ });
188
+
189
+ describe("clear method", () => {
190
+ it("destroys all entries", () => {
191
+ const person1 = Person.create({ name: "John" });
192
+ const person2 = Person.create({ name: "Jane" });
193
+ const node = person1.$jazz.raw.core.node;
194
+ const cache = new SubscriptionCache();
195
+
196
+ const scope1 = cache.getOrCreate(
197
+ node,
198
+ Person,
199
+ person1.$jazz.id,
200
+ true,
201
+ false,
202
+ false,
203
+ );
204
+ const scope2 = cache.getOrCreate(
205
+ node,
206
+ Person,
207
+ person2.$jazz.id,
208
+ true,
209
+ false,
210
+ false,
211
+ );
212
+
213
+ cache.clear();
214
+
215
+ expect(scope1.closed).toBe(true);
216
+ expect(scope2.closed).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe("edge cases", () => {
221
+ it("throws error for null/undefined id", () => {
222
+ const person = Person.create({ name: "John" });
223
+ const node = person.$jazz.raw.core.node;
224
+ const cache = new SubscriptionCache();
225
+
226
+ expect(() => {
227
+ cache.getOrCreate(node, Person, null as any, true, false, false);
228
+ }).toThrow("Cannot create subscription with undefined or null id");
229
+
230
+ expect(() => {
231
+ cache.getOrCreate(node, Person, undefined as any, true, false, false);
232
+ }).toThrow("Cannot create subscription with undefined or null id");
233
+
234
+ cache.clear();
235
+ });
236
+ });
237
+ });
@@ -1098,6 +1098,7 @@ describe("CoMap resolution", async () => {
1098
1098
  });
1099
1099
 
1100
1100
  test("loading a remotely available map with skipRetry set to false", async () => {
1101
+ disableJazzTestSync();
1101
1102
  // Make the retry delay extra long to avoid flakyness in the resolved checks
1102
1103
  cojsonInternals.CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 100_000_000;
1103
1104
 
@@ -1114,13 +1115,6 @@ describe("CoMap resolution", async () => {
1114
1115
 
1115
1116
  const currentAccount = Account.getMe();
1116
1117
 
1117
- // Disconnect the current account
1118
- currentAccount.$jazz.localNode.syncManager
1119
- .getServerPeers(currentAccount.$jazz.raw.id)
1120
- .forEach((peer) => {
1121
- peer.gracefulShutdown();
1122
- });
1123
-
1124
1118
  const group = Group.create();
1125
1119
  group.addMember("everyone", "writer");
1126
1120
 
@@ -1133,6 +1127,10 @@ describe("CoMap resolution", async () => {
1133
1127
  group,
1134
1128
  );
1135
1129
 
1130
+ await setupJazzTestSync({
1131
+ asyncPeers: true,
1132
+ });
1133
+
1136
1134
  const userB = await createJazzTestAccount();
1137
1135
  let resolved = false;
1138
1136
  const promise = Person.load(person.$jazz.id, {