jazz-tools 0.15.15 → 0.15.16

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 (40) hide show
  1. package/.turbo/turbo-build.log +36 -40
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{chunk-4CFNXQE7.js → chunk-OSVAAVWQ.js} +103 -6
  4. package/dist/chunk-OSVAAVWQ.js.map +1 -0
  5. package/dist/index.js +357 -3
  6. package/dist/index.js.map +1 -1
  7. package/dist/react/index.js +2 -0
  8. package/dist/react/index.js.map +1 -1
  9. package/dist/react/testing.js +3 -1
  10. package/dist/react/testing.js.map +1 -1
  11. package/dist/testing.js +1 -1
  12. package/dist/tools/coValues/group.d.ts +1 -0
  13. package/dist/tools/coValues/group.d.ts.map +1 -1
  14. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  15. package/dist/tools/coValues/interfaces.d.ts +58 -2
  16. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  17. package/dist/tools/coValues/request.d.ts +82 -0
  18. package/dist/tools/coValues/request.d.ts.map +1 -0
  19. package/dist/tools/exports.d.ts +2 -1
  20. package/dist/tools/exports.d.ts.map +1 -1
  21. package/dist/tools/lib/id.d.ts +2 -0
  22. package/dist/tools/lib/id.d.ts.map +1 -0
  23. package/dist/tools/subscribe/SubscriptionScope.d.ts +3 -2
  24. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  25. package/dist/tools/tests/exportImport.test.d.ts +2 -0
  26. package/dist/tools/tests/exportImport.test.d.ts.map +1 -0
  27. package/dist/tools/tests/request.test.d.ts +2 -0
  28. package/dist/tools/tests/request.test.d.ts.map +1 -0
  29. package/package.json +6 -5
  30. package/src/tools/coValues/group.ts +1 -0
  31. package/src/tools/coValues/inbox.ts +4 -3
  32. package/src/tools/coValues/interfaces.ts +177 -2
  33. package/src/tools/coValues/request.ts +631 -0
  34. package/src/tools/exports.ts +8 -0
  35. package/src/tools/lib/id.ts +3 -0
  36. package/src/tools/subscribe/SubscriptionScope.ts +10 -1
  37. package/src/tools/tests/exportImport.test.ts +526 -0
  38. package/src/tools/tests/request.test.ts +951 -0
  39. package/tsup.config.ts +0 -2
  40. package/dist/chunk-4CFNXQE7.js.map +0 -1
