jazz-tools 0.18.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +43 -41
- package/CHANGELOG.md +20 -0
- package/dist/{chunk-IERUTUXB.js → chunk-LHQQZH7I.js} +121 -36
- package/dist/chunk-LHQQZH7I.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/media/{chunk-KR2V6X2N.js → chunk-W3S526L3.js} +96 -93
- package/dist/media/chunk-W3S526L3.js.map +1 -0
- package/dist/media/create-image/browser.d.ts +43 -0
- package/dist/media/create-image/browser.d.ts.map +1 -0
- package/dist/media/create-image/react-native.d.ts +37 -0
- package/dist/media/create-image/react-native.d.ts.map +1 -0
- package/dist/media/create-image/server.d.ts +34 -0
- package/dist/media/create-image/server.d.ts.map +1 -0
- package/dist/media/create-image/server.test.d.ts +2 -0
- package/dist/media/create-image/server.test.d.ts.map +1 -0
- package/dist/media/{create-image.d.ts → create-image-factory.d.ts} +8 -7
- package/dist/media/create-image-factory.d.ts.map +1 -0
- package/dist/media/create-image-factory.test.d.ts +2 -0
- package/dist/media/create-image-factory.test.d.ts.map +1 -0
- package/dist/media/exports.d.ts +3 -0
- package/dist/media/exports.d.ts.map +1 -0
- package/dist/media/index.browser.d.ts +2 -14
- package/dist/media/index.browser.d.ts.map +1 -1
- package/dist/media/index.browser.js +11 -20
- package/dist/media/index.browser.js.map +1 -1
- package/dist/media/index.d.ts +12 -4
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/index.native.d.ts +2 -16
- package/dist/media/index.native.d.ts.map +1 -1
- package/dist/media/index.native.js +23 -42
- package/dist/media/index.native.js.map +1 -1
- package/dist/media/index.server.d.ts +3 -0
- package/dist/media/index.server.d.ts.map +1 -0
- package/dist/media/index.server.js +103 -0
- package/dist/media/index.server.js.map +1 -0
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/index.js +120 -35
- package/dist/react-core/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/coFeed.d.ts +12 -0
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +19 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +61 -11
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts +2 -0
- package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts.map +1 -0
- package/dist/tools/testing.d.ts.map +1 -1
- package/package.json +27 -11
- package/src/media/create-image/browser.ts +161 -0
- package/src/media/create-image/react-native.ts +158 -0
- package/src/media/create-image/server.test.ts +74 -0
- package/src/media/create-image/server.ts +181 -0
- package/src/media/{create-image.test.ts → create-image-factory.test.ts} +1 -1
- package/src/media/{create-image.ts → create-image-factory.ts} +22 -12
- package/src/media/exports.ts +2 -0
- package/src/media/index.browser.ts +2 -150
- package/src/media/index.native.ts +2 -166
- package/src/media/index.server.ts +2 -0
- package/src/media/index.ts +16 -8
- package/src/tools/coValues/account.ts +3 -1
- package/src/tools/coValues/coFeed.ts +5 -0
- package/src/tools/coValues/coMap.ts +3 -1
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +19 -0
- package/src/tools/subscribe/CoValueCoreSubscription.test.ts +1000 -0
- package/src/tools/subscribe/CoValueCoreSubscription.ts +179 -43
- package/src/tools/tests/account.test.ts +12 -0
- package/src/tools/tests/coFeed.test.ts +25 -0
- package/src/tools/tests/coList.test.ts +20 -0
- package/src/tools/tests/coMap.record.test.ts +1 -0
- package/src/tools/tests/coMap.test.ts +12 -2
- package/tsup.config.ts +1 -0
- package/dist/chunk-IERUTUXB.js.map +0 -1
- package/dist/media/chunk-KR2V6X2N.js.map +0 -1
- package/dist/media/create-image.d.ts.map +0 -1
- package/dist/media/create-image.test.d.ts +0 -2
- package/dist/media/create-image.test.d.ts.map +0 -1
@@ -1,76 +1,212 @@
|
|
1
|
-
import { CoValueCore, LocalNode,
|
1
|
+
import { CoValueCore, LocalNode, RawCoID, RawCoValue } from "cojson";
|
2
|
+
import type { Account, Group } from "../internal.js";
|
2
3
|
|
4
|
+
export type BranchDefinition = { name: string; owner?: Group | Account };
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Manages subscriptions to CoValue cores, handling both direct subscriptions
|
8
|
+
* and branch-based subscriptions with automatic loading and error handling.
|
9
|
+
*
|
10
|
+
* It tries to resolve the value immediately if already available in memory.
|
11
|
+
*/
|
3
12
|
export class CoValueCoreSubscription {
|
4
|
-
_unsubscribe: () => void = () => {};
|
5
|
-
unsubscribed = false;
|
13
|
+
private _unsubscribe: () => void = () => {};
|
14
|
+
private unsubscribed = false;
|
6
15
|
|
7
|
-
|
16
|
+
private branchOwnerId?: RawCoID;
|
17
|
+
private branchName?: string;
|
18
|
+
private source: CoValueCore;
|
19
|
+
private localNode: LocalNode;
|
20
|
+
private listener: (value: RawCoValue | "unavailable") => void;
|
21
|
+
private skipRetry?: boolean;
|
8
22
|
|
9
23
|
constructor(
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
24
|
+
localNode: LocalNode,
|
25
|
+
id: string,
|
26
|
+
listener: (value: RawCoValue | "unavailable") => void,
|
27
|
+
skipRetry?: boolean,
|
28
|
+
branch?: BranchDefinition,
|
14
29
|
) {
|
15
|
-
|
30
|
+
this.localNode = localNode;
|
31
|
+
this.listener = listener;
|
32
|
+
this.skipRetry = skipRetry;
|
33
|
+
this.branchName = branch?.name;
|
34
|
+
this.branchOwnerId = branch?.owner?.$jazz.raw.id;
|
35
|
+
this.source = localNode.getCoValue(id as RawCoID);
|
36
|
+
|
37
|
+
this.initializeSubscription();
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Main entry point for subscription initialization.
|
42
|
+
* Determines the subscription strategy based on current availability and branch requirements.
|
43
|
+
*/
|
44
|
+
private initializeSubscription(): void {
|
45
|
+
const source = this.source;
|
46
|
+
|
47
|
+
// If the CoValue is already available, handle it immediately
|
48
|
+
if (source.isAvailable()) {
|
49
|
+
this.handleAvailableSource(source);
|
50
|
+
return;
|
51
|
+
}
|
52
|
+
|
53
|
+
// If a specific branch is requested while the source is not available, attempt to checkout that branch
|
54
|
+
if (this.branchName) {
|
55
|
+
this.handleBranchCheckout();
|
56
|
+
return;
|
57
|
+
}
|
16
58
|
|
17
|
-
|
18
|
-
|
59
|
+
// If we don't have a branch requested, load the CoValue
|
60
|
+
this.loadCoValue();
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Handles the case where the CoValue source is immediately available.
|
65
|
+
* Either subscribes directly or attempts to get the requested branch.
|
66
|
+
*/
|
67
|
+
private handleAvailableSource(source: CoValueCore): void {
|
68
|
+
if (!this.branchName) {
|
69
|
+
this.subscribe(source.getCurrentContent());
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
|
73
|
+
// Try to get the specific branch from the available source
|
74
|
+
const branch = source.getBranch(this.branchName, this.branchOwnerId);
|
75
|
+
|
76
|
+
if (branch.isAvailable()) {
|
77
|
+
// Branch is available, subscribe to it
|
78
|
+
this.subscribe(branch.getCurrentContent());
|
79
|
+
return;
|
19
80
|
} else {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
81
|
+
// Branch not available, fall through to checkout logic
|
82
|
+
this.handleBranchCheckout();
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Attempts to checkout a specific branch of the CoValue.
|
88
|
+
* This is called when the source isn't available but a branch is requested.
|
89
|
+
*/
|
90
|
+
private handleBranchCheckout(): void {
|
91
|
+
this.localNode
|
92
|
+
.checkoutBranch(this.source.id, this.branchName!, this.branchOwnerId)
|
93
|
+
.then((value) => {
|
94
|
+
if (this.unsubscribed) return;
|
95
|
+
|
96
|
+
if (value !== "unavailable") {
|
97
|
+
// Branch checkout successful, subscribe to it
|
98
|
+
this.subscribe(value);
|
99
|
+
} else {
|
100
|
+
// Branch checkout failed, handle the error
|
101
|
+
this.handleUnavailableBranch();
|
102
|
+
}
|
103
|
+
})
|
104
|
+
.catch((error) => {
|
105
|
+
// Handle unexpected errors during branch checkout
|
106
|
+
console.error(error);
|
107
|
+
this.emit("unavailable");
|
108
|
+
});
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Handles the case where a branch checkout fails.
|
113
|
+
* Determines whether to retry or report unavailability.
|
114
|
+
*/
|
115
|
+
private handleUnavailableBranch(): void {
|
116
|
+
const source = this.source;
|
117
|
+
if (source.isAvailable()) {
|
118
|
+
// This should be impossible - if source is available we can create the branch and it should be available
|
119
|
+
throw new Error("Branch is unavailable");
|
36
120
|
}
|
121
|
+
|
122
|
+
// Source isn't available either, subscribe to state changes and report unavailability
|
123
|
+
this.subscribeToUnavailableSource();
|
124
|
+
this.emit("unavailable");
|
37
125
|
}
|
38
126
|
|
39
|
-
|
40
|
-
|
127
|
+
/**
|
128
|
+
* Loads the CoValue core from the network/storage.
|
129
|
+
* This is the fallback strategy when immediate availability fails.
|
130
|
+
*/
|
131
|
+
private loadCoValue(): void {
|
132
|
+
this.localNode
|
133
|
+
.loadCoValueCore(this.source.id, undefined, this.skipRetry)
|
134
|
+
.then((value) => {
|
135
|
+
if (this.unsubscribed) return;
|
136
|
+
|
137
|
+
if (value.isAvailable()) {
|
138
|
+
// Loading successful, subscribe to the loaded value
|
139
|
+
this.subscribe(value.getCurrentContent());
|
140
|
+
} else {
|
141
|
+
// Loading failed, subscribe to state changes and report unavailability
|
142
|
+
this.subscribeToUnavailableSource();
|
143
|
+
this.emit("unavailable");
|
144
|
+
}
|
145
|
+
})
|
146
|
+
.catch((error) => {
|
147
|
+
// Handle unexpected errors during loading
|
148
|
+
console.error(error);
|
149
|
+
this.emit("unavailable");
|
150
|
+
});
|
151
|
+
}
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Subscribes to state changes of an unavailable CoValue source.
|
155
|
+
* This allows the subscription to become active when the source becomes available after a first loading attempt.
|
156
|
+
*/
|
157
|
+
private subscribeToUnavailableSource(): void {
|
158
|
+
const source = this.source;
|
159
|
+
|
41
160
|
const handleStateChange = (
|
42
|
-
|
161
|
+
_: CoValueCore,
|
43
162
|
unsubFromStateChange: () => void,
|
44
163
|
) => {
|
45
|
-
|
46
|
-
|
164
|
+
// We are waiting for the source to become available, it's ok to wait indefinitiely
|
165
|
+
// until either this becomes available or we unsubscribe, because we have already
|
166
|
+
// emitted an "unavailable" event.
|
167
|
+
if (!source.isAvailable()) {
|
47
168
|
return;
|
48
169
|
}
|
49
170
|
|
50
|
-
|
51
|
-
|
52
|
-
|
171
|
+
unsubFromStateChange();
|
172
|
+
|
173
|
+
if (this.branchName) {
|
174
|
+
// Branch was requested, attempt checkout again
|
175
|
+
this.handleBranchCheckout();
|
176
|
+
} else {
|
177
|
+
// No branch requested, subscribe directly and cleanup state subscription
|
178
|
+
this.subscribe(source.getCurrentContent());
|
53
179
|
}
|
54
180
|
};
|
55
181
|
|
56
|
-
|
57
|
-
|
58
|
-
this._unsubscribe = () => {
|
59
|
-
unsubFromStateChange();
|
60
|
-
};
|
182
|
+
// Subscribe to state changes and store the unsubscribe function
|
183
|
+
this._unsubscribe = source.subscribe(handleStateChange);
|
61
184
|
}
|
62
185
|
|
63
|
-
|
186
|
+
/**
|
187
|
+
* Subscribes to a specific CoValue and notifies the listener.
|
188
|
+
* This is the final step where we actually start receiving updates.
|
189
|
+
*/
|
190
|
+
private subscribe(value: RawCoValue): void {
|
64
191
|
if (this.unsubscribed) return;
|
65
192
|
|
193
|
+
// Subscribe to the value and store the unsubscribe function
|
66
194
|
this._unsubscribe = value.subscribe((value) => {
|
67
|
-
this.
|
195
|
+
this.emit(value);
|
68
196
|
});
|
197
|
+
}
|
198
|
+
|
199
|
+
emit(value: RawCoValue | "unavailable"): void {
|
200
|
+
if (this.unsubscribed) return;
|
69
201
|
|
70
202
|
this.listener(value);
|
71
203
|
}
|
72
204
|
|
73
|
-
|
205
|
+
/**
|
206
|
+
* Unsubscribes from all active subscriptions and marks the instance as unsubscribed.
|
207
|
+
* This prevents any further operations and ensures proper cleanup.
|
208
|
+
*/
|
209
|
+
unsubscribe(): void {
|
74
210
|
if (this.unsubscribed) return;
|
75
211
|
this.unsubscribed = true;
|
76
212
|
this._unsubscribe();
|
@@ -393,3 +393,15 @@ describe("account.$jazz.has", () => {
|
|
393
393
|
expect(account.root.settings).toBe("default");
|
394
394
|
});
|
395
395
|
});
|
396
|
+
|
397
|
+
describe("account.toJSON", () => {
|
398
|
+
test("returns only the acccount's Jazz id", async () => {
|
399
|
+
const account = await createJazzTestAccount({
|
400
|
+
creationProps: { name: "John" },
|
401
|
+
});
|
402
|
+
|
403
|
+
expect(account.toJSON()).toEqual({
|
404
|
+
$jazz: { id: account.$jazz.id },
|
405
|
+
});
|
406
|
+
});
|
407
|
+
});
|
@@ -105,6 +105,15 @@ describe("Simple CoFeed operations", async () => {
|
|
105
105
|
expect(stream.perSession[me.$jazz.sessionID]?.value).toEqual("milk");
|
106
106
|
});
|
107
107
|
|
108
|
+
test("toJSON", () => {
|
109
|
+
expect(stream.toJSON()).toEqual({
|
110
|
+
$jazz: { id: stream.$jazz.id },
|
111
|
+
in: {
|
112
|
+
[me.$jazz.sessionID]: stream.perSession[me.$jazz.sessionID]?.value,
|
113
|
+
},
|
114
|
+
});
|
115
|
+
});
|
116
|
+
|
108
117
|
describe("Mutation", () => {
|
109
118
|
test("push element into CoFeed of non-collaborative values", () => {
|
110
119
|
stream.$jazz.push("bread");
|
@@ -304,6 +313,22 @@ describe("Simple FileStream operations", async () => {
|
|
304
313
|
expect(stream.getChunks()).toBe(undefined);
|
305
314
|
});
|
306
315
|
|
316
|
+
test("toJSON", () => {
|
317
|
+
stream.start({ mimeType: "text/plain" });
|
318
|
+
stream.push(new Uint8Array([1, 2, 3]));
|
319
|
+
stream.push(new Uint8Array([4, 5, 6]));
|
320
|
+
stream.end();
|
321
|
+
|
322
|
+
expect(stream.toJSON()).toEqual({
|
323
|
+
$jazz: { id: stream.$jazz.id },
|
324
|
+
mimeType: "text/plain",
|
325
|
+
chunks: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])],
|
326
|
+
filename: undefined,
|
327
|
+
finished: true,
|
328
|
+
totalSizeBytes: undefined,
|
329
|
+
});
|
330
|
+
});
|
331
|
+
|
307
332
|
test("Mutation", () => {
|
308
333
|
stream.start({ mimeType: "text/plain" });
|
309
334
|
stream.push(new Uint8Array([1, 2, 3]));
|
@@ -243,6 +243,26 @@ describe("Simple CoList operations", async () => {
|
|
243
243
|
list.$jazz.push("cheese");
|
244
244
|
expect(list[3]?.toString()).toBe("cheese");
|
245
245
|
});
|
246
|
+
|
247
|
+
test("cannot push a shallowly-loaded CoValue into a deeply-loaded CoList", async () => {
|
248
|
+
const Task = co.map({ title: co.plainText() });
|
249
|
+
const TaskList = co.list(Task);
|
250
|
+
|
251
|
+
const task = Task.create({ title: "Do the dishes" });
|
252
|
+
const taskList = TaskList.create([]);
|
253
|
+
|
254
|
+
const loadedTask = await Task.load(task.$jazz.id);
|
255
|
+
const loadedTaskList = await TaskList.load(taskList.$jazz.id, {
|
256
|
+
resolve: { $each: { title: true } },
|
257
|
+
});
|
258
|
+
|
259
|
+
assert(loadedTask);
|
260
|
+
assert(loadedTaskList);
|
261
|
+
// @ts-expect-error loadedTask may not have its `title` loaded
|
262
|
+
loadedTaskList.$jazz.push(loadedTask);
|
263
|
+
// In this case the title is loaded, so the assertion passes
|
264
|
+
expect(loadedTaskList.at(-1)?.title.toString()).toBe("Do the dishes");
|
265
|
+
});
|
246
266
|
});
|
247
267
|
|
248
268
|
describe("unshift", () => {
|
@@ -281,14 +281,16 @@ describe("CoMap", async () => {
|
|
281
281
|
expect(person.friend?.age).toEqual(21);
|
282
282
|
});
|
283
283
|
|
284
|
-
test("JSON.stringify should
|
284
|
+
test("JSON.stringify should include user-defined properties + $jazz.id", () => {
|
285
285
|
const Person = co.map({
|
286
286
|
name: z.string(),
|
287
287
|
});
|
288
288
|
|
289
289
|
const person = Person.create({ name: "John" });
|
290
290
|
|
291
|
-
expect(JSON.stringify(person)).toEqual(
|
291
|
+
expect(JSON.stringify(person)).toEqual(
|
292
|
+
`{"$jazz":{"id":"${person.$jazz.id}"},"name":"John"}`,
|
293
|
+
);
|
292
294
|
});
|
293
295
|
|
294
296
|
test("toJSON should not fail when there is a key in the raw value not represented in the schema", () => {
|
@@ -302,6 +304,7 @@ describe("CoMap", async () => {
|
|
302
304
|
person.$jazz.raw.set("extra", "extra");
|
303
305
|
|
304
306
|
expect(person.toJSON()).toEqual({
|
307
|
+
$jazz: { id: person.$jazz.id },
|
305
308
|
name: "John",
|
306
309
|
age: 20,
|
307
310
|
});
|
@@ -323,9 +326,11 @@ describe("CoMap", async () => {
|
|
323
326
|
});
|
324
327
|
|
325
328
|
expect(person.toJSON()).toEqual({
|
329
|
+
$jazz: { id: person.$jazz.id },
|
326
330
|
name: "John",
|
327
331
|
age: 20,
|
328
332
|
friend: {
|
333
|
+
$jazz: { id: person.friend?.$jazz.id },
|
329
334
|
name: "Jane",
|
330
335
|
age: 21,
|
331
336
|
},
|
@@ -349,6 +354,7 @@ describe("CoMap", async () => {
|
|
349
354
|
person.$jazz.set("friend", person);
|
350
355
|
|
351
356
|
expect(person.toJSON()).toEqual({
|
357
|
+
$jazz: { id: person.$jazz.id },
|
352
358
|
name: "John",
|
353
359
|
age: 20,
|
354
360
|
friend: {
|
@@ -373,6 +379,7 @@ describe("CoMap", async () => {
|
|
373
379
|
});
|
374
380
|
|
375
381
|
expect(john.toJSON()).toMatchObject({
|
382
|
+
$jazz: { id: john.$jazz.id },
|
376
383
|
name: "John",
|
377
384
|
age: 20,
|
378
385
|
birthday: birthday.toISOString(),
|
@@ -407,6 +414,7 @@ describe("CoMap", async () => {
|
|
407
414
|
const john = Person.create({ name: "John", age: 30, x: 1 });
|
408
415
|
|
409
416
|
expect(john.toJSON()).toEqual({
|
417
|
+
$jazz: { id: john.$jazz.id },
|
410
418
|
name: "John",
|
411
419
|
age: 30,
|
412
420
|
});
|
@@ -510,6 +518,7 @@ describe("CoMap", async () => {
|
|
510
518
|
expect(john.age).toEqual(undefined);
|
511
519
|
|
512
520
|
expect(john.toJSON()).toEqual({
|
521
|
+
$jazz: { id: john.$jazz.id },
|
513
522
|
name: "John",
|
514
523
|
});
|
515
524
|
// The CoMap proxy hides the age property from the `in` operator
|
@@ -541,6 +550,7 @@ describe("CoMap", async () => {
|
|
541
550
|
expect(john.age).not.toBeDefined();
|
542
551
|
expect(john.pet).not.toBeDefined();
|
543
552
|
expect(john.toJSON()).toEqual({
|
553
|
+
$jazz: { id: john.$jazz.id },
|
544
554
|
name: "John",
|
545
555
|
});
|
546
556
|
expect("age" in john).toEqual(false);
|
package/tsup.config.ts
CHANGED