jazz-tools 0.18.3 → 0.18.5

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 (60) hide show
  1. package/.turbo/turbo-build.log +34 -34
  2. package/CHANGELOG.md +22 -0
  3. package/dist/better-auth/auth/react.d.ts +5 -2
  4. package/dist/better-auth/auth/react.d.ts.map +1 -1
  5. package/dist/better-auth/auth/server.d.ts +21 -1
  6. package/dist/better-auth/auth/server.d.ts.map +1 -1
  7. package/dist/better-auth/auth/server.js +10 -5
  8. package/dist/better-auth/auth/server.js.map +1 -1
  9. package/dist/browser/createBrowserContext.d.ts.map +1 -1
  10. package/dist/browser/index.js +7 -0
  11. package/dist/browser/index.js.map +1 -1
  12. package/dist/{chunk-IERUTUXB.js → chunk-3LE7N6TH.js} +121 -36
  13. package/dist/chunk-3LE7N6TH.js.map +1 -0
  14. package/dist/index.js +1 -1
  15. package/dist/react-core/index.js +120 -35
  16. package/dist/react-core/index.js.map +1 -1
  17. package/dist/testing.js +1 -1
  18. package/dist/tools/coValues/account.d.ts.map +1 -1
  19. package/dist/tools/coValues/coFeed.d.ts +12 -0
  20. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  21. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  22. package/dist/tools/implementation/anonymousJazzAgent.d.ts +1 -1
  23. package/dist/tools/implementation/anonymousJazzAgent.d.ts.map +1 -1
  24. package/dist/tools/implementation/zodSchema/coExport.d.ts +2 -0
  25. package/dist/tools/implementation/zodSchema/coExport.d.ts.map +1 -1
  26. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +19 -0
  27. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  28. package/dist/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.d.ts +4 -0
  29. package/dist/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.d.ts.map +1 -1
  30. package/dist/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.d.ts.map +1 -1
  31. package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
  32. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +61 -11
  33. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  34. package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts +2 -0
  35. package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts.map +1 -0
  36. package/dist/tools/testing.d.ts.map +1 -1
  37. package/package.json +5 -4
  38. package/src/better-auth/auth/server.ts +38 -7
  39. package/src/better-auth/auth/tests/server.test.ts +95 -7
  40. package/src/browser/createBrowserContext.ts +8 -0
  41. package/src/tools/coValues/account.ts +3 -1
  42. package/src/tools/coValues/coFeed.ts +5 -0
  43. package/src/tools/coValues/coMap.ts +3 -1
  44. package/src/tools/implementation/anonymousJazzAgent.ts +1 -1
  45. package/src/tools/implementation/zodSchema/coExport.ts +2 -0
  46. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +19 -0
  47. package/src/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.ts +8 -1
  48. package/src/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.ts +0 -1
  49. package/src/tools/implementation/zodSchema/unionUtils.ts +0 -1
  50. package/src/tools/subscribe/CoValueCoreSubscription.test.ts +1000 -0
  51. package/src/tools/subscribe/CoValueCoreSubscription.ts +179 -43
  52. package/src/tools/tests/account.test.ts +12 -0
  53. package/src/tools/tests/coFeed.test.ts +25 -0
  54. package/src/tools/tests/coList.test-d.ts +17 -0
  55. package/src/tools/tests/coList.test.ts +20 -0
  56. package/src/tools/tests/coMap.record.test-d.ts +18 -0
  57. package/src/tools/tests/coMap.record.test.ts +1 -0
  58. package/src/tools/tests/coMap.test-d.ts +15 -0
  59. package/src/tools/tests/coMap.test.ts +12 -2
  60. package/dist/chunk-IERUTUXB.js.map +0 -1