@@ -0,0 +1,526 @@
1
+ import { cojsonInternals } from "cojson";
2
+ import { assert, beforeEach, describe, expect, test } from "vitest";
3
+ import { exportCoValue, importContentPieces } from "../coValues/interfaces.js";
4
+ import { Account, CoPlainText, Group, co, z } from "../exports.js";
5
+ import {
6
+ createJazzTestAccount,
7
+ createJazzTestGuest,
8
+ setupJazzTestSync,
9
+ } from "../testing.js";
10
+
11
+ cojsonInternals.CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 10;
12
+
13
+ beforeEach(async () => {
14
+ await setupJazzTestSync();
15
+ await createJazzTestAccount({
16
+ isCurrentActiveAccount: true,
17
+ });
18
+ });
19
+
20
+ describe("exportCoValue", () => {
21
+ test("exports a simple CoMap", async () => {
22
+ const Person = co.map({
23
+ name: z.string(),
24
+ age: z.number(),
25
+ });
26
+
27
+ const group = Group.create();
28
+ const person = Person.create({ name: "John", age: 30 }, group);
29
+ group.addMember("everyone", "reader");
30
+
31
+ const alice = await createJazzTestAccount();
32
+
33
+ const exported = await exportCoValue(Person, person.id, {
34
+ loadAs: alice,
35
+ });
36
+
37
+ expect(exported).not.toBeNull();
38
+ expect(exported).toBeInstanceOf(Array);
39
+ expect(exported!.length).toBeGreaterThan(0);
40
+
41
+ // Verify the exported content contains the person data
42
+ const hasPersonContent = exported!.some((piece) => piece.id === person.id);
43
+ expect(hasPersonContent).toBe(true);
44
+ });
45
+
46
+ test("exports a CoMap with nested references", async () => {
47
+ const Address = co.map({
48
+ street: z.string(),
49
+ city: z.string(),
50
+ });
51
+
52
+ const Person = co.map({
53
+ name: z.string(),
54
+ address: Address,
55
+ });
56
+
57
+ const group = Group.create();
58
+ const address = Address.create(
59
+ { street: "123 Main St", city: "New York" },
60
+ group,
61
+ );
62
+ const person = Person.create({ name: "John", address }, group);
63
+ group.addMember("everyone", "reader");
64
+
65
+ const alice = await createJazzTestAccount();
66
+
67
+ const exported = await exportCoValue(Person, person.id, {
68
+ resolve: { address: true },
69
+ loadAs: alice,
70
+ });
71
+
72
+ expect(exported).not.toBeNull();
73
+ expect(exported).toBeInstanceOf(Array);
74
+ expect(exported!.length).toBeGreaterThan(0);
75
+
76
+ // Verify both person and address content are exported
77
+ const personContent = exported!.filter((piece) => piece.id === person.id);
78
+ const addressContent = exported!.filter((piece) => piece.id === address.id);
79
+
80
+ expect(personContent.length).toBeGreaterThan(0);
81
+ expect(addressContent.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ test("exports a CoList", async () => {
85
+ const TodoList = co.list(z.string());
86
+
87
+ const group = Group.create();
88
+ const todos = TodoList.create([], group);
89
+ todos.push("Buy groceries");
90
+ todos.push("Walk the dog");
91
+ group.addMember("everyone", "reader");
92
+
93
+ const alice = await createJazzTestAccount();
94
+
95
+ const exported = await exportCoValue(TodoList, todos.id, {
96
+ loadAs: alice,
97
+ });
98
+
99
+ expect(exported).not.toBeNull();
100
+ expect(exported).toBeInstanceOf(Array);
101
+ expect(exported!.length).toBeGreaterThan(0);
102
+
103
+ const hasTodoContent = exported!.some((piece) => piece.id === todos.id);
104
+ expect(hasTodoContent).toBe(true);
105
+ });
106
+
107
+ test("exports a CoStream", async () => {
108
+ const ChatStream = co.feed(z.string());
109
+
110
+ const group = Group.create();
111
+ const chat = ChatStream.create([], group);
112
+ chat.push("Hello");
113
+ chat.push("World");
114
+ group.addMember("everyone", "reader");
115
+
116
+ const alice = await createJazzTestAccount();
117
+
118
+ const exported = await exportCoValue(ChatStream, chat.id, {
119
+ loadAs: alice,
120
+ });
121
+
122
+ expect(exported).not.toBeNull();
123
+ expect(exported).toBeInstanceOf(Array);
124
+ expect(exported!.length).toBeGreaterThan(0);
125
+
126
+ const hasChatContent = exported!.some((piece) => piece.id === chat.id);
127
+ expect(hasChatContent).toBe(true);
128
+ });
129
+
130
+ test("returns null for unauthorized CoValue", async () => {
131
+ const Person = co.map({
132
+ name: z.string(),
133
+ });
134
+
135
+ const group = Group.create();
136
+ const person = Person.create({ name: "John" }, group);
137
+ // Don't add any members, so it's private
138
+
139
+ const alice = await createJazzTestAccount();
140
+
141
+ const exported = await exportCoValue(Person, person.id, {
142
+ loadAs: alice,
143
+ });
144
+
145
+ expect(exported).toBeNull();
146
+ });
147
+
148
+ test("exports with custom resolve options", async () => {
149
+ const Address = co.map({
150
+ street: z.string(),
151
+ city: z.string(),
152
+ });
153
+
154
+ const Person = co.map({
155
+ name: z.string(),
156
+ address: Address,
157
+ });
158
+
159
+ const group = Group.create();
160
+ const address = Address.create(
161
+ { street: "123 Main St", city: "New York" },
162
+ group,
163
+ );
164
+ const person = Person.create({ name: "John", address }, group);
165
+ group.addMember("everyone", "reader");
166
+
167
+ const alice = await createJazzTestAccount();
168
+
169
+ // Export without resolving nested references
170
+ const exportedWithoutResolve = await exportCoValue(Person, person.id, {
171
+ resolve: { address: false },
172
+ loadAs: alice,
173
+ });
174
+
175
+ // Export with resolving nested references
176
+ const exportedWithResolve = await exportCoValue(Person, person.id, {
177
+ resolve: { address: true },
178
+ loadAs: alice,
179
+ });
180
+
181
+ expect(exportedWithoutResolve).not.toBeNull();
182
+ expect(exportedWithResolve).not.toBeNull();
183
+
184
+ // The version with resolve should have more content pieces
185
+ expect(exportedWithResolve!.length).toBeGreaterThanOrEqual(
186
+ exportedWithoutResolve!.length,
187
+ );
188
+ });
189
+
190
+ test("exports should handle errors on child covalues gracefully", async () => {
191
+ const Address = co.map({
192
+ street: co.plainText(),
193
+ city: co.plainText(),
194
+ });
195
+
196
+ const Person = co.map({
197
+ name: z.string(),
198
+ address: Address,
199
+ });
200
+
201
+ const group = Group.create();
202
+ const address = Address.create(
203
+ {
204
+ street: CoPlainText.create("123 Main St"),
205
+ city: CoPlainText.create("New York"),
206
+ },
207
+ group,
208
+ );
209
+ const person = Person.create({ name: "John", address }, group);
210
+
211
+ // Only add the person to the group, not the address
212
+ // This makes the address unauthorized for other accounts
213
+ group.addMember("everyone", "reader");
214
+
215
+ const alice = await createJazzTestAccount();
216
+
217
+ // Export from alice's perspective with resolve: true
218
+ // This should attempt to resolve the address but handle the error gracefully
219
+ const exported = await exportCoValue(Person, person.id, {
220
+ resolve: { address: { street: true, city: true } },
221
+ loadAs: alice,
222
+ bestEffortResolution: true,
223
+ });
224
+
225
+ assert(exported);
226
+
227
+ // Verify the person content is exported
228
+ const personContent = exported.filter((piece) => piece.id === person.id);
229
+ expect(personContent.length).toBeGreaterThan(0);
230
+
231
+ const addressContent = exported.filter((piece) => piece.id === address.id);
232
+ expect(addressContent.length).toBeGreaterThan(0);
233
+
234
+ const streetContent = exported.filter(
235
+ (piece) => piece.id === address.street.id,
236
+ );
237
+ expect(streetContent).toHaveLength(0);
238
+
239
+ const cityContent = exported.filter(
240
+ (piece) => piece.id === address.city.id,
241
+ );
242
+ expect(cityContent).toHaveLength(0);
243
+ });
244
+ });
245
+
246
+ describe("importContentPieces", () => {
247
+ test("imports content pieces successfully", async () => {
248
+ const Person = co.map({
249
+ name: z.string(),
250
+ age: z.number(),
251
+ });
252
+
253
+ const group = Group.create();
254
+ const person = Person.create({ name: "John", age: 30 }, group);
255
+ group.addMember("everyone", "reader");
256
+
257
+ const alice = await createJazzTestAccount();
258
+ const bob = await createJazzTestAccount();
259
+
260
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
261
+ peer.gracefulShutdown();
262
+ });
263
+
264
+ // Export from alice's perspective
265
+ const exported = await exportCoValue(Person, person.id, {
266
+ loadAs: alice,
267
+ });
268
+
269
+ expect(exported).not.toBeNull();
270
+
271
+ // Import to bob's node
272
+ importContentPieces(exported!, bob);
273
+
274
+ // Verify bob can now access the person
275
+ const importedPerson = await Person.load(person.id, { loadAs: bob });
276
+ expect(importedPerson).not.toBeNull();
277
+ expect(importedPerson?.name).toBe("John");
278
+ expect(importedPerson?.age).toBe(30);
279
+ });
280
+
281
+ test("imports content pieces with nested references", async () => {
282
+ const Address = co.map({
283
+ street: z.string(),
284
+ city: z.string(),
285
+ });
286
+
287
+ const Person = co.map({
288
+ name: z.string(),
289
+ address: Address,
290
+ });
291
+
292
+ const group = Group.create();
293
+ const address = Address.create(
294
+ { street: "123 Main St", city: "New York" },
295
+ group,
296
+ );
297
+ const person = Person.create({ name: "John", address }, group);
298
+ group.addMember("everyone", "reader");
299
+
300
+ const alice = await createJazzTestAccount();
301
+ const bob = await createJazzTestAccount();
302
+
303
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
304
+ peer.gracefulShutdown();
305
+ });
306
+
307
+ // Export with resolved references
308
+ const exported = await exportCoValue(Person, person.id, {
309
+ resolve: { address: true },
310
+ loadAs: alice,
311
+ });
312
+
313
+ expect(exported).not.toBeNull();
314
+
315
+ // Import to bob's node
316
+ importContentPieces(exported!, bob);
317
+
318
+ // Verify bob can access both person and address
319
+ const importedPerson = await Person.load(person.id, {
320
+ resolve: { address: true },
321
+ loadAs: bob,
322
+ });
323
+
324
+ expect(importedPerson).not.toBeNull();
325
+ expect(importedPerson?.name).toBe("John");
326
+ expect(importedPerson?.address).not.toBeNull();
327
+ expect(importedPerson?.address.street).toBe("123 Main St");
328
+ expect(importedPerson?.address.city).toBe("New York");
329
+ });
330
+
331
+ test("imports content pieces to anonymous agent", async () => {
332
+ const Person = co.map({
333
+ name: z.string(),
334
+ });
335
+
336
+ const group = Group.create();
337
+ const person = Person.create({ name: "John" }, group);
338
+ group.addMember("everyone", "reader");
339
+
340
+ const alice = await createJazzTestAccount();
341
+ const { guest } = await createJazzTestGuest();
342
+
343
+ guest.node.syncManager.getPeers().forEach((peer) => {
344
+ peer.gracefulShutdown();
345
+ });
346
+
347
+ // Export from alice's perspective
348
+ const exported = await exportCoValue(Person, person.id, {
349
+ loadAs: alice,
350
+ });
351
+
352
+ expect(exported).not.toBeNull();
353
+
354
+ // Import to anonymous agent
355
+ importContentPieces(exported!, guest);
356
+
357
+ // Verify anonymous agent can access the person
358
+ const importedPerson = await Person.load(person.id, { loadAs: guest });
359
+ expect(importedPerson).not.toBeNull();
360
+ expect(importedPerson?.name).toBe("John");
361
+ });
362
+
363
+ test("imports content pieces without specifying loadAs (uses current account)", async () => {
364
+ const Person = co.map({
365
+ name: z.string(),
366
+ });
367
+
368
+ const group = Group.create();
369
+ const person = Person.create({ name: "John" }, group);
370
+ group.addMember("everyone", "reader");
371
+
372
+ const alice = await createJazzTestAccount();
373
+ const bob = await createJazzTestAccount({
374
+ isCurrentActiveAccount: true,
375
+ });
376
+
377
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
378
+ peer.gracefulShutdown();
379
+ });
380
+
381
+ // Export from alice's perspective
382
+ const exported = await exportCoValue(Person, person.id, {
383
+ loadAs: alice,
384
+ });
385
+
386
+ expect(exported).not.toBeNull();
387
+
388
+ // Import without specifying loadAs (should use current account)
389
+ importContentPieces(exported!);
390
+
391
+ // Verify bob can access the person
392
+ const importedPerson = await Person.load(person.id, { loadAs: bob });
393
+ expect(importedPerson).not.toBeNull();
394
+ expect(importedPerson?.name).toBe("John");
395
+ });
396
+
397
+ test("handles empty content pieces array", async () => {
398
+ const alice = await createJazzTestAccount();
399
+
400
+ // Should not throw when importing empty array
401
+ expect(() => {
402
+ importContentPieces([], alice);
403
+ }).not.toThrow();
404
+ });
405
+
406
+ test("handles duplicate content pieces", async () => {
407
+ const Person = co.map({
408
+ name: z.string(),
409
+ });
410
+
411
+ const group = Group.create();
412
+ const person = Person.create({ name: "John" }, group);
413
+ group.addMember("everyone", "reader");
414
+
415
+ const alice = await createJazzTestAccount();
416
+ const bob = await createJazzTestAccount();
417
+
418
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
419
+ peer.gracefulShutdown();
420
+ });
421
+
422
+ // Export from alice's perspective
423
+ const exported = await exportCoValue(Person, person.id, {
424
+ loadAs: alice,
425
+ });
426
+
427
+ expect(exported).not.toBeNull();
428
+
429
+ // Import the same content pieces twice
430
+ importContentPieces(exported!, bob);
431
+ importContentPieces(exported!, bob);
432
+
433
+ // Should still work correctly
434
+ const importedPerson = await Person.load(person.id, { loadAs: bob });
435
+ expect(importedPerson).not.toBeNull();
436
+ expect(importedPerson?.name).toBe("John");
437
+ });
438
+
439
+ test("imports content pieces with complex nested structure", async () => {
440
+ const Comment = co.map({
441
+ text: z.string(),
442
+ author: z.string(),
443
+ });
444
+
445
+ const Post = co.map({
446
+ title: z.string(),
447
+ content: z.string(),
448
+ comments: co.list(Comment),
449
+ });
450
+
451
+ const Blog = co.map({
452
+ name: z.string(),
453
+ posts: co.list(Post),
454
+ });
455
+
456
+ const group = Group.create();
457
+ const comment1 = Comment.create(
458
+ { text: "Great post!", author: "Alice" },
459
+ group,
460
+ );
461
+ const comment2 = Comment.create({ text: "Thanks!", author: "Bob" }, group);
462
+
463
+ const post = Post.create(
464
+ {
465
+ title: "My First Post",
466
+ content: "Hello World",
467
+ comments: Post.def.shape.comments.create([comment1, comment2], group),
468
+ },
469
+ group,
470
+ );
471
+
472
+ const blog = Blog.create(
473
+ { name: "My Blog", posts: Blog.def.shape.posts.create([post], group) },
474
+ group,
475
+ );
476
+
477
+ group.addMember("everyone", "reader");
478
+
479
+ const alice = await createJazzTestAccount();
480
+ const bob = await createJazzTestAccount();
481
+
482
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
483
+ peer.gracefulShutdown();
484
+ });
485
+
486
+ // Export with all nested references resolved
487
+ const exported = await exportCoValue(Blog, blog.id, {
488
+ resolve: {
489
+ posts: {
490
+ $each: {
491
+ comments: true,
492
+ },
493
+ },
494
+ },
495
+ loadAs: alice,
496
+ });
497
+
498
+ expect(exported).not.toBeNull();
499
+
500
+ // Import to bob's node
501
+ importContentPieces(exported!, bob);
502
+
503
+ // Verify bob can access the entire structure
504
+ const importedBlog = await Blog.load(blog.id, {
505
+ resolve: {
506
+ posts: {
507
+ $each: {
508
+ comments: true,
509
+ },
510
+ },
511
+ },
512
+ loadAs: bob,
513
+ });
514
+
515
+ expect(importedBlog).not.toBeNull();
516
+ expect(importedBlog?.name).toBe("My Blog");
517
+ expect(importedBlog?.posts.length).toBe(1);
518
+ expect(importedBlog?.posts[0]?.title).toBe("My First Post");
519
+ expect(importedBlog?.posts[0]?.content).toBe("Hello World");
520
+ expect(importedBlog?.posts[0]?.comments.length).toBe(2);
521
+ expect(importedBlog?.posts[0]?.comments[0]?.text).toBe("Great post!");
522
+ expect(importedBlog?.posts[0]?.comments[0]?.author).toBe("Alice");
523
+ expect(importedBlog?.posts[0]?.comments[1]?.text).toBe("Thanks!");
524
+ expect(importedBlog?.posts[0]?.comments[1]?.author).toBe("Bob");
525
+ });
526
+ });