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.
- package/.turbo/turbo-build.log +65 -59
- package/CHANGELOG.md +34 -3
- package/dist/{chunk-CUS6O5NE.js → chunk-FFEEPZEG.js} +454 -122
- package/dist/chunk-FFEEPZEG.js.map +1 -0
- package/dist/expo/polyfills.js +22 -0
- package/dist/expo/polyfills.js.map +1 -0
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +59 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +133 -34
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/testUtils.d.ts +1 -0
- package/dist/react-core/tests/testUtils.d.ts.map +1 -1
- package/dist/react-core/tests/useSuspenseAccount.test.d.ts +2 -0
- package/dist/react-core/tests/useSuspenseAccount.test.d.ts.map +1 -0
- package/dist/react-core/tests/useSuspenseCoState.test.d.ts +2 -0
- package/dist/react-core/tests/useSuspenseCoState.test.d.ts.map +1 -0
- package/dist/react-core/use.d.ts +3 -0
- package/dist/react-core/use.d.ts.map +1 -0
- package/dist/react-native/index.d.ts +1 -1
- package/dist/react-native/index.d.ts.map +1 -1
- package/dist/react-native/index.js +717 -9
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native/polyfills.js +22 -0
- package/dist/react-native/polyfills.js.map +1 -0
- package/dist/react-native-core/crypto/RNCrypto.d.ts +2 -0
- package/dist/react-native-core/crypto/RNCrypto.d.ts.map +1 -0
- package/dist/react-native-core/crypto/RNCrypto.js +3 -0
- package/dist/react-native-core/crypto/RNCrypto.js.map +1 -0
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.d.ts.map +1 -1
- package/dist/react-native-core/index.js +5 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/platform.d.ts +2 -1
- package/dist/react-native-core/platform.d.ts.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/account.d.ts +3 -3
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/coFeed.d.ts +3 -3
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/coList.d.ts +4 -4
- package/dist/tools/coValues/coList.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts +7 -7
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/coValues/coPlainText.d.ts +2 -2
- package/dist/tools/coValues/coPlainText.d.ts.map +1 -1
- package/dist/tools/coValues/coVector.d.ts +2 -2
- package/dist/tools/coValues/coVector.d.ts.map +1 -1
- package/dist/tools/coValues/deepLoading.d.ts +24 -0
- package/dist/tools/coValues/deepLoading.d.ts.map +1 -1
- package/dist/tools/coValues/group.d.ts +2 -2
- package/dist/tools/coValues/group.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +7 -7
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/coValues/schemaUnion.d.ts +2 -2
- package/dist/tools/coValues/schemaUnion.d.ts.map +1 -1
- package/dist/tools/config.d.ts +3 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/exports.d.ts +2 -0
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +3 -0
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts +4 -4
- package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +4 -4
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts +4 -4
- package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/GroupSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/GroupSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +8 -22
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
- package/dist/tools/subscribe/JazzError.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionCache.d.ts +51 -0
- package/dist/tools/subscribe/SubscriptionCache.d.ts.map +1 -0
- package/dist/tools/subscribe/SubscriptionScope.d.ts +27 -2
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/subscribe/errorReporting.d.ts +31 -0
- package/dist/tools/subscribe/errorReporting.d.ts.map +1 -0
- package/dist/tools/subscribe/utils.d.ts +9 -1
- package/dist/tools/subscribe/utils.d.ts.map +1 -1
- package/dist/tools/testing.d.ts +2 -2
- package/dist/tools/testing.d.ts.map +1 -1
- package/dist/tools/tests/SubscriptionCache.test.d.ts +2 -0
- package/dist/tools/tests/SubscriptionCache.test.d.ts.map +1 -0
- package/dist/tools/tests/errorReporting.test.d.ts +2 -0
- package/dist/tools/tests/errorReporting.test.d.ts.map +1 -0
- package/package.json +22 -7
- package/src/react/hooks.tsx +2 -0
- package/src/react/index.ts +1 -14
- package/src/react-core/hooks.ts +181 -16
- package/src/react-core/tests/createCoValueSubscriptionContext.test.tsx +18 -8
- package/src/react-core/tests/testUtils.tsx +67 -5
- package/src/react-core/tests/useCoState.test.ts +3 -7
- package/src/react-core/tests/useSubscriptionSelector.test.ts +3 -7
- package/src/react-core/tests/useSuspenseAccount.test.tsx +343 -0
- package/src/react-core/tests/useSuspenseCoState.test.tsx +1182 -0
- package/src/react-core/use.ts +46 -0
- package/src/react-native/index.ts +1 -1
- package/src/react-native-core/crypto/RNCrypto.ts +1 -0
- package/src/react-native-core/hooks.tsx +2 -0
- package/src/react-native-core/index.ts +2 -0
- package/src/react-native-core/platform.ts +2 -1
- package/src/react-native-core/polyfills/index.js +28 -0
- package/src/tools/coValues/account.ts +3 -4
- package/src/tools/coValues/coFeed.ts +3 -2
- package/src/tools/coValues/coList.ts +4 -4
- package/src/tools/coValues/coMap.ts +4 -4
- package/src/tools/coValues/coPlainText.ts +2 -2
- package/src/tools/coValues/coVector.ts +2 -2
- package/src/tools/coValues/deepLoading.ts +31 -0
- package/src/tools/coValues/group.ts +2 -2
- package/src/tools/coValues/interfaces.ts +21 -26
- package/src/tools/coValues/schemaUnion.ts +2 -2
- package/src/tools/config.ts +9 -0
- package/src/tools/exports.ts +4 -0
- package/src/tools/implementation/ContextManager.ts +13 -0
- package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +2 -2
- package/src/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.ts +2 -2
- package/src/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.ts +2 -2
- package/src/tools/implementation/zodSchema/schemaTypes/CoListSchema.ts +4 -4
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +4 -4
- package/src/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.ts +4 -10
- package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +2 -2
- package/src/tools/implementation/zodSchema/schemaTypes/GroupSchema.ts +2 -2
- package/src/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.ts +2 -2
- package/src/tools/implementation/zodSchema/schemaTypes/RichTextSchema.ts +2 -2
- package/src/tools/subscribe/CoValueCoreSubscription.ts +71 -100
- package/src/tools/subscribe/JazzError.ts +9 -6
- package/src/tools/subscribe/SubscriptionCache.ts +272 -0
- package/src/tools/subscribe/SubscriptionScope.ts +218 -29
- package/src/tools/subscribe/errorReporting.ts +67 -0
- package/src/tools/subscribe/utils.ts +77 -0
- package/src/tools/testing.ts +0 -3
- package/src/tools/tests/CoValueCoreSubscription.test.ts +46 -12
- package/src/tools/tests/ContextManager.test.ts +85 -0
- package/src/tools/tests/SubscriptionCache.test.ts +237 -0
- package/src/tools/tests/coMap.test.ts +5 -7
- package/src/tools/tests/deepLoading.test.ts +47 -47
- package/src/tools/tests/errorReporting.test.ts +103 -0
- package/src/tools/tests/load.test.ts +21 -1
- package/src/tools/tests/request.test.ts +2 -1
- package/src/tools/tests/subscribe.test.ts +44 -0
- package/tsup.config.ts +17 -0
- 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
|
|
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 {
|
|
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.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
+
}
|