jazz-tools 0.13.17 → 0.13.19
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 +7 -7
- package/CHANGELOG.md +17 -0
- package/dist/{chunk-PYBQOYML.js → chunk-4HBHY4I7.js} +735 -595
- package/dist/chunk-4HBHY4I7.js.map +1 -0
- package/dist/coValues/account.d.ts +5 -4
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/coFeed.d.ts +1 -0
- package/dist/coValues/coFeed.d.ts.map +1 -1
- package/dist/coValues/coList.d.ts +1 -0
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coMap.d.ts +4 -2
- package/dist/coValues/coMap.d.ts.map +1 -1
- package/dist/coValues/coPlainText.d.ts +2 -2
- package/dist/coValues/coPlainText.d.ts.map +1 -1
- package/dist/coValues/deepLoading.d.ts +1 -9
- package/dist/coValues/deepLoading.d.ts.map +1 -1
- package/dist/coValues/extensions/imageDef.d.ts.map +1 -1
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/inbox.d.ts.map +1 -1
- package/dist/coValues/interfaces.d.ts +4 -1
- package/dist/coValues/interfaces.d.ts.map +1 -1
- package/dist/implementation/createContext.d.ts.map +1 -1
- package/dist/implementation/refs.d.ts +5 -10
- package/dist/implementation/refs.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/subscribe/CoValueCoreSubscription.d.ts +14 -0
- package/dist/subscribe/CoValueCoreSubscription.d.ts.map +1 -0
- package/dist/subscribe/JazzError.d.ts +16 -0
- package/dist/subscribe/JazzError.d.ts.map +1 -0
- package/dist/subscribe/SubscriptionScope.d.ts +43 -0
- package/dist/subscribe/SubscriptionScope.d.ts.map +1 -0
- package/dist/subscribe/index.d.ts +21 -0
- package/dist/subscribe/index.d.ts.map +1 -0
- package/dist/subscribe/types.d.ts +12 -0
- package/dist/subscribe/types.d.ts.map +1 -0
- package/dist/subscribe/utils.d.ts +10 -0
- package/dist/subscribe/utils.d.ts.map +1 -0
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/tests/coMap.record.test.d.ts +2 -0
- package/dist/tests/coMap.record.test.d.ts.map +1 -0
- package/dist/tests/utils.d.ts +2 -2
- package/dist/tests/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/coValues/account.ts +43 -31
- package/src/coValues/coFeed.ts +11 -7
- package/src/coValues/coList.ts +13 -17
- package/src/coValues/coMap.ts +72 -80
- package/src/coValues/coPlainText.ts +13 -2
- package/src/coValues/deepLoading.ts +4 -277
- package/src/coValues/extensions/imageDef.ts +1 -7
- package/src/coValues/group.ts +7 -6
- package/src/coValues/inbox.ts +4 -11
- package/src/coValues/interfaces.ts +54 -111
- package/src/implementation/createContext.ts +3 -4
- package/src/implementation/invites.ts +2 -2
- package/src/implementation/refs.ts +30 -121
- package/src/internal.ts +1 -2
- package/src/subscribe/CoValueCoreSubscription.ts +71 -0
- package/src/subscribe/JazzError.ts +48 -0
- package/src/subscribe/SubscriptionScope.ts +536 -0
- package/src/subscribe/index.ts +82 -0
- package/src/subscribe/types.ts +7 -0
- package/src/subscribe/utils.ts +36 -0
- package/src/testing.ts +1 -1
- package/src/tests/ContextManager.test.ts +13 -9
- package/src/tests/coFeed.test.ts +6 -6
- package/src/tests/coList.test.ts +304 -115
- package/src/tests/coMap.record.test.ts +325 -0
- package/src/tests/coMap.test.ts +718 -645
- package/src/tests/coPlainText.test.ts +2 -2
- package/src/tests/createContext.test.ts +8 -8
- package/src/tests/deepLoading.test.ts +8 -34
- package/src/tests/groupsAndAccounts.test.ts +6 -4
- package/src/tests/subscribe.test.ts +614 -42
- package/src/tests/utils.ts +8 -6
- package/dist/chunk-PYBQOYML.js.map +0 -1
- package/dist/implementation/subscriptionScope.d.ts +0 -34
- package/dist/implementation/subscriptionScope.d.ts.map +0 -1
- package/src/implementation/subscriptionScope.ts +0 -165
@@ -0,0 +1,536 @@
|
|
1
|
+
import type { LocalNode, RawCoValue } from "cojson";
|
2
|
+
import type { CoFeed, CoList, CoMap } from "../exports.js";
|
3
|
+
import {
|
4
|
+
type CoValue,
|
5
|
+
type ID,
|
6
|
+
type RefEncoded,
|
7
|
+
type RefsToResolve,
|
8
|
+
isRefEncoded,
|
9
|
+
} from "../internal.js";
|
10
|
+
import { CoValueCoreSubscription } from "./CoValueCoreSubscription.js";
|
11
|
+
import { JazzError, type JazzErrorIssue } from "./JazzError.js";
|
12
|
+
import type { SubscriptionValue, Unloaded } from "./types.js";
|
13
|
+
import { createCoValue, getOwnerFromRawValue } from "./utils.js";
|
14
|
+
|
15
|
+
export class SubscriptionScope<D extends CoValue> {
|
16
|
+
childNodes = new Map<string, SubscriptionScope<CoValue>>();
|
17
|
+
childValues: Map<string, SubscriptionValue<any, any> | Unloaded> = new Map<
|
18
|
+
string,
|
19
|
+
SubscriptionValue<D, any>
|
20
|
+
>();
|
21
|
+
value: SubscriptionValue<D, any> | Unloaded;
|
22
|
+
childErrors: Map<string, JazzError> = new Map();
|
23
|
+
validationErrors: Map<string, JazzError> = new Map();
|
24
|
+
errorFromChildren: JazzError | undefined;
|
25
|
+
subscription: CoValueCoreSubscription;
|
26
|
+
dirty = false;
|
27
|
+
resolve: RefsToResolve<any>;
|
28
|
+
idsSubscribed = new Set<string>();
|
29
|
+
autoloaded = new Set<string>();
|
30
|
+
autoloadedKeys = new Set<string>();
|
31
|
+
totalValidTransactions = 0;
|
32
|
+
|
33
|
+
silenceUpdates = false;
|
34
|
+
|
35
|
+
constructor(
|
36
|
+
public node: LocalNode,
|
37
|
+
resolve: RefsToResolve<D>,
|
38
|
+
public id: ID<D>,
|
39
|
+
public schema: RefEncoded<D>,
|
40
|
+
) {
|
41
|
+
this.resolve = resolve;
|
42
|
+
this.value = { type: "unloaded", id };
|
43
|
+
this.subscription = new CoValueCoreSubscription(node, id, (value) => {
|
44
|
+
this.handleUpdate(value);
|
45
|
+
});
|
46
|
+
}
|
47
|
+
|
48
|
+
updateValue(value: SubscriptionValue<D, any>) {
|
49
|
+
this.value = value;
|
50
|
+
|
51
|
+
// Flags that the value has changed and we need to trigger an update
|
52
|
+
this.dirty = true;
|
53
|
+
}
|
54
|
+
|
55
|
+
handleUpdate(update: RawCoValue | "unavailable") {
|
56
|
+
if (update === "unavailable") {
|
57
|
+
if (this.value.type === "unloaded") {
|
58
|
+
this.updateValue(
|
59
|
+
new JazzError(this.id, "unavailable", [
|
60
|
+
{
|
61
|
+
code: "unavailable",
|
62
|
+
message: "The value is unavailable",
|
63
|
+
params: {
|
64
|
+
id: this.id,
|
65
|
+
},
|
66
|
+
path: [],
|
67
|
+
},
|
68
|
+
]),
|
69
|
+
);
|
70
|
+
}
|
71
|
+
this.triggerUpdate();
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
|
75
|
+
const owner = getOwnerFromRawValue(update);
|
76
|
+
|
77
|
+
const ruleset = update.core.verified.header.ruleset;
|
78
|
+
|
79
|
+
// Groups and accounts are accessible by everyone, for the other coValues we use the role to check access
|
80
|
+
const hasAccess = ruleset.type === "group" || owner.myRole() !== undefined;
|
81
|
+
|
82
|
+
if (!hasAccess) {
|
83
|
+
if (this.value.type !== "unauthorized") {
|
84
|
+
this.updateValue(
|
85
|
+
new JazzError(this.id, "unauthorized", [
|
86
|
+
{
|
87
|
+
code: "unauthorized",
|
88
|
+
message:
|
89
|
+
"The current user is not authorized to access this value",
|
90
|
+
params: {
|
91
|
+
id: this.id,
|
92
|
+
},
|
93
|
+
path: [],
|
94
|
+
},
|
95
|
+
]),
|
96
|
+
);
|
97
|
+
this.triggerUpdate();
|
98
|
+
}
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
|
102
|
+
// When resolving a CoValue with available children, we want to trigger a single update
|
103
|
+
// after loading all the children, not one per children
|
104
|
+
this.silenceUpdates = true;
|
105
|
+
|
106
|
+
if (this.value.type !== "loaded") {
|
107
|
+
this.updateValue(createCoValue(this.schema, update, this));
|
108
|
+
this.loadChildren();
|
109
|
+
} else {
|
110
|
+
const hasChanged =
|
111
|
+
update.totalValidTransactions !== this.totalValidTransactions ||
|
112
|
+
// Checking the identity of the _raw value makes us cover the cases where the group
|
113
|
+
// has been updated and the coValues that don't update the totalValidTransactions value (e.g. FileStream)
|
114
|
+
this.value.value._raw !== update;
|
115
|
+
|
116
|
+
if (this.loadChildren()) {
|
117
|
+
this.updateValue(createCoValue(this.schema, update, this));
|
118
|
+
} else if (hasChanged) {
|
119
|
+
this.updateValue(createCoValue(this.schema, update, this));
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
this.totalValidTransactions = update.totalValidTransactions;
|
124
|
+
|
125
|
+
this.silenceUpdates = false;
|
126
|
+
this.triggerUpdate();
|
127
|
+
}
|
128
|
+
|
129
|
+
computeChildErrors() {
|
130
|
+
let issues: JazzErrorIssue[] = [];
|
131
|
+
let errorType: JazzError["type"] = "unavailable";
|
132
|
+
|
133
|
+
if (this.childErrors.size === 0 && this.validationErrors.size === 0) {
|
134
|
+
return undefined;
|
135
|
+
}
|
136
|
+
|
137
|
+
for (const [key, value] of this.childErrors.entries()) {
|
138
|
+
// We don't want to block updates if the error is on an autoloaded value
|
139
|
+
if (this.autoloaded.has(key)) {
|
140
|
+
continue;
|
141
|
+
}
|
142
|
+
|
143
|
+
errorType = value.type;
|
144
|
+
if (value.issues) {
|
145
|
+
issues.push(...value.issues);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
for (const value of this.validationErrors.values()) {
|
150
|
+
errorType = value.type;
|
151
|
+
if (value.issues) {
|
152
|
+
issues.push(...value.issues);
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
if (issues.length) {
|
157
|
+
return new JazzError(this.id, errorType, issues);
|
158
|
+
}
|
159
|
+
|
160
|
+
return undefined;
|
161
|
+
}
|
162
|
+
|
163
|
+
handleChildUpdate = (
|
164
|
+
id: string,
|
165
|
+
value: SubscriptionValue<any, any> | Unloaded,
|
166
|
+
key?: string,
|
167
|
+
) => {
|
168
|
+
if (value.type === "unloaded") {
|
169
|
+
return;
|
170
|
+
}
|
171
|
+
|
172
|
+
this.childValues.set(id, value);
|
173
|
+
|
174
|
+
if (value.type === "unavailable" || value.type === "unauthorized") {
|
175
|
+
this.childErrors.set(id, value.prependPath(key ?? id));
|
176
|
+
|
177
|
+
this.errorFromChildren = this.computeChildErrors();
|
178
|
+
} else if (this.errorFromChildren && this.childErrors.has(id)) {
|
179
|
+
this.childErrors.delete(id);
|
180
|
+
|
181
|
+
this.errorFromChildren = this.computeChildErrors();
|
182
|
+
}
|
183
|
+
|
184
|
+
if (this.shouldSendUpdates()) {
|
185
|
+
if (this.value.type === "loaded") {
|
186
|
+
// On child updates, we re-create the value instance to make the updates
|
187
|
+
// seamless-immutable and so be compatible with React and the React compiler
|
188
|
+
this.updateValue(
|
189
|
+
createCoValue(this.schema, this.value.value._raw, this),
|
190
|
+
);
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
this.triggerUpdate();
|
195
|
+
};
|
196
|
+
|
197
|
+
shouldSendUpdates() {
|
198
|
+
if (this.value.type === "unloaded") return false;
|
199
|
+
|
200
|
+
// If the value is in error, we send the update regardless of the children statuses
|
201
|
+
if (this.value.type !== "loaded") return true;
|
202
|
+
|
203
|
+
for (const value of this.childValues.values()) {
|
204
|
+
// We don't wait for autoloaded values to be loaded, in order to stream updates
|
205
|
+
// on autoloaded lists or records
|
206
|
+
if (value.type === "unloaded" && !this.autoloaded.has(value.id)) {
|
207
|
+
return false;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
return true;
|
212
|
+
}
|
213
|
+
|
214
|
+
triggerUpdate() {
|
215
|
+
if (!this.shouldSendUpdates()) return;
|
216
|
+
if (!this.dirty) return;
|
217
|
+
if (this.subscribers.size === 0) return;
|
218
|
+
if (this.silenceUpdates) return;
|
219
|
+
|
220
|
+
const error = this.errorFromChildren;
|
221
|
+
const value = this.value;
|
222
|
+
|
223
|
+
if (error) {
|
224
|
+
this.subscribers.forEach((listener) => listener(error));
|
225
|
+
} else if (value.type !== "unloaded") {
|
226
|
+
this.subscribers.forEach((listener) => listener(value));
|
227
|
+
}
|
228
|
+
|
229
|
+
this.dirty = false;
|
230
|
+
}
|
231
|
+
|
232
|
+
subscribers = new Set<(value: SubscriptionValue<D, any>) => void>();
|
233
|
+
subscribe(listener: (value: SubscriptionValue<D, any>) => void) {
|
234
|
+
this.subscribers.add(listener);
|
235
|
+
|
236
|
+
return () => {
|
237
|
+
this.subscribers.delete(listener);
|
238
|
+
};
|
239
|
+
}
|
240
|
+
|
241
|
+
setListener(listener: (value: SubscriptionValue<D, any>) => void) {
|
242
|
+
this.subscribers.add(listener);
|
243
|
+
this.triggerUpdate();
|
244
|
+
}
|
245
|
+
|
246
|
+
subscribeToKey(key: string) {
|
247
|
+
if (this.resolve === true || !this.resolve) {
|
248
|
+
this.resolve = {};
|
249
|
+
}
|
250
|
+
|
251
|
+
if (this.resolve.$each || key in this.resolve) {
|
252
|
+
return;
|
253
|
+
}
|
254
|
+
|
255
|
+
// Adding the key to the resolve object to resolve the key when calling loadChildren
|
256
|
+
this.resolve[key as keyof typeof this.resolve] = true;
|
257
|
+
// Track the keys that are autoloaded to flag any id on that key as autoloaded
|
258
|
+
this.autoloadedKeys.add(key);
|
259
|
+
|
260
|
+
if (this.value.type !== "loaded") {
|
261
|
+
return;
|
262
|
+
}
|
263
|
+
|
264
|
+
const value = this.value.value;
|
265
|
+
|
266
|
+
// We don't want to trigger an update when autoloading available children
|
267
|
+
// because on userland it looks like nothing has changed since the value
|
268
|
+
// is available on the first access
|
269
|
+
// This helps alot with correctness when triggering the autoloading while rendering components (on React and Svelte)
|
270
|
+
this.silenceUpdates = true;
|
271
|
+
|
272
|
+
if (value._type === "CoMap" || value._type === "Account") {
|
273
|
+
const map = value as CoMap;
|
274
|
+
|
275
|
+
this.loadCoMapKey(map, key, true);
|
276
|
+
} else if (value._type === "CoList") {
|
277
|
+
const list = value as CoList;
|
278
|
+
|
279
|
+
this.loadCoListKey(list, key, true);
|
280
|
+
}
|
281
|
+
|
282
|
+
this.silenceUpdates = false;
|
283
|
+
}
|
284
|
+
|
285
|
+
subscribeToId(id: string, descriptor: RefEncoded<any>) {
|
286
|
+
if (this.idsSubscribed.has(id) || this.childValues.has(id)) {
|
287
|
+
return;
|
288
|
+
}
|
289
|
+
|
290
|
+
this.idsSubscribed.add(id);
|
291
|
+
this.autoloaded.add(id);
|
292
|
+
|
293
|
+
// We don't want to trigger an update when autoloading available children
|
294
|
+
// because on userland it looks like nothing has changed since the value
|
295
|
+
// is available on the first access
|
296
|
+
// This helps alot with correctness when triggering the autoloading while rendering components (on React and Svelte)
|
297
|
+
this.silenceUpdates = true;
|
298
|
+
|
299
|
+
this.childValues.set(id, { type: "unloaded", id });
|
300
|
+
const child = new SubscriptionScope(
|
301
|
+
this.node,
|
302
|
+
true,
|
303
|
+
id as ID<any>,
|
304
|
+
descriptor,
|
305
|
+
);
|
306
|
+
this.childNodes.set(id, child);
|
307
|
+
child.setListener((value) => this.handleChildUpdate(id, value));
|
308
|
+
|
309
|
+
this.silenceUpdates = false;
|
310
|
+
}
|
311
|
+
|
312
|
+
loadChildren() {
|
313
|
+
const { resolve } = this;
|
314
|
+
|
315
|
+
if (this.value.type !== "loaded") {
|
316
|
+
return false;
|
317
|
+
}
|
318
|
+
|
319
|
+
const value = this.value.value;
|
320
|
+
|
321
|
+
const depth =
|
322
|
+
typeof resolve !== "object" || resolve === null ? {} : (resolve as any);
|
323
|
+
|
324
|
+
let hasChanged = false;
|
325
|
+
|
326
|
+
const idsToLoad = new Set<string>(this.idsSubscribed);
|
327
|
+
|
328
|
+
const coValueType = value._type;
|
329
|
+
|
330
|
+
if (Object.keys(depth).length > 0) {
|
331
|
+
if (coValueType === "CoMap" || coValueType === "Account") {
|
332
|
+
const map = value as CoMap;
|
333
|
+
const keys = "$each" in depth ? map._raw.keys() : Object.keys(depth);
|
334
|
+
|
335
|
+
for (const key of keys) {
|
336
|
+
const id = this.loadCoMapKey(map, key, depth[key] ?? depth.$each);
|
337
|
+
|
338
|
+
if (id) {
|
339
|
+
idsToLoad.add(id);
|
340
|
+
}
|
341
|
+
}
|
342
|
+
} else if (value._type === "CoList") {
|
343
|
+
const list = value as CoList;
|
344
|
+
|
345
|
+
const descriptor = list.getItemsDescriptor();
|
346
|
+
|
347
|
+
if (descriptor && isRefEncoded(descriptor)) {
|
348
|
+
list._raw.processNewTransactions();
|
349
|
+
const entries = list._raw.entries();
|
350
|
+
const keys =
|
351
|
+
"$each" in depth ? Object.keys(entries) : Object.keys(depth);
|
352
|
+
|
353
|
+
for (const key of keys) {
|
354
|
+
const id = this.loadCoListKey(list, key, depth[key] ?? depth.$each);
|
355
|
+
|
356
|
+
if (id) {
|
357
|
+
idsToLoad.add(id);
|
358
|
+
}
|
359
|
+
}
|
360
|
+
}
|
361
|
+
} else if (value._type === "CoStream") {
|
362
|
+
const stream = value as CoFeed;
|
363
|
+
const descriptor = stream.getItemsDescriptor();
|
364
|
+
|
365
|
+
if (descriptor && isRefEncoded(descriptor)) {
|
366
|
+
for (const session of stream._raw.sessions()) {
|
367
|
+
const values = stream._raw.items[session] ?? [];
|
368
|
+
|
369
|
+
for (const [i, item] of values.entries()) {
|
370
|
+
const key = `${session}/${i}`;
|
371
|
+
|
372
|
+
if (!depth.$each && !depth[key]) {
|
373
|
+
continue;
|
374
|
+
}
|
375
|
+
|
376
|
+
const id = item.value as string | undefined;
|
377
|
+
|
378
|
+
if (id) {
|
379
|
+
idsToLoad.add(id);
|
380
|
+
this.loadChildNode(id, depth[key] ?? depth.$each, descriptor);
|
381
|
+
this.validationErrors.delete(key);
|
382
|
+
} else if (!descriptor.optional) {
|
383
|
+
this.validationErrors.set(
|
384
|
+
key,
|
385
|
+
new JazzError(undefined, "unavailable", [
|
386
|
+
{
|
387
|
+
code: "validationError",
|
388
|
+
message: `The ref on position ${key} requested on ${stream.constructor.name} is missing`,
|
389
|
+
params: {},
|
390
|
+
path: [key],
|
391
|
+
},
|
392
|
+
]),
|
393
|
+
);
|
394
|
+
}
|
395
|
+
}
|
396
|
+
}
|
397
|
+
}
|
398
|
+
}
|
399
|
+
}
|
400
|
+
|
401
|
+
this.errorFromChildren = this.computeChildErrors();
|
402
|
+
|
403
|
+
// Collect all the deleted ids
|
404
|
+
for (const id of this.childNodes.keys()) {
|
405
|
+
if (!idsToLoad.has(id)) {
|
406
|
+
hasChanged = true;
|
407
|
+
const childNode = this.childNodes.get(id);
|
408
|
+
|
409
|
+
if (childNode) {
|
410
|
+
childNode.destroy();
|
411
|
+
}
|
412
|
+
|
413
|
+
this.childNodes.delete(id);
|
414
|
+
this.childValues.delete(id);
|
415
|
+
}
|
416
|
+
}
|
417
|
+
|
418
|
+
return hasChanged;
|
419
|
+
}
|
420
|
+
|
421
|
+
loadCoMapKey(map: CoMap, key: string, depth: Record<string, any> | true) {
|
422
|
+
const id = map._raw.get(key) as string | undefined;
|
423
|
+
const descriptor = map.getDescriptor(key);
|
424
|
+
|
425
|
+
if (!descriptor) {
|
426
|
+
this.childErrors.set(
|
427
|
+
key,
|
428
|
+
new JazzError(undefined, "unavailable", [
|
429
|
+
{
|
430
|
+
code: "validationError",
|
431
|
+
message: `The ref ${key} requested on ${map.constructor.name} is not defined in the schema`,
|
432
|
+
params: {},
|
433
|
+
path: [key],
|
434
|
+
},
|
435
|
+
]),
|
436
|
+
);
|
437
|
+
return undefined;
|
438
|
+
}
|
439
|
+
|
440
|
+
if (isRefEncoded(descriptor)) {
|
441
|
+
if (id) {
|
442
|
+
this.loadChildNode(id, depth, descriptor, key);
|
443
|
+
this.validationErrors.delete(key);
|
444
|
+
|
445
|
+
return id;
|
446
|
+
} else if (!descriptor.optional) {
|
447
|
+
this.validationErrors.set(
|
448
|
+
key,
|
449
|
+
new JazzError(undefined, "unavailable", [
|
450
|
+
{
|
451
|
+
code: "validationError",
|
452
|
+
message: `The ref ${key} requested on ${map.constructor.name} is missing`,
|
453
|
+
params: {},
|
454
|
+
path: [key],
|
455
|
+
},
|
456
|
+
]),
|
457
|
+
);
|
458
|
+
}
|
459
|
+
}
|
460
|
+
|
461
|
+
return undefined;
|
462
|
+
}
|
463
|
+
|
464
|
+
loadCoListKey(list: CoList, key: string, depth: Record<string, any> | true) {
|
465
|
+
const descriptor = list.getItemsDescriptor();
|
466
|
+
|
467
|
+
if (!descriptor || !isRefEncoded(descriptor)) {
|
468
|
+
return undefined;
|
469
|
+
}
|
470
|
+
|
471
|
+
const entries = list._raw.entries();
|
472
|
+
const entry = entries[Number(key)];
|
473
|
+
|
474
|
+
if (!entry) {
|
475
|
+
return undefined;
|
476
|
+
}
|
477
|
+
|
478
|
+
const id = entry.value as string | undefined;
|
479
|
+
|
480
|
+
if (id) {
|
481
|
+
this.loadChildNode(id, depth, descriptor, key);
|
482
|
+
this.validationErrors.delete(key);
|
483
|
+
|
484
|
+
return id;
|
485
|
+
} else if (!descriptor.optional) {
|
486
|
+
this.validationErrors.set(
|
487
|
+
key,
|
488
|
+
new JazzError(undefined, "unavailable", [
|
489
|
+
{
|
490
|
+
code: "validationError",
|
491
|
+
message: `The ref on position ${key} requested on ${list.constructor.name} is missing`,
|
492
|
+
params: {},
|
493
|
+
path: [key],
|
494
|
+
},
|
495
|
+
]),
|
496
|
+
);
|
497
|
+
}
|
498
|
+
|
499
|
+
return undefined;
|
500
|
+
}
|
501
|
+
|
502
|
+
loadChildNode(
|
503
|
+
id: string,
|
504
|
+
query: RefsToResolve<any>,
|
505
|
+
descriptor: RefEncoded<any>,
|
506
|
+
key?: string,
|
507
|
+
) {
|
508
|
+
if (this.childValues.has(id)) {
|
509
|
+
return;
|
510
|
+
}
|
511
|
+
|
512
|
+
if (key && this.autoloadedKeys.has(key)) {
|
513
|
+
this.autoloaded.add(id);
|
514
|
+
}
|
515
|
+
|
516
|
+
// Cloning the resolve objects to avoid mutating the original object when tracking autoloaded values
|
517
|
+
const resolve =
|
518
|
+
typeof query === "object" && query !== null ? { ...query } : query;
|
519
|
+
|
520
|
+
this.childValues.set(id, { type: "unloaded", id });
|
521
|
+
const child = new SubscriptionScope(
|
522
|
+
this.node,
|
523
|
+
resolve,
|
524
|
+
id as ID<any>,
|
525
|
+
descriptor,
|
526
|
+
);
|
527
|
+
this.childNodes.set(id, child);
|
528
|
+
child.setListener((value) => this.handleChildUpdate(id, value, key));
|
529
|
+
}
|
530
|
+
|
531
|
+
destroy() {
|
532
|
+
this.subscription.unsubscribe();
|
533
|
+
this.subscribers.clear();
|
534
|
+
this.childNodes.forEach((child) => child.destroy());
|
535
|
+
}
|
536
|
+
}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import type { CoValue, CoValueClass, RefEncoded } from "../internal.js";
|
2
|
+
import { SubscriptionScope } from "./SubscriptionScope.js";
|
3
|
+
|
4
|
+
export function getSubscriptionScope<D extends CoValue>(value: D) {
|
5
|
+
const subscriptionScope = value._subscriptionScope;
|
6
|
+
|
7
|
+
if (subscriptionScope) {
|
8
|
+
return subscriptionScope;
|
9
|
+
}
|
10
|
+
|
11
|
+
const node = value._raw.core.node;
|
12
|
+
const resolve = true;
|
13
|
+
const id = value.id;
|
14
|
+
|
15
|
+
const newSubscriptionScope = new SubscriptionScope(node, resolve, id, {
|
16
|
+
ref: value.constructor as CoValueClass<D>,
|
17
|
+
optional: false,
|
18
|
+
});
|
19
|
+
|
20
|
+
Object.defineProperty(value, "_subscriptionScope", {
|
21
|
+
value: subscriptionScope,
|
22
|
+
writable: false,
|
23
|
+
enumerable: false,
|
24
|
+
configurable: false,
|
25
|
+
});
|
26
|
+
|
27
|
+
return newSubscriptionScope;
|
28
|
+
}
|
29
|
+
|
30
|
+
/** Autoload internals */
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Given a coValue, access a child coValue by key
|
34
|
+
*
|
35
|
+
* By subscribing to a given key, the subscription will automatically react to the id changes
|
36
|
+
* on that key (e.g. deleting the key value will result on unsubscribing from the id)
|
37
|
+
*/
|
38
|
+
export function accessChildByKey<D extends CoValue>(
|
39
|
+
parent: D,
|
40
|
+
childId: string,
|
41
|
+
key: string,
|
42
|
+
) {
|
43
|
+
const subscriptionScope = getSubscriptionScope(parent);
|
44
|
+
|
45
|
+
if (!subscriptionScope.childValues.has(childId)) {
|
46
|
+
subscriptionScope.subscribeToKey(key);
|
47
|
+
}
|
48
|
+
|
49
|
+
const value = subscriptionScope.childValues.get(childId);
|
50
|
+
|
51
|
+
if (value?.type === "loaded") {
|
52
|
+
return value.value;
|
53
|
+
} else {
|
54
|
+
return null;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Given a coValue, access a child coValue by id
|
60
|
+
*
|
61
|
+
* By subscribing to a given id, the subscription becomes permanent and will unsubscribe
|
62
|
+
* only when the root subscription scope is destroyed.
|
63
|
+
*
|
64
|
+
* Used for refs that never change (e.g. CoFeed entries, CoMap edits)
|
65
|
+
*/
|
66
|
+
export function accessChildById<D extends CoValue>(
|
67
|
+
parent: D,
|
68
|
+
childId: string,
|
69
|
+
schema: RefEncoded<CoValue>,
|
70
|
+
) {
|
71
|
+
const subscriptionScope = getSubscriptionScope(parent);
|
72
|
+
|
73
|
+
subscriptionScope.subscribeToId(childId, schema);
|
74
|
+
|
75
|
+
const value = subscriptionScope.childValues.get(childId);
|
76
|
+
|
77
|
+
if (value?.type === "loaded") {
|
78
|
+
return value.value;
|
79
|
+
} else {
|
80
|
+
return null;
|
81
|
+
}
|
82
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
import type { CoValue, RefsToResolve, Resolved } from "../internal.js";
|
2
|
+
import type { JazzError } from "./JazzError.js";
|
3
|
+
|
4
|
+
export type SubscriptionValue<D extends CoValue, R extends RefsToResolve<D>> =
|
5
|
+
| { type: "loaded"; value: Resolved<D, R>; id: string }
|
6
|
+
| JazzError;
|
7
|
+
export type Unloaded = { type: "unloaded"; id: string };
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { RawAccount, RawCoValue, RawGroup } from "cojson";
|
2
|
+
import { RegisteredSchemas } from "../coValues/registeredSchemas.js";
|
3
|
+
import { CoValue, RefEncoded, instantiateRefEncoded } from "../internal.js";
|
4
|
+
import { coValuesCache } from "../lib/cache.js";
|
5
|
+
import { SubscriptionScope } from "./SubscriptionScope.js";
|
6
|
+
|
7
|
+
export function getOwnerFromRawValue(raw: RawCoValue) {
|
8
|
+
let owner = raw instanceof RawGroup ? raw : raw.group;
|
9
|
+
|
10
|
+
return coValuesCache.get(owner as any, () =>
|
11
|
+
owner instanceof RawAccount
|
12
|
+
? RegisteredSchemas["Account"].fromRaw(owner)
|
13
|
+
: RegisteredSchemas["Group"].fromRaw(owner as any),
|
14
|
+
);
|
15
|
+
}
|
16
|
+
|
17
|
+
export function createCoValue<D extends CoValue>(
|
18
|
+
ref: RefEncoded<D>,
|
19
|
+
raw: RawCoValue,
|
20
|
+
subscriptionScope: SubscriptionScope<D>,
|
21
|
+
) {
|
22
|
+
const freshValueInstance = instantiateRefEncoded(ref, raw);
|
23
|
+
|
24
|
+
Object.defineProperty(freshValueInstance, "_subscriptionScope", {
|
25
|
+
value: subscriptionScope,
|
26
|
+
writable: false,
|
27
|
+
enumerable: false,
|
28
|
+
configurable: false,
|
29
|
+
});
|
30
|
+
|
31
|
+
return {
|
32
|
+
type: "loaded" as const,
|
33
|
+
value: freshValueInstance,
|
34
|
+
id: subscriptionScope.id,
|
35
|
+
};
|
36
|
+
}
|
package/src/testing.ts
CHANGED
@@ -185,7 +185,7 @@ export class TestJazzContextManager<
|
|
185
185
|
|
186
186
|
const credentials = {
|
187
187
|
accountID: account.id,
|
188
|
-
accountSecret: node.
|
188
|
+
accountSecret: node.getCurrentAgent().agentSecret,
|
189
189
|
secretSeed: SecretSeedMap.get(account.id),
|
190
190
|
provider,
|
191
191
|
} satisfies AuthCredentials;
|