@@ -1,76 +1,212 @@
1
- import { CoValueCore, LocalNode, RawCoMap, RawCoValue } from "cojson";
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
- value: RawCoMap | undefined;
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
- public node: LocalNode,
11
- public id: string,
12
- public listener: (value: RawCoValue | "unavailable") => void,
13
- public skipRetry?: boolean,
24
+ localNode: LocalNode,
25
+ id: string,
26
+ listener: (value: RawCoValue | "unavailable") => void,
27
+ skipRetry?: boolean,
28
+ branch?: BranchDefinition,
14
29
  ) {
15
- const entry = this.node.getCoValue(this.id as any);
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
- if (entry?.isAvailable()) {
18
- this.subscribe(entry.getCurrentContent());
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
- this.node
21
- .loadCoValueCore(this.id as any, undefined, skipRetry)
22
- .then((value) => {
23
- if (this.unsubscribed) return;
24
-
25
- if (value.isAvailable()) {
26
- this.subscribe(value.getCurrentContent());
27
- } else {
28
- this.subscribeToState();
29
- this.listener("unavailable");
30
- }
31
- })
32
- .catch((error) => {
33
- console.error("Unexpected error loading CoValue: ", error);
34
- this.listener("unavailable");
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
- subscribeToState() {
40
- const entry = this.node.getCoValue(this.id as any);
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
- core: CoValueCore,
161
+ _: CoValueCore,
43
162
  unsubFromStateChange: () => void,
44
163
  ) => {
45
- if (this.unsubscribed) {
46
- unsubFromStateChange();
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
- if (core.isAvailable()) {
51
- this.subscribe(core.getCurrentContent());
52
- unsubFromStateChange();
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
- const unsubFromStateChange = entry.subscribe(handleStateChange);
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
- subscribe(value: RawCoValue) {
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.listener(value);
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
- unsubscribe() {
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]));
@@ -19,6 +19,23 @@ describe("CoList", () => {
19
19
  matches(list);
20
20
  });
21
21
 
22
+ test("co.input returns the type for the init payload", () => {
23
+ const ListSchema = co.list(
24
+ co.map({
25
+ name: z.string(),
26
+ age: z.number(),
27
+ address: co.map({
28
+ street: z.string(),
29
+ city: z.string(),
30
+ }),
31
+ }),
32
+ );
33
+
34
+ const init: co.input<typeof ListSchema> = [];
35
+
36
+ ListSchema.create(init);
37
+ });
38
+
22
39
  test("has the owner property", () => {
23
40
  const StringList = co.list(z.string());
24
41
 
@@ -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", () => {
@@ -24,6 +24,24 @@ describe("CoMap.Record", () => {
24
24
  matches(person);
25
25
  });
26
26
 
27
+ test("co.input returns the type for the init payload", () => {
28
+ const Person = co.record(
29
+ z.string(),
30
+ co.map({
31
+ name: z.string(),
32
+ age: z.number(),
33
+ address: co.map({
34
+ street: z.string(),
35
+ city: z.string(),
36
+ }),
37
+ }),
38
+ );
39
+
40
+ const init: co.input<typeof Person> = {};
41
+
42
+ Person.create(init);
43
+ });
44
+
27
45
  test("has the owner property", () => {
28
46
  const Person = co.record(z.string(), z.string());
29
47
 
@@ -140,6 +140,7 @@ describe("CoMap.Record", async () => {
140
140
  expect("age" in person).toEqual(false);
141
141
 
142
142
  expect(person.toJSON()).toEqual({
143
+ $jazz: { id: person.$jazz.id },
143
144
  name: "John",
144
145
  });
145
146
  });
@@ -45,6 +45,21 @@ describe("CoMap", async () => {
45
45
  matches(john);
46
46
  });
47
47
 
48
+ test("co.input returns the type for the init payload", () => {
49
+ const Person = co.map({
50
+ name: z.string(),
51
+ age: z.number(),
52
+ address: co.map({
53
+ street: z.string(),
54
+ city: z.string(),
55
+ }),
56
+ });
57
+
58
+ const init = {} as co.input<typeof Person>;
59
+
60
+ Person.create(init);
61
+ });
62
+
48
63
  test("has the owner property", () => {
49
64
  const Person = co.map({
50
65
  name: z.string(),
@@ -281,14 +281,16 @@ describe("CoMap", async () => {
281
281
  expect(person.friend?.age).toEqual(21);
282
282
  });
283
283
 
284
- test("JSON.stringify should not include internal properties", () => {
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('{"name":"John"}');
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);