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
@@ -0,0 +1,272 @@
1
+ import { LocalNode } from "cojson";
2
+ import type {
3
+ CoValue,
4
+ CoValueClassOrSchema,
5
+ RefEncoded,
6
+ RefsToResolve,
7
+ ResolveQuery,
8
+ } from "../internal.js";
9
+ import { coValueClassFromCoValueClassOrSchema } from "../internal.js";
10
+ import { SubscriptionScope } from "./SubscriptionScope.js";
11
+ import type { BranchDefinition } from "./types.js";
12
+ import { isEqualRefsToResolve } from "./utils.js";
13
+
14
+ interface CacheEntry {
15
+ subscriptionScope: SubscriptionScope<any>;
16
+ schema: CoValueClassOrSchema;
17
+ resolve: RefsToResolve<any>;
18
+ branch?: BranchDefinition;
19
+ subscriberCount: number;
20
+ cleanupTimeoutId?: ReturnType<typeof setTimeout>;
21
+ unsubscribeFromScope: () => void;
22
+ }
23
+
24
+ export class SubscriptionCache {
25
+ // Nested cache: outer map keyed by id, inner set of CacheEntry
26
+ private cache: Map<string, Set<CacheEntry>>;
27
+ private cleanupTimeout: number;
28
+
29
+ constructor(cleanupTimeout: number = 5000) {
30
+ this.cache = new Map();
31
+ this.cleanupTimeout = cleanupTimeout;
32
+ }
33
+
34
+ /**
35
+ * Get the inner set for a given id (read-only access)
36
+ */
37
+ private getIdSet(id: string): Set<CacheEntry> | undefined {
38
+ return this.cache.get(id);
39
+ }
40
+
41
+ /**
42
+ * Get the inner set for a given id, creating it if it doesn't exist
43
+ */
44
+ private getIdSetOrCreate(id: string): Set<CacheEntry> {
45
+ let idSet = this.cache.get(id);
46
+ if (!idSet) {
47
+ idSet = new Set();
48
+ this.cache.set(id, idSet);
49
+ }
50
+ return idSet;
51
+ }
52
+
53
+ /**
54
+ * Check if an entry matches the provided parameters
55
+ */
56
+ private matchesEntry(
57
+ entry: CacheEntry,
58
+ schema: CoValueClassOrSchema,
59
+ resolve: RefsToResolve<any>,
60
+ branch?: BranchDefinition,
61
+ ): boolean {
62
+ // Compare schema by object identity
63
+ if (entry.schema !== schema) {
64
+ return false;
65
+ }
66
+
67
+ // Compare resolve queries using isEqualRefsToResolve
68
+ if (!isEqualRefsToResolve(entry.resolve, resolve)) {
69
+ return false;
70
+ }
71
+
72
+ // Compare branch names by string equality
73
+ const branchName = branch?.name;
74
+ if (entry.branch?.name !== branchName) {
75
+ return false;
76
+ }
77
+
78
+ // Compare branch owner ids by string equality
79
+ const branchOwnerId = branch?.owner?.$jazz.id;
80
+ if (entry.branch?.owner?.$jazz.id !== branchOwnerId) {
81
+ return false;
82
+ }
83
+
84
+ return true;
85
+ }
86
+
87
+ /**
88
+ * Find a matching cache entry by comparing against entry properties
89
+ * Uses id-based nesting to quickly filter candidates
90
+ */
91
+ private findMatchingEntry(
92
+ schema: CoValueClassOrSchema,
93
+ id: string,
94
+ resolve: RefsToResolve<any>,
95
+ branch?: BranchDefinition,
96
+ ): CacheEntry | undefined {
97
+ // Get the inner set for this id (quick filter)
98
+ const idSet = this.getIdSet(id);
99
+ if (!idSet) {
100
+ return undefined;
101
+ }
102
+
103
+ // Search only within entries for this id
104
+ for (const entry of idSet) {
105
+ if (this.matchesEntry(entry, schema, resolve, branch)) {
106
+ return entry;
107
+ }
108
+ }
109
+
110
+ return undefined;
111
+ }
112
+
113
+ /**
114
+ * Handle subscriber count changes from SubscriptionScope
115
+ */
116
+ private handleSubscriberChange(entry: CacheEntry, count: number): void {
117
+ entry.subscriberCount = count;
118
+
119
+ if (count === 0) {
120
+ // Schedule cleanup when subscriber count reaches zero
121
+ this.scheduleCleanup(entry);
122
+ } else {
123
+ // Cancel cleanup if count increases from zero
124
+ this.cancelCleanup(entry);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Schedule cleanup timeout for an entry
130
+ */
131
+ private scheduleCleanup(entry: CacheEntry): void {
132
+ // Cancel any existing cleanup timeout
133
+ this.cancelCleanup(entry);
134
+
135
+ entry.cleanupTimeoutId = setTimeout(() => {
136
+ this.destroyEntry(entry);
137
+ }, this.cleanupTimeout);
138
+ }
139
+
140
+ /**
141
+ * Cancel pending cleanup timeout for an entry
142
+ */
143
+ private cancelCleanup(entry: CacheEntry): void {
144
+ if (entry.cleanupTimeoutId !== undefined) {
145
+ clearTimeout(entry.cleanupTimeoutId);
146
+ entry.cleanupTimeoutId = undefined;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Destroy a cache entry and its SubscriptionScope
152
+ */
153
+ private destroyEntry(entry: CacheEntry): void {
154
+ // Cancel any pending cleanup
155
+ this.cancelCleanup(entry);
156
+
157
+ // Unsubscribe from subscriber changes
158
+ entry.unsubscribeFromScope();
159
+
160
+ // Destroy the SubscriptionScope
161
+ try {
162
+ entry.subscriptionScope.destroy();
163
+ } catch (error) {
164
+ // Log error but don't throw - we still want to remove the entry
165
+ console.error("Error destroying SubscriptionScope:", error);
166
+ }
167
+
168
+ // Remove from nested cache structure
169
+ const id = entry.subscriptionScope.id;
170
+ const idSet = this.getIdSet(id);
171
+ if (idSet) {
172
+ idSet.delete(entry);
173
+ // Clean up empty inner set to prevent memory leaks
174
+ if (idSet.size === 0) {
175
+ this.cache.delete(id);
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get or create a SubscriptionScope from the cache
182
+ */
183
+ getOrCreate<S extends CoValueClassOrSchema>(
184
+ node: LocalNode,
185
+ schema: S,
186
+ id: string,
187
+ resolve: ResolveQuery<S>,
188
+ skipRetry?: boolean,
189
+ bestEffortResolution?: boolean,
190
+ branch?: BranchDefinition,
191
+ ): SubscriptionScope<CoValue> {
192
+ // Handle undefined/null id case
193
+ if (!id) {
194
+ throw new Error("Cannot create subscription with undefined or null id");
195
+ }
196
+
197
+ // Search for matching entry
198
+ const matchingEntry = this.findMatchingEntry(schema, id, resolve, branch);
199
+
200
+ if (matchingEntry) {
201
+ // Found existing entry - cancel any pending cleanup since we're reusing it
202
+ this.cancelCleanup(matchingEntry);
203
+
204
+ return matchingEntry.subscriptionScope as SubscriptionScope<CoValue>;
205
+ }
206
+
207
+ // Create new SubscriptionScope
208
+ // Transform schema to RefEncoded format
209
+ const refEncoded: RefEncoded<CoValue> = {
210
+ ref: coValueClassFromCoValueClassOrSchema(schema) as any,
211
+ optional: true,
212
+ };
213
+
214
+ // Create new SubscriptionScope with all required parameters
215
+ const subscriptionScope = new SubscriptionScope<CoValue>(
216
+ node,
217
+ // @ts-expect-error the SubscriptionScope is too generic for TS to infer its instances are CoValues
218
+ resolve,
219
+ id,
220
+ refEncoded,
221
+ skipRetry ?? false,
222
+ bestEffortResolution ?? false,
223
+ branch,
224
+ );
225
+
226
+ const handleSubscriberChange = (count: number) => {
227
+ const idSet = this.getIdSet(id);
228
+ if (idSet && idSet.has(entry)) {
229
+ this.handleSubscriberChange(entry, count);
230
+ }
231
+ };
232
+
233
+ // Create cache entry with initial subscriber count (starts at 0)
234
+ const entry: CacheEntry = {
235
+ subscriptionScope,
236
+ schema,
237
+ resolve,
238
+ branch,
239
+ subscriberCount: subscriptionScope.subscribers.size,
240
+ unsubscribeFromScope: subscriptionScope.onSubscriberChange(
241
+ handleSubscriberChange,
242
+ ),
243
+ };
244
+
245
+ // Store in nested cache structure
246
+ const idSet = this.getIdSetOrCreate(id);
247
+ idSet.add(entry);
248
+
249
+ return subscriptionScope;
250
+ }
251
+
252
+ /**
253
+ * Clear all cache entries and destroy all SubscriptionScope instances
254
+ */
255
+ clear(): void {
256
+ // Collect all entries first to avoid iteration issues during deletion
257
+ const entriesToDestroy: CacheEntry[] = [];
258
+ for (const idSet of this.cache.values()) {
259
+ for (const entry of idSet) {
260
+ entriesToDestroy.push(entry);
261
+ }
262
+ }
263
+
264
+ // Destroy all entries
265
+ for (const entry of entriesToDestroy) {
266
+ this.destroyEntry(entry);
267
+ }
268
+
269
+ // Clear the cache map (should already be empty, but ensure it)
270
+ this.cache.clear();
271
+ }
272
+ }
@@ -1,4 +1,4 @@
1
- import type { LocalNode, RawCoValue } from "cojson";
1
+ import { LocalNode, RawCoValue } from "cojson";
2
2
  import {
3
3
  CoFeed,
4
4
  CoList,
@@ -23,7 +23,18 @@ import type {
23
23
  SubscriptionValueLoading,
24
24
  } from "./types.js";
25
25
  import { CoValueLoadingState, NotLoadedCoValueState } from "./types.js";
26
- import { createCoValue, myRoleForRawValue } from "./utils.js";
26
+ import {
27
+ captureError,
28
+ isCustomErrorReportingEnabled,
29
+ } from "./errorReporting.js";
30
+ import {
31
+ createCoValue,
32
+ isEqualRefsToResolve,
33
+ myRoleForRawValue,
34
+ PromiseWithStatus,
35
+ rejectedPromise,
36
+ resolvedPromise,
37
+ } from "./utils.js";
27
38
 
28
39
  export class SubscriptionScope<D extends CoValue> {
29
40
  childNodes = new Map<string, SubscriptionScope<CoValue>>();
@@ -58,6 +69,13 @@ export class SubscriptionScope<D extends CoValue> {
58
69
 
59
70
  silenceUpdates = false;
60
71
 
72
+ /**
73
+ * Stack trace captured at subscription creation time.
74
+ * This helps identify which component/hook created the subscription
75
+ * when debugging "value unavailable" errors.
76
+ */
77
+ callerStack: Error | undefined;
78
+
61
79
  constructor(
62
80
  public node: LocalNode,
63
81
  resolve: RefsToResolve<D>,
@@ -66,7 +84,10 @@ export class SubscriptionScope<D extends CoValue> {
66
84
  public skipRetry = false,
67
85
  public bestEffortResolution = false,
68
86
  public unstable_branch?: BranchDefinition,
87
+ callerStack?: Error | undefined,
69
88
  ) {
89
+ // Use caller stack if provided, otherwise capture here (less useful but better than nothing)
90
+ this.callerStack = callerStack;
70
91
  this.resolve = resolve;
71
92
  this.value = { type: CoValueLoadingState.LOADING, id };
72
93
 
@@ -74,6 +95,7 @@ export class SubscriptionScope<D extends CoValue> {
74
95
  | RawCoValue
75
96
  | typeof CoValueLoadingState.UNAVAILABLE
76
97
  | undefined;
98
+
77
99
  this.subscription = new CoValueCoreSubscription(
78
100
  node,
79
101
  id,
@@ -126,37 +148,40 @@ export class SubscriptionScope<D extends CoValue> {
126
148
  handleUpdate(update: RawCoValue | typeof CoValueLoadingState.UNAVAILABLE) {
127
149
  if (update === CoValueLoadingState.UNAVAILABLE) {
128
150
  if (this.value.type === CoValueLoadingState.LOADING) {
129
- this.updateValue(
130
- new JazzError(this.id, CoValueLoadingState.UNAVAILABLE, [
131
- {
132
- code: CoValueLoadingState.UNAVAILABLE,
133
- message: "The value is unavailable",
134
- params: {
135
- id: this.id,
136
- },
137
- path: [],
151
+ const error = new JazzError(this.id, CoValueLoadingState.UNAVAILABLE, [
152
+ {
153
+ code: CoValueLoadingState.UNAVAILABLE,
154
+ message: `Jazz Unavailable Error: unable to load ${this.id}`,
155
+ params: {
156
+ id: this.id,
138
157
  },
139
- ]),
140
- );
158
+ path: [],
159
+ },
160
+ ]);
161
+
162
+ this.updateValue(error);
141
163
  }
164
+
142
165
  this.triggerUpdate();
143
166
  return;
144
167
  }
145
168
 
146
169
  if (!hasAccessToCoValue(update)) {
147
170
  if (this.value.type !== CoValueLoadingState.UNAUTHORIZED) {
148
- this.updateValue(
149
- new JazzError(this.id, CoValueLoadingState.UNAUTHORIZED, [
150
- {
151
- code: CoValueLoadingState.UNAUTHORIZED,
152
- message: `The current user (${this.node.getCurrentAgent().id}) is not authorized to access this value`,
153
- params: {
154
- id: this.id,
155
- },
156
- path: [],
171
+ const message = `Jazz Authorization Error: The current user (${this.node.getCurrentAgent().id}) is not authorized to access ${this.id}`;
172
+
173
+ const error = new JazzError(this.id, CoValueLoadingState.UNAUTHORIZED, [
174
+ {
175
+ code: CoValueLoadingState.UNAUTHORIZED,
176
+ message,
177
+ params: {
178
+ id: this.id,
157
179
  },
158
- ]),
159
- );
180
+ path: [],
181
+ },
182
+ ]);
183
+
184
+ this.updateValue(error);
160
185
  this.triggerUpdate();
161
186
  }
162
187
  return;
@@ -284,6 +309,66 @@ export class SubscriptionScope<D extends CoValue> {
284
309
 
285
310
  unloadedValue: NotLoaded<D> | undefined;
286
311
 
312
+ lastPromise:
313
+ | {
314
+ value: MaybeLoaded<D> | undefined;
315
+ promise: PromiseWithStatus<MaybeLoaded<D>>;
316
+ }
317
+ | undefined;
318
+
319
+ cachePromise(value: MaybeLoaded<D>, callback: () => PromiseWithStatus<D>) {
320
+ if (this.lastPromise?.value === value) {
321
+ return this.lastPromise.promise;
322
+ }
323
+
324
+ const promise = callback();
325
+ this.lastPromise = { value, promise };
326
+
327
+ return promise;
328
+ }
329
+
330
+ getPromise() {
331
+ const currentValue = this.getCurrentValue();
332
+
333
+ if (currentValue.$isLoaded) {
334
+ return resolvedPromise(currentValue);
335
+ }
336
+
337
+ if (currentValue.$jazz.loadingState !== CoValueLoadingState.LOADING) {
338
+ const error = this.getError();
339
+ return rejectedPromise(
340
+ new Error(error?.toString() ?? "Unknown error", {
341
+ cause: this.callerStack,
342
+ }),
343
+ );
344
+ }
345
+
346
+ // We need to cache rejected promises to make React Suspense happy
347
+ return this.cachePromise(currentValue, () => {
348
+ return new Promise<D>((resolve, reject) => {
349
+ const unsubscribe = this.subscribe(() => {
350
+ const currentValue = this.getCurrentValue();
351
+
352
+ if (currentValue.$jazz.loadingState === CoValueLoadingState.LOADING) {
353
+ return;
354
+ }
355
+
356
+ if (currentValue.$isLoaded) {
357
+ resolve(currentValue);
358
+ } else {
359
+ reject(
360
+ new Error(this.getError()?.toString() ?? "Unknown error", {
361
+ cause: this.callerStack,
362
+ }),
363
+ );
364
+ }
365
+
366
+ unsubscribe();
367
+ });
368
+ });
369
+ });
370
+ }
371
+
287
372
  private getUnloadedValue(reason: NotLoadedCoValueState): NotLoaded<D> {
288
373
  if (this.unloadedValue?.$jazz.loadingState === reason) {
289
374
  return this.unloadedValue;
@@ -296,6 +381,8 @@ export class SubscriptionScope<D extends CoValue> {
296
381
  return unloadedValue;
297
382
  }
298
383
 
384
+ lastErrorLogged: JazzError | undefined;
385
+
299
386
  getCurrentValue(): MaybeLoaded<D> {
300
387
  const rawValue = this.getCurrentRawValue();
301
388
 
@@ -304,6 +391,7 @@ export class SubscriptionScope<D extends CoValue> {
304
391
  rawValue === CoValueLoadingState.UNAVAILABLE ||
305
392
  rawValue === CoValueLoadingState.LOADING
306
393
  ) {
394
+ this.logError();
307
395
  return this.getUnloadedValue(rawValue);
308
396
  }
309
397
 
@@ -315,7 +403,6 @@ export class SubscriptionScope<D extends CoValue> {
315
403
  this.value.type === CoValueLoadingState.UNAUTHORIZED ||
316
404
  this.value.type === CoValueLoadingState.UNAVAILABLE
317
405
  ) {
318
- console.error(this.value.toString());
319
406
  return this.value.type;
320
407
  }
321
408
 
@@ -324,7 +411,6 @@ export class SubscriptionScope<D extends CoValue> {
324
411
  }
325
412
 
326
413
  if (this.errorFromChildren) {
327
- console.error(this.errorFromChildren.toString());
328
414
  return this.errorFromChildren.type;
329
415
  }
330
416
 
@@ -335,6 +421,73 @@ export class SubscriptionScope<D extends CoValue> {
335
421
  return CoValueLoadingState.LOADING;
336
422
  }
337
423
 
424
+ getCreationStackLines() {
425
+ const stack = this.callerStack?.stack;
426
+
427
+ if (!stack) {
428
+ return "";
429
+ }
430
+
431
+ const creationStackLines = stack.split("\n").slice(2, 15);
432
+ const creationAppFrame = creationStackLines.find(
433
+ (line) =>
434
+ !line.includes("node_modules") &&
435
+ !line.includes("useCoValueSubscription") &&
436
+ !line.includes("useCoState") &&
437
+ !line.includes("useAccount") &&
438
+ !line.includes("jazz-tools"),
439
+ );
440
+
441
+ let result = "\n\n";
442
+
443
+ if (creationAppFrame) {
444
+ (result += "Subscription created "), (result += creationAppFrame.trim());
445
+ }
446
+
447
+ result += "\nFull subscription creation stack:";
448
+ for (const line of creationStackLines.slice(0, 8)) {
449
+ result += "\n " + line.trim();
450
+ }
451
+
452
+ return result;
453
+ }
454
+
455
+ getError() {
456
+ if (
457
+ this.value.type === CoValueLoadingState.UNAUTHORIZED ||
458
+ this.value.type === CoValueLoadingState.UNAVAILABLE
459
+ ) {
460
+ return this.value;
461
+ }
462
+
463
+ if (this.errorFromChildren) {
464
+ return this.errorFromChildren;
465
+ }
466
+ }
467
+
468
+ logError() {
469
+ const error = this.getError();
470
+
471
+ if (!error || this.lastErrorLogged === error) {
472
+ return;
473
+ }
474
+
475
+ if (error.type === CoValueLoadingState.UNAVAILABLE && this.skipRetry) {
476
+ return;
477
+ }
478
+
479
+ this.lastErrorLogged = error;
480
+
481
+ if (isCustomErrorReportingEnabled()) {
482
+ captureError(new Error(error.toString(), { cause: this.callerStack }), {
483
+ getPrettyStackTrace: () => this.getCreationStackLines(),
484
+ jazzError: error,
485
+ });
486
+ } else {
487
+ console.error(`${error.toString()}${this.getCreationStackLines()}`);
488
+ }
489
+ }
490
+
338
491
  triggerUpdate() {
339
492
  if (!this.shouldSendUpdates()) return;
340
493
  if (!this.dirty) return;
@@ -354,16 +507,45 @@ export class SubscriptionScope<D extends CoValue> {
354
507
  }
355
508
 
356
509
  subscribers = new Set<(value: SubscriptionValue<D, any>) => void>();
510
+ subscriberChangeCallbacks = new Set<(count: number) => void>();
511
+
512
+ /**
513
+ * Subscribe to subscriber count changes
514
+ * Callback receives the total number of subscribers
515
+ * Returns an unsubscribe function
516
+ */
517
+ onSubscriberChange(callback: (count: number) => void): () => void {
518
+ this.subscriberChangeCallbacks.add(callback);
519
+
520
+ return () => {
521
+ this.subscriberChangeCallbacks.delete(callback);
522
+ };
523
+ }
524
+
525
+ private notifySubscriberChange() {
526
+ const count = this.subscribers.size;
527
+ this.subscriberChangeCallbacks.forEach((callback) => {
528
+ callback(count);
529
+ });
530
+ }
531
+
357
532
  subscribe(listener: (value: SubscriptionValue<D, any>) => void) {
358
533
  this.subscribers.add(listener);
534
+ this.notifySubscriberChange();
359
535
 
360
536
  return () => {
361
537
  this.subscribers.delete(listener);
538
+ this.notifySubscriberChange();
362
539
  };
363
540
  }
364
541
 
365
542
  setListener(listener: (value: SubscriptionValue<D, any>) => void) {
543
+ const hadListener = this.subscribers.has(listener);
366
544
  this.subscribers.add(listener);
545
+ // Only notify if this is a new listener (count actually changed)
546
+ if (!hadListener) {
547
+ this.notifySubscriberChange();
548
+ }
367
549
  this.triggerUpdate();
368
550
  }
369
551
 
@@ -577,7 +759,7 @@ export class SubscriptionScope<D extends CoValue> {
577
759
  new JazzError(undefined, CoValueLoadingState.UNAVAILABLE, [
578
760
  {
579
761
  code: "validationError",
580
- message: `The ref on position ${key} requested on ${stream.constructor.name} is missing`,
762
+ message: `Jazz Validation Error: The ref on position ${key} is missing`,
581
763
  params: {},
582
764
  path: [key],
583
765
  },
@@ -642,7 +824,7 @@ export class SubscriptionScope<D extends CoValue> {
642
824
  new JazzError(undefined, CoValueLoadingState.UNAVAILABLE, [
643
825
  {
644
826
  code: "validationError",
645
- message: `The ref ${key} requested on ${map.constructor.name} is missing`,
827
+ message: `Jazz Validation Error: The ref ${key} is required but missing`,
646
828
  params: {},
647
829
  path: [key],
648
830
  },
@@ -681,7 +863,7 @@ export class SubscriptionScope<D extends CoValue> {
681
863
  new JazzError(undefined, CoValueLoadingState.UNAVAILABLE, [
682
864
  {
683
865
  code: "validationError",
684
- message: `The ref on position ${key} requested on ${list.constructor.name} is missing`,
866
+ message: `Jazz Validation Error: The ref on position ${key} is required but missing`,
685
867
  params: {},
686
868
  path: [key],
687
869
  },
@@ -752,7 +934,14 @@ export class SubscriptionScope<D extends CoValue> {
752
934
  this.closed = true;
753
935
 
754
936
  this.subscription.unsubscribe();
937
+ const hadSubscribers = this.subscribers.size > 0;
755
938
  this.subscribers.clear();
939
+ // Notify callbacks that subscriber count is now 0 if there were subscribers before
940
+ if (hadSubscribers) {
941
+ this.notifySubscriberChange();
942
+ }
943
+ // Clear subscriber change callbacks to prevent memory leaks
944
+ this.subscriberChangeCallbacks.clear();
756
945
  this.childNodes.forEach((child) => child.destroy());
757
946
  }
758
947
  }
@@ -0,0 +1,67 @@
1
+ import type { JazzError } from "./JazzError";
2
+
3
+ /**
4
+ * A platform agnostic way to check if we're in development mode
5
+ *
6
+ * Works in Node.js and bundled code, falls back to false if process is not available
7
+ */
8
+ const isDev = (function () {
9
+ try {
10
+ return process.env.NODE_ENV === "development";
11
+ } catch {
12
+ return false;
13
+ }
14
+ })();
15
+
16
+ type CustomErrorReporterProps = {
17
+ getPrettyStackTrace: () => string;
18
+ jazzError: JazzError;
19
+ };
20
+
21
+ type CustomErrorReporter = (
22
+ error: Error,
23
+ props: CustomErrorReporterProps,
24
+ ) => void;
25
+
26
+ let customErrorReporter: CustomErrorReporter | undefined;
27
+ let captureErrorCause: boolean = isDev;
28
+
29
+ /**
30
+ * Turns on the additonal debug info coming from React hooks on the original subscription of the errors.
31
+ *
32
+ * Enabled by default in development mode.
33
+ */
34
+ export function enableCaptureErrorCause(capture: boolean) {
35
+ captureErrorCause = capture;
36
+ }
37
+
38
+ /**
39
+ * Set a custom error reporter to be used instead of the default console.error.
40
+ *
41
+ * Useful for sending errors to a logging service or silence some annoying errors in production.
42
+ */
43
+ export function setCustomErrorReporter(reporter?: CustomErrorReporter) {
44
+ customErrorReporter = reporter;
45
+ }
46
+
47
+ /**
48
+ * Check if we're in development mode.
49
+ * Stack traces are only captured in development to avoid overhead in production.
50
+ */
51
+ export function isCustomErrorReportingEnabled(): boolean {
52
+ return customErrorReporter !== undefined;
53
+ }
54
+
55
+ /**
56
+ * Capture a stack trace only in development mode.
57
+ * Returns undefined in production to avoid overhead.
58
+ */
59
+ export function captureStack() {
60
+ return captureErrorCause ? new Error() : undefined;
61
+ }
62
+
63
+ export function captureError(error: Error, props: CustomErrorReporterProps) {
64
+ if (customErrorReporter) {
65
+ customErrorReporter(error, props);
66
+ }
67
+ }