jazz-tools 0.13.16 → 0.13.18

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 (83) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/CHANGELOG.md +19 -0
  3. package/dist/{chunk-GIZWJXSR.js → chunk-ITSHLDQB.js} +731 -600
  4. package/dist/chunk-ITSHLDQB.js.map +1 -0
  5. package/dist/coValues/account.d.ts +5 -4
  6. package/dist/coValues/account.d.ts.map +1 -1
  7. package/dist/coValues/coFeed.d.ts +3 -3
  8. package/dist/coValues/coFeed.d.ts.map +1 -1
  9. package/dist/coValues/coList.d.ts +1 -0
  10. package/dist/coValues/coList.d.ts.map +1 -1
  11. package/dist/coValues/coMap.d.ts +4 -2
  12. package/dist/coValues/coMap.d.ts.map +1 -1
  13. package/dist/coValues/coPlainText.d.ts +2 -2
  14. package/dist/coValues/coPlainText.d.ts.map +1 -1
  15. package/dist/coValues/deepLoading.d.ts +1 -9
  16. package/dist/coValues/deepLoading.d.ts.map +1 -1
  17. package/dist/coValues/extensions/imageDef.d.ts.map +1 -1
  18. package/dist/coValues/group.d.ts.map +1 -1
  19. package/dist/coValues/inbox.d.ts.map +1 -1
  20. package/dist/coValues/interfaces.d.ts +4 -1
  21. package/dist/coValues/interfaces.d.ts.map +1 -1
  22. package/dist/implementation/createContext.d.ts.map +1 -1
  23. package/dist/implementation/refs.d.ts +5 -10
  24. package/dist/implementation/refs.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/internal.d.ts +1 -1
  27. package/dist/internal.d.ts.map +1 -1
  28. package/dist/subscribe/CoValueCoreSubscription.d.ts +14 -0
  29. package/dist/subscribe/CoValueCoreSubscription.d.ts.map +1 -0
  30. package/dist/subscribe/JazzError.d.ts +16 -0
  31. package/dist/subscribe/JazzError.d.ts.map +1 -0
  32. package/dist/subscribe/SubscriptionScope.d.ts +42 -0
  33. package/dist/subscribe/SubscriptionScope.d.ts.map +1 -0
  34. package/dist/subscribe/index.d.ts +21 -0
  35. package/dist/subscribe/index.d.ts.map +1 -0
  36. package/dist/subscribe/types.d.ts +12 -0
  37. package/dist/subscribe/types.d.ts.map +1 -0
  38. package/dist/subscribe/utils.d.ts +10 -0
  39. package/dist/subscribe/utils.d.ts.map +1 -0
  40. package/dist/testing.js +2 -2
  41. package/dist/testing.js.map +1 -1
  42. package/dist/tests/coMap.record.test.d.ts +2 -0
  43. package/dist/tests/coMap.record.test.d.ts.map +1 -0
  44. package/dist/tests/utils.d.ts +2 -2
  45. package/dist/tests/utils.d.ts.map +1 -1
  46. package/package.json +2 -2
  47. package/src/coValues/account.ts +43 -31
  48. package/src/coValues/coFeed.ts +28 -13
  49. package/src/coValues/coList.ts +13 -17
  50. package/src/coValues/coMap.ts +72 -80
  51. package/src/coValues/coPlainText.ts +13 -2
  52. package/src/coValues/deepLoading.ts +4 -277
  53. package/src/coValues/extensions/imageDef.ts +1 -7
  54. package/src/coValues/group.ts +7 -6
  55. package/src/coValues/inbox.ts +4 -11
  56. package/src/coValues/interfaces.ts +54 -111
  57. package/src/implementation/createContext.ts +3 -4
  58. package/src/implementation/invites.ts +2 -2
  59. package/src/implementation/refs.ts +30 -121
  60. package/src/internal.ts +1 -2
  61. package/src/subscribe/CoValueCoreSubscription.ts +71 -0
  62. package/src/subscribe/JazzError.ts +48 -0
  63. package/src/subscribe/SubscriptionScope.ts +523 -0
  64. package/src/subscribe/index.ts +82 -0
  65. package/src/subscribe/types.ts +7 -0
  66. package/src/subscribe/utils.ts +36 -0
  67. package/src/testing.ts +1 -1
  68. package/src/tests/ContextManager.test.ts +13 -9
  69. package/src/tests/coFeed.test.ts +104 -4
  70. package/src/tests/coList.test.ts +304 -115
  71. package/src/tests/coMap.record.test.ts +325 -0
  72. package/src/tests/coMap.test.ts +718 -645
  73. package/src/tests/coPlainText.test.ts +2 -2
  74. package/src/tests/createContext.test.ts +8 -8
  75. package/src/tests/deepLoading.test.ts +8 -34
  76. package/src/tests/groupsAndAccounts.test.ts +6 -4
  77. package/src/tests/subscribe.test.ts +357 -42
  78. package/src/tests/utils.ts +8 -6
  79. package/tsconfig.json +2 -1
  80. package/dist/chunk-GIZWJXSR.js.map +0 -1
  81. package/dist/implementation/subscriptionScope.d.ts +0 -34
  82. package/dist/implementation/subscriptionScope.d.ts.map +0 -1
  83. package/src/implementation/subscriptionScope.ts +0 -165
@@ -1,420 +1,423 @@
1
1
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
2
- import { describe, expect, expectTypeOf, test, vi } from "vitest";
3
- import { Group, randomSessionProvider } from "../exports.js";
4
2
  import {
5
- Account,
6
- CoMap,
7
- Encoders,
8
- co,
9
- cojsonInternals,
10
- createJazzContextFromExistingCredentials,
11
- isControlledAccount,
12
- } from "../index.js";
13
- import { setupTwoNodes } from "./utils.js";
3
+ assert,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ expectTypeOf,
8
+ it,
9
+ test,
10
+ vi,
11
+ } from "vitest";
12
+ import { Group, Resolved, subscribeToCoValue } from "../exports.js";
13
+ import { Account, CoMap, Encoders, co, cojsonInternals } from "../index.js";
14
+ import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
15
+ import { setupTwoNodes, waitFor } from "./utils.js";
14
16
 
15
17
  const { connectedPeers } = cojsonInternals;
16
18
 
17
19
  const Crypto = await WasmCrypto.create();
18
20
 
19
- class TestMap extends CoMap {
20
- color = co.string;
21
- _height = co.number;
22
- birthday = co.Date;
23
- name? = co.string;
24
- nullable = co.optional.encoded<string | undefined>({
25
- encode: (value: string | undefined) => value || null,
26
- decode: (value: unknown) => (value as string) || undefined,
27
- });
28
- optionalDate = co.optional.Date;
29
-
30
- get roughColor() {
31
- return this.color + "ish";
32
- }
33
- }
21
+ beforeEach(async () => {
22
+ await setupJazzTestSync();
34
23
 
35
- describe("Simple CoMap operations", async () => {
36
- const me = await Account.create({
24
+ await createJazzTestAccount({
25
+ isCurrentActiveAccount: true,
37
26
  creationProps: { name: "Hermes Puggington" },
38
- crypto: Crypto,
39
27
  });
28
+ });
40
29
 
41
- const birthday = new Date();
42
-
43
- const map = TestMap.create(
44
- {
45
- color: "red",
46
- _height: 10,
47
- birthday: birthday,
48
- nullable: undefined,
49
- },
50
- { owner: me },
51
- );
52
-
53
- test("Construction", () => {
54
- expect(map.color).toEqual("red");
55
- expect(map.roughColor).toEqual("redish");
56
- expect(map._height).toEqual(10);
57
- expect(map.birthday).toEqual(birthday);
58
- expect(map._raw.get("birthday")).toEqual(birthday.toISOString());
59
- expect(Object.keys(map)).toEqual([
60
- "color",
61
- "_height",
62
- "birthday",
63
- "nullable",
64
- ]);
65
- });
30
+ describe("CoMap", async () => {
31
+ describe("init", () => {
32
+ test("create a CoMap with basic property access", () => {
33
+ class Person extends CoMap {
34
+ color = co.string;
35
+ _height = co.number;
36
+ birthday = co.Date;
37
+ name = co.string;
38
+ nullable = co.optional.encoded<string | undefined>({
39
+ encode: (value: string | undefined) => value || null,
40
+ decode: (value: unknown) => (value as string) || undefined,
41
+ });
42
+ optionalDate = co.optional.Date;
43
+
44
+ get roughColor() {
45
+ return this.color + "ish";
46
+ }
47
+ }
48
+
49
+ const birthday = new Date("1989-11-27");
50
+
51
+ const john = Person.create({
52
+ color: "red",
53
+ _height: 10,
54
+ birthday,
55
+ name: "John",
56
+ });
66
57
 
67
- test("Construction with an Account", () => {
68
- const map = TestMap.create(
69
- { color: "red", _height: 10, birthday: birthday },
70
- me,
71
- );
58
+ expect(john.color).toEqual("red");
59
+ expect(john.roughColor).toEqual("redish");
60
+ expect(john._height).toEqual(10);
61
+ expect(john.birthday).toEqual(birthday);
62
+ expect(john._raw.get("birthday")).toEqual(birthday.toISOString());
63
+ expect(Object.keys(john)).toEqual([
64
+ "color",
65
+ "_height",
66
+ "birthday",
67
+ "name",
68
+ ]);
69
+ });
72
70
 
73
- expect(map.color).toEqual("red");
74
- });
71
+ test("property existence", () => {
72
+ class Person extends CoMap {
73
+ name = co.string;
74
+ }
75
75
 
76
- test("Construction with a Group", () => {
77
- const group = Group.create(me);
78
- const map = TestMap.create(
79
- { color: "red", _height: 10, birthday: birthday },
80
- group,
81
- );
76
+ const john = Person.create({ name: "John" });
82
77
 
83
- expect(map.color).toEqual("red");
84
- });
78
+ expect("name" in john).toEqual(true);
79
+ expect("age" in john).toEqual(false);
80
+ });
85
81
 
86
- test("Construction with too many things provided", () => {
87
- const mapWithExtra = TestMap.create(
88
- {
89
- color: "red",
90
- _height: 10,
91
- birthday: birthday,
92
- name: "Hermes",
93
- extra: "extra",
94
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
- } as any,
96
- { owner: me },
97
- );
82
+ test("create a CoMap with an account as owner", () => {
83
+ class Person extends CoMap {
84
+ name = co.string;
85
+ }
98
86
 
99
- expect(mapWithExtra.color).toEqual("red");
100
- });
87
+ const john = Person.create({ name: "John" }, Account.getMe());
101
88
 
102
- test("Empty schema", () => {
103
- const emptyMap = CoMap.create({}, { owner: me });
89
+ expect(john.name).toEqual("John");
90
+ expect(john._raw.get("name")).toEqual("John");
91
+ });
104
92
 
105
- // @ts-expect-error
106
- expect(emptyMap.color).toEqual(undefined);
107
- });
93
+ test("create a CoMap with a group as owner", () => {
94
+ class Person extends CoMap {
95
+ name = co.string;
96
+ }
108
97
 
109
- test("setting date as undefined should throw", () => {
110
- expect(() =>
111
- TestMap.create(
112
- {
98
+ const john = Person.create({ name: "John" }, Group.create());
99
+
100
+ expect(john.name).toEqual("John");
101
+ expect(john._raw.get("name")).toEqual("John");
102
+ });
103
+
104
+ test("Empty schema", () => {
105
+ const emptyMap = CoMap.create({});
106
+
107
+ // @ts-expect-error
108
+ expect(emptyMap.color).toEqual(undefined);
109
+ });
110
+
111
+ test("setting date as undefined should throw", () => {
112
+ class Person extends CoMap {
113
+ color = co.string;
114
+ _height = co.number;
115
+ birthday = co.Date;
116
+ name = co.string;
117
+ nullable = co.optional.encoded<string | undefined>({
118
+ encode: (value: string | undefined) => value || null,
119
+ decode: (value: unknown) => (value as string) || undefined,
120
+ });
121
+ optionalDate = co.optional.Date;
122
+
123
+ get roughColor() {
124
+ return this.color + "ish";
125
+ }
126
+ }
127
+
128
+ expect(() =>
129
+ Person.create({
113
130
  color: "red",
114
131
  _height: 10,
132
+ name: "John",
115
133
  birthday: undefined!,
116
- },
117
- { owner: me },
118
- ),
119
- ).toThrow();
120
- });
134
+ }),
135
+ ).toThrow();
136
+ });
121
137
 
122
- test("toJSON should not fail when there is a key in the raw value not represented in the schema", () => {
123
- class TestMap extends CoMap {
124
- color = co.string;
125
- height = co.number;
126
- }
138
+ test("CoMap with reference", () => {
139
+ class Dog extends CoMap {
140
+ name = co.string;
141
+ breed = co.string;
142
+ }
143
+
144
+ class Person extends CoMap {
145
+ name = co.string;
146
+ age = co.number;
147
+ dog = co.ref(Dog);
148
+ }
149
+
150
+ const person = Person.create({
151
+ name: "John",
152
+ age: 20,
153
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
154
+ });
127
155
 
128
- const map = TestMap.create({ color: "red", height: 10 }, { owner: me });
156
+ expect(person.dog?.name).toEqual("Rex");
157
+ expect(person.dog?.breed).toEqual("Labrador");
158
+ });
129
159
 
130
- map._raw.set("extra", "extra");
160
+ test("CoMap with self reference", () => {
161
+ class Person extends CoMap {
162
+ name = co.string;
163
+ age = co.number;
164
+ friend = co.optional.ref(Person);
165
+ }
166
+
167
+ const person = Person.create({
168
+ name: "John",
169
+ age: 20,
170
+ friend: Person.create({ name: "Jane", age: 21 }),
171
+ });
131
172
 
132
- expect(map.toJSON()).toEqual({
133
- _type: "CoMap",
134
- id: map.id,
135
- color: "red",
136
- height: 10,
173
+ expect(person.friend?.name).toEqual("Jane");
174
+ expect(person.friend?.age).toEqual(21);
137
175
  });
138
- });
139
176
 
140
- test("toJSON should handle references", () => {
141
- class TestMap extends CoMap {
142
- color = co.string;
143
- height = co.number;
144
- nested = co.optional.ref(TestMap);
145
- }
177
+ test("toJSON should not fail when there is a key in the raw value not represented in the schema", () => {
178
+ class Person extends CoMap {
179
+ name = co.string;
180
+ age = co.number;
181
+ }
146
182
 
147
- const map = TestMap.create({ color: "red", height: 10 }, { owner: me });
183
+ const person = Person.create({ name: "John", age: 20 });
148
184
 
149
- map.nested = TestMap.create({ color: "blue", height: 20 }, { owner: me });
185
+ person._raw.set("extra", "extra");
150
186
 
151
- expect(map.toJSON()).toEqual({
152
- _type: "CoMap",
153
- id: map.id,
154
- color: "red",
155
- height: 10,
156
- nested: {
187
+ expect(person.toJSON()).toEqual({
157
188
  _type: "CoMap",
158
- id: map.nested?.id,
159
- color: "blue",
160
- height: 20,
161
- },
189
+ id: person.id,
190
+ name: "John",
191
+ age: 20,
192
+ });
162
193
  });
163
- });
164
194
 
165
- test("toJSON should handle circular references", () => {
166
- class TestMap extends CoMap {
167
- color = co.string;
168
- height = co.number;
169
- nested = co.optional.ref(TestMap);
170
- }
195
+ test("toJSON should handle references", () => {
196
+ class Person extends CoMap {
197
+ name = co.string;
198
+ age = co.number;
199
+ friend = co.optional.ref(Person);
200
+ }
201
+
202
+ const person = Person.create({
203
+ name: "John",
204
+ age: 20,
205
+ friend: Person.create({ name: "Jane", age: 21 }),
206
+ });
171
207
 
172
- const map = TestMap.create({ color: "red", height: 10 }, { owner: me });
208
+ expect(person.toJSON()).toEqual({
209
+ _type: "CoMap",
210
+ id: person.id,
211
+ name: "John",
212
+ age: 20,
213
+ friend: {
214
+ _type: "CoMap",
215
+ id: person.friend?.id,
216
+ name: "Jane",
217
+ age: 21,
218
+ },
219
+ });
220
+ });
173
221
 
174
- map.nested = map;
222
+ test("toJSON should handle circular references", () => {
223
+ class Person extends CoMap {
224
+ name = co.string;
225
+ age = co.number;
226
+ friend = co.optional.ref(Person);
227
+ }
175
228
 
176
- expect(map.toJSON()).toEqual({
177
- _type: "CoMap",
178
- id: map.id,
179
- color: "red",
180
- height: 10,
181
- nested: {
182
- _circular: map.id,
183
- },
184
- });
185
- });
229
+ const person = Person.create({
230
+ name: "John",
231
+ age: 20,
232
+ });
186
233
 
187
- test("testing toJSON on a CoMap with a Date field", () => {
188
- const map = TestMap.create(
189
- {
190
- color: "red",
191
- _height: 10,
192
- birthday: new Date(),
193
- },
194
- { owner: me },
195
- );
234
+ person.friend = person;
196
235
 
197
- expect(map.toJSON()).toMatchObject({
198
- color: "red",
199
- _height: 10,
200
- birthday: expect.any(String),
201
- _type: "CoMap",
202
- id: expect.any(String),
236
+ expect(person.toJSON()).toEqual({
237
+ _type: "CoMap",
238
+ id: person.id,
239
+ name: "John",
240
+ age: 20,
241
+ friend: {
242
+ _circular: person.id,
243
+ },
244
+ });
203
245
  });
204
- });
205
246
 
206
- test("setting optional date as undefined should not throw", () => {
207
- const map = TestMap.create(
208
- {
209
- color: "red",
210
- _height: 10,
247
+ test("testing toJSON on a CoMap with a Date field", () => {
248
+ class Person extends CoMap {
249
+ name = co.string;
250
+ age = co.number;
251
+ birthday = co.Date;
252
+ }
253
+
254
+ const birthday = new Date();
255
+
256
+ const john = Person.create({
257
+ name: "John",
258
+ age: 20,
211
259
  birthday,
212
- optionalDate: undefined,
213
- },
214
- { owner: me },
215
- );
216
- expect(map.optionalDate).toBeUndefined();
217
- });
260
+ });
218
261
 
219
- describe("Mutation", () => {
220
- test("assignment & deletion", () => {
221
- map.color = "blue";
222
- expect(map.color).toEqual("blue");
223
- expect(map._raw.get("color")).toEqual("blue");
224
- const newBirthday = new Date();
225
- map.birthday = newBirthday;
226
- expect(map.birthday).toEqual(newBirthday);
227
- expect(map._raw.get("birthday")).toEqual(newBirthday.toISOString());
228
-
229
- Object.assign(map, { color: "green", _height: 20 });
230
- expect(map.color).toEqual("green");
231
- expect(map._raw.get("color")).toEqual("green");
232
- expect(map._height).toEqual(20);
233
- expect(map._raw.get("_height")).toEqual(20);
234
-
235
- map.nullable = "not null";
236
- map.nullable = undefined;
237
- delete map.nullable;
238
- map.nullable = undefined;
239
-
240
- map.name = "Secret name";
241
- expect(map.name).toEqual("Secret name");
242
- map.name = undefined;
243
- expect(map.name).toEqual(undefined);
244
- expect(Object.keys(map)).toContain("name");
245
- delete map.name;
246
- expect(map.name).toEqual(undefined);
247
- expect(Object.keys(map)).not.toContain("name");
248
-
249
- expect(map._edits).toMatchObject({
250
- _height: {
251
- by: { id: me.id },
252
- value: 20,
253
- },
254
- birthday: {
255
- by: { id: me.id },
256
- value: newBirthday,
257
- },
258
- color: {
259
- by: { id: me.id },
260
- value: "green",
261
- all: [
262
- expect.objectContaining({
263
- by: { _type: "Account", id: me.id },
264
- value: "red",
265
- }),
266
- expect.objectContaining({
267
- by: { _type: "Account", id: me.id },
268
- value: "blue",
269
- }),
270
- expect.objectContaining({
271
- by: { _type: "Account", id: me.id },
272
- value: "green",
273
- }),
274
- ],
275
- },
276
- nullable: {
277
- by: { id: me.id },
278
- value: null,
279
- all: [
280
- expect.objectContaining({
281
- by: { _type: "Account", id: me.id },
282
- value: null,
283
- }),
284
- expect.objectContaining({
285
- by: { _type: "Account", id: me.id },
286
- value: "not null",
287
- }),
288
- expect.objectContaining({
289
- by: { _type: "Account", id: me.id },
290
- value: null,
291
- }),
292
- expect.objectContaining({
293
- by: { _type: "Account", id: me.id },
294
- value: undefined,
295
- }),
296
- expect.objectContaining({
297
- by: { _type: "Account", id: me.id },
298
- value: null,
299
- }),
300
- ],
301
- },
262
+ expect(john.toJSON()).toMatchObject({
263
+ name: "John",
264
+ age: 20,
265
+ birthday: birthday.toISOString(),
266
+ _type: "CoMap",
267
+ id: john.id,
302
268
  });
269
+ });
303
270
 
304
- expect(JSON.parse(JSON.stringify(map._edits))).toMatchObject({
305
- _height: {
306
- by: { id: me.id },
307
- value: 20,
308
- },
309
- birthday: {
310
- by: { id: me.id },
311
- value: newBirthday.toISOString(),
312
- },
313
- color: {
314
- by: { id: me.id },
315
- value: "green",
316
- all: [
317
- expect.objectContaining({
318
- by: { _type: "Account", id: me.id },
319
- value: "red",
320
- }),
321
- expect.objectContaining({
322
- by: { _type: "Account", id: me.id },
323
- value: "blue",
324
- }),
325
- expect.objectContaining({
326
- by: { _type: "Account", id: me.id },
327
- value: "green",
328
- }),
329
- ],
330
- },
331
- nullable: {
332
- by: { id: me.id },
333
- value: null,
334
- },
271
+ test("setting optional date as undefined should not throw", () => {
272
+ class Person extends CoMap {
273
+ name = co.string;
274
+ age = co.number;
275
+ birthday = co.optional.Date;
276
+ }
277
+
278
+ const john = Person.create({
279
+ name: "John",
280
+ age: 20,
281
+ });
282
+
283
+ expect(john.toJSON()).toMatchObject({
284
+ name: "John",
285
+ age: 20,
335
286
  });
336
287
  });
337
- });
338
288
 
339
- describe("property existence", () => {
340
- class TestMap extends CoMap.Record(co.string) {}
341
- test("CoMap", () => {
342
- const map = TestMap.create(
343
- { name: "test" },
344
- {
345
- owner: me,
346
- },
347
- );
289
+ it("should disallow extra properties", () => {
290
+ class Person extends CoMap {
291
+ name = co.string;
292
+ age = co.number;
293
+ }
348
294
 
349
- expect("name" in map).toBe(true);
350
- expect("something" in map).toBe(false);
295
+ // @ts-expect-error - x is not a valid property
296
+ const john = Person.create({ name: "John", age: 30, x: 1 });
297
+
298
+ expect(john.toJSON()).toEqual({
299
+ _type: "CoMap",
300
+ id: john.id,
301
+ name: "John",
302
+ age: 30,
303
+ });
351
304
  });
352
305
  });
353
306
 
354
- class RecursiveMap extends CoMap {
355
- name = co.string;
356
- next?: co<RecursiveMap | null> = co.ref(RecursiveMap);
357
- }
307
+ describe("Mutation", () => {
308
+ test("change a primitive value", () => {
309
+ class Person extends CoMap {
310
+ name = co.string;
311
+ age = co.number;
312
+ }
313
+
314
+ const john = Person.create({ name: "John", age: 20 });
315
+
316
+ john.name = "Jane";
317
+
318
+ expect(john.name).toEqual("Jane");
319
+ expect(john.age).toEqual(20);
320
+ });
321
+
322
+ test("delete an optional value", () => {
323
+ class Person extends CoMap {
324
+ name = co.string;
325
+ age = co.optional.number;
326
+ }
327
+
328
+ const john = Person.create({ name: "John", age: 20 });
329
+
330
+ delete john.age;
331
+
332
+ expect(john.name).toEqual("John");
333
+ expect(john.age).toEqual(undefined);
358
334
 
359
- const recursiveMap = RecursiveMap.create(
360
- {
361
- name: "first",
362
- next: RecursiveMap.create(
335
+ expect(john.toJSON()).toEqual({
336
+ _type: "CoMap",
337
+ id: john.id,
338
+ name: "John",
339
+ });
340
+ });
341
+
342
+ test("update a reference", () => {
343
+ class Dog extends CoMap {
344
+ name = co.string;
345
+ }
346
+
347
+ class Person extends CoMap {
348
+ name = co.string;
349
+ age = co.number;
350
+ dog = co.ref(Dog);
351
+ }
352
+
353
+ const john = Person.create({
354
+ name: "John",
355
+ age: 20,
356
+ dog: Dog.create({ name: "Rex" }),
357
+ });
358
+
359
+ john.dog = Dog.create({ name: "Fido" });
360
+
361
+ expect(john.dog?.name).toEqual("Fido");
362
+ });
363
+
364
+ test("changes should be listed in _edits", () => {
365
+ class Person extends CoMap {
366
+ name = co.string;
367
+ age = co.number;
368
+ }
369
+
370
+ const john = Person.create({ name: "John", age: 20 });
371
+
372
+ const me = Account.getMe();
373
+
374
+ john.age = 21;
375
+
376
+ expect(john._edits.age.all).toEqual([
377
+ {
378
+ by: expect.objectContaining({ _type: "Account", id: me.id }),
379
+ value: 20,
380
+ key: "age",
381
+ ref: undefined,
382
+ madeAt: expect.any(Date),
383
+ },
363
384
  {
364
- name: "second",
365
- next: RecursiveMap.create(
366
- {
367
- name: "third",
368
- },
369
- { owner: me },
370
- ),
385
+ by: expect.objectContaining({ _type: "Account", id: me.id }),
386
+ value: 21,
387
+ key: "age",
388
+ ref: undefined,
389
+ madeAt: expect.any(Date),
371
390
  },
372
- { owner: me },
373
- ),
374
- },
375
- { owner: me },
376
- );
377
-
378
- describe("Recursive CoMap", () => {
379
- test("Construction", () => {
380
- expect(recursiveMap.name).toEqual("first");
381
- expect(recursiveMap.next?.name).toEqual("second");
382
- expect(recursiveMap.next?.next?.name).toEqual("third");
391
+ ]);
383
392
  });
384
393
  });
385
394
 
386
- class MapWithEnumOfMaps extends CoMap {
387
- name = co.string;
388
- child = co.ref<typeof ChildA | typeof ChildB>((raw) =>
389
- raw.get("type") === "a" ? ChildA : ChildB,
390
- );
391
- }
395
+ test("Enum of maps", () => {
396
+ class MapWithEnumOfMaps extends CoMap {
397
+ name = co.string;
398
+ child = co.ref<typeof ChildA | typeof ChildB>((raw) =>
399
+ raw.get("type") === "a" ? ChildA : ChildB,
400
+ );
401
+ }
392
402
 
393
- class ChildA extends CoMap {
394
- type = co.literal("a");
395
- value = co.number;
396
- }
403
+ class ChildA extends CoMap {
404
+ type = co.literal("a");
405
+ value = co.number;
406
+ }
397
407
 
398
- class ChildB extends CoMap {
399
- type = co.literal("b");
400
- value = co.string;
401
- }
408
+ class ChildB extends CoMap {
409
+ type = co.literal("b");
410
+ value = co.string;
411
+ }
402
412
 
403
- const mapWithEnum = MapWithEnumOfMaps.create(
404
- {
413
+ const mapWithEnum = MapWithEnumOfMaps.create({
405
414
  name: "enum",
406
- child: ChildA.create(
407
- {
408
- type: "a",
409
- value: 5,
410
- },
411
- { owner: me },
412
- ),
413
- },
414
- { owner: me },
415
- );
415
+ child: ChildA.create({
416
+ type: "a",
417
+ value: 5,
418
+ }),
419
+ });
416
420
 
417
- test("Enum of maps", () => {
418
421
  expect(mapWithEnum.name).toEqual("enum");
419
422
  expect(mapWithEnum.child?.type).toEqual("a");
420
423
  expect(mapWithEnum.child?.value).toEqual(5);
@@ -423,393 +426,464 @@ describe("Simple CoMap operations", async () => {
423
426
  });
424
427
 
425
428
  describe("CoMap resolution", async () => {
426
- class TwiceNestedMap extends CoMap {
427
- taste = co.string;
428
- }
429
-
430
- class NestedMap extends CoMap {
431
- name = co.string;
432
- twiceNested = co.ref(TwiceNestedMap);
429
+ test("loading a locally available map with deep resolve", async () => {
430
+ class Dog extends CoMap {
431
+ name = co.string;
432
+ breed = co.string;
433
+ }
433
434
 
434
- get _fancyName() {
435
- return "Sir " + this.name;
435
+ class Person extends CoMap {
436
+ name = co.string;
437
+ age = co.number;
438
+ dog = co.ref(Dog);
436
439
  }
437
- }
438
440
 
439
- class TestMap extends CoMap {
440
- color = co.string;
441
- height = co.number;
442
- nested = co.ref(NestedMap);
441
+ const person = Person.create({
442
+ name: "John",
443
+ age: 20,
444
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
445
+ });
446
+
447
+ const loadedPerson = await Person.load(person.id, {
448
+ resolve: {
449
+ dog: true,
450
+ },
451
+ });
452
+
453
+ assert(loadedPerson);
454
+ expect(loadedPerson.dog.name).toEqual("Rex");
455
+ });
456
+
457
+ test("loading a locally available map using autoload for the refs", async () => {
458
+ class Dog extends CoMap {
459
+ name = co.string;
460
+ breed = co.string;
461
+ }
443
462
 
444
- get _roughColor() {
445
- return this.color + "ish";
463
+ class Person extends CoMap {
464
+ name = co.string;
465
+ age = co.number;
466
+ dog = co.ref(Dog);
446
467
  }
447
- }
448
468
 
449
- const initNodeAndMap = async () => {
450
- const me = await Account.create({
451
- creationProps: { name: "Hermes Puggington" },
452
- crypto: Crypto,
469
+ const person = Person.create({
470
+ name: "John",
471
+ age: 20,
472
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
453
473
  });
454
474
 
455
- const map = TestMap.create(
475
+ const loadedPerson = await Person.load(person.id);
476
+
477
+ assert(loadedPerson);
478
+ expect(loadedPerson.dog?.name).toEqual("Rex");
479
+ });
480
+
481
+ test("loading a remotely available map with deep resolve", async () => {
482
+ class Dog extends CoMap {
483
+ name = co.string;
484
+ breed = co.string;
485
+ }
486
+
487
+ class Person extends CoMap {
488
+ name = co.string;
489
+ age = co.number;
490
+ dog = co.ref(Dog);
491
+ }
492
+
493
+ const group = Group.create();
494
+ group.addMember("everyone", "writer");
495
+
496
+ const person = Person.create(
456
497
  {
457
- color: "red",
458
- height: 10,
459
- nested: NestedMap.create(
460
- {
461
- name: "nested",
462
- twiceNested: TwiceNestedMap.create(
463
- { taste: "sour" },
464
- { owner: me },
465
- ),
466
- },
467
- { owner: me },
468
- ),
498
+ name: "John",
499
+ age: 20,
500
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
469
501
  },
470
- { owner: me },
502
+ group,
471
503
  );
472
504
 
473
- return { me, map };
474
- };
505
+ const userB = await createJazzTestAccount();
475
506
 
476
- test("Construction", async () => {
477
- const { map } = await initNodeAndMap();
478
-
479
- // const test: Schema.Schema.To<typeof NestedMap>
507
+ const loadedPerson = await Person.load(person.id, {
508
+ resolve: {
509
+ dog: true,
510
+ },
511
+ loadAs: userB,
512
+ });
480
513
 
481
- expect(map.color).toEqual("red");
482
- expect(map._roughColor).toEqual("redish");
483
- expect(map.height).toEqual(10);
484
- expect(map.nested?.name).toEqual("nested");
485
- expect(map.nested?._fancyName).toEqual("Sir nested");
486
- expect(map.nested?.id).toBeDefined();
487
- expect(map.nested?.twiceNested?.taste).toEqual("sour");
514
+ assert(loadedPerson);
515
+ expect(loadedPerson.dog.name).toEqual("Rex");
488
516
  });
489
517
 
490
- test("Loading and availability", async () => {
491
- const { me, map } = await initNodeAndMap();
492
- const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
493
- peer1role: "server",
494
- peer2role: "client",
495
- });
518
+ test("loading a remotely available map using autoload for the refs", async () => {
519
+ class Dog extends CoMap {
520
+ name = co.string;
521
+ breed = co.string;
522
+ }
496
523
 
497
- if (!isControlledAccount(me)) {
498
- throw "me is not a controlled account";
524
+ class Person extends CoMap {
525
+ name = co.string;
526
+ age = co.number;
527
+ dog = co.ref(Dog);
499
528
  }
500
- me._raw.core.node.syncManager.addPeer(secondPeer);
501
- const { account: meOnSecondPeer } =
502
- await createJazzContextFromExistingCredentials({
503
- credentials: {
504
- accountID: me.id,
505
- secret: me._raw.agentSecret,
506
- },
507
- sessionProvider: randomSessionProvider,
508
- peersToLoadFrom: [initialAsPeer],
509
- crypto: Crypto,
510
- });
511
529
 
512
- const loadedMap = await TestMap.load(map.id, { loadAs: meOnSecondPeer });
530
+ const group = Group.create();
531
+ group.addMember("everyone", "writer");
513
532
 
514
- expect(loadedMap?.color).toEqual("red");
515
- expect(loadedMap?.height).toEqual(10);
516
- expect(loadedMap?.nested).toEqual(null);
517
- expect(loadedMap?._refs.nested?.id).toEqual(map.nested?.id);
518
- expect(loadedMap?._refs.nested?.value).toEqual(null);
533
+ const person = Person.create(
534
+ {
535
+ name: "John",
536
+ age: 20,
537
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
538
+ },
539
+ group,
540
+ );
519
541
 
520
- const loadedNestedMap = await NestedMap.load(map.nested!.id, {
521
- loadAs: meOnSecondPeer,
542
+ const userB = await createJazzTestAccount();
543
+ const loadedPerson = await Person.load(person.id, {
544
+ loadAs: userB,
522
545
  });
523
546
 
524
- expect(loadedMap?.nested?.name).toEqual("nested");
525
- expect(loadedMap?.nested?._fancyName).toEqual("Sir nested");
526
- expect(loadedMap?._refs.nested?.value).toEqual(loadedNestedMap);
527
- expect(loadedMap?.nested?.twiceNested?.taste).toEqual(undefined);
547
+ assert(loadedPerson);
548
+ expect(loadedPerson.dog).toBe(null);
528
549
 
529
- const loadedTwiceNestedMap = await TwiceNestedMap.load(
530
- map.nested!.twiceNested!.id,
531
- { loadAs: meOnSecondPeer },
532
- );
550
+ await waitFor(() => expect(loadedPerson.dog).toBeTruthy());
533
551
 
534
- expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sour");
535
- expect(loadedMap?.nested?._refs.twiceNested?.value).toEqual(
536
- loadedTwiceNestedMap,
537
- );
552
+ expect(loadedPerson.dog?.name).toEqual("Rex");
553
+ });
554
+
555
+ test("accessing the value refs", async () => {
556
+ class Dog extends CoMap {
557
+ name = co.string;
558
+ breed = co.string;
559
+ }
560
+
561
+ class Person extends CoMap {
562
+ name = co.string;
563
+ age = co.number;
564
+ dog = co.ref(Dog);
565
+ }
538
566
 
539
- const otherNestedMap = NestedMap.create(
567
+ const group = Group.create();
568
+ group.addMember("everyone", "writer");
569
+
570
+ const person = Person.create(
540
571
  {
541
- name: "otherNested",
542
- twiceNested: TwiceNestedMap.create(
543
- { taste: "sweet" },
544
- { owner: meOnSecondPeer },
545
- ),
572
+ name: "John",
573
+ age: 20,
574
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
546
575
  },
547
- { owner: meOnSecondPeer },
576
+ group,
548
577
  );
549
578
 
550
- loadedMap!.nested = otherNestedMap;
551
- expect(loadedMap?.nested?.name).toEqual("otherNested");
552
- expect(loadedMap?._refs.nested?.id).toEqual(otherNestedMap.id);
553
- expect(loadedMap?._refs.nested?.value).toEqual(otherNestedMap);
554
- expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sweet");
555
- expect(loadedMap?.nested?._refs.twiceNested?.value).toBeDefined();
579
+ const userB = await createJazzTestAccount();
580
+ const loadedPerson = await Person.load(person.id, {
581
+ loadAs: userB,
582
+ });
583
+
584
+ assert(loadedPerson);
585
+
586
+ expect(loadedPerson._refs.dog.id).toBe(person.dog!.id);
587
+
588
+ const dog = await loadedPerson._refs.dog.load();
589
+
590
+ assert(dog);
591
+
592
+ expect(dog.name).toEqual("Rex");
556
593
  });
557
594
 
558
- async function setupTest() {
559
- const { me, map } = await initNodeAndMap();
595
+ test("subscription on a locally available map with deep resolve", async () => {
596
+ class Dog extends CoMap {
597
+ name = co.string;
598
+ breed = co.string;
599
+ }
600
+
601
+ class Person extends CoMap {
602
+ name = co.string;
603
+ age = co.number;
604
+ dog = co.ref(Dog);
605
+ }
560
606
 
561
- const [initialAsPeer, secondAsPeer] = connectedPeers("initial", "second", {
562
- peer1role: "server",
563
- peer2role: "client",
607
+ const person = Person.create({
608
+ name: "John",
609
+ age: 20,
610
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
564
611
  });
565
612
 
566
- if (!isControlledAccount(me)) {
567
- throw "me is not a controlled account";
568
- }
569
- me._raw.core.node.syncManager.addPeer(secondAsPeer);
570
- const { account: meOnSecondPeer } =
571
- await createJazzContextFromExistingCredentials({
572
- credentials: {
573
- accountID: me.id,
574
- secret: me._raw.agentSecret,
613
+ const updates: Resolved<Person, { dog: true }>[] = [];
614
+ const spy = vi.fn((person) => updates.push(person));
615
+
616
+ Person.subscribe(
617
+ person.id,
618
+ {
619
+ resolve: {
620
+ dog: true,
575
621
  },
576
- sessionProvider: randomSessionProvider,
577
- peersToLoadFrom: [initialAsPeer],
578
- crypto: Crypto,
579
- });
622
+ },
623
+ spy,
624
+ );
580
625
 
581
- const queue = new cojsonInternals.Channel<TestMap>();
626
+ expect(spy).not.toHaveBeenCalled();
582
627
 
583
- await meOnSecondPeer.waitForAllCoValuesSync();
628
+ await waitFor(() => expect(spy).toHaveBeenCalled());
584
629
 
585
- TestMap.subscribe(map.id, { loadAs: meOnSecondPeer }, (subscribedMap) => {
586
- // Read to property to trigger loading
587
- subscribedMap.nested?.twiceNested?.taste;
588
- void queue.push(subscribedMap);
589
- });
630
+ expect(spy).toHaveBeenCalledTimes(1);
590
631
 
591
- return { me, map, meOnSecondPeer, queue };
592
- }
632
+ expect(updates[0]?.dog.name).toEqual("Rex");
633
+
634
+ person.dog!.name = "Fido";
593
635
 
594
- test("initial subscription loads nested data progressively", async () => {
595
- const { queue } = await setupTest();
636
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
596
637
 
597
- const update1 = (await queue.next()).value;
598
- expect(update1.nested).toEqual(null);
638
+ expect(updates[1]?.dog.name).toEqual("Fido");
599
639
 
600
- const update2 = (await queue.next()).value;
601
- expect(update2.nested?.name).toEqual("nested");
640
+ expect(spy).toHaveBeenCalledTimes(2);
602
641
  });
603
642
 
604
- test("updates to nested properties are received", async () => {
605
- const { map, queue } = await setupTest();
643
+ test("subscription on a locally available map with autoload", async () => {
644
+ class Dog extends CoMap {
645
+ name = co.string;
646
+ breed = co.string;
647
+ }
606
648
 
607
- // Skip initial updates
608
- await queue.next();
609
- await queue.next();
649
+ class Person extends CoMap {
650
+ name = co.string;
651
+ age = co.number;
652
+ dog = co.ref(Dog);
653
+ }
610
654
 
611
- map.nested!.name = "nestedUpdated";
655
+ const person = Person.create({
656
+ name: "John",
657
+ age: 20,
658
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
659
+ });
612
660
 
613
- await queue.next(); // Skip intermediate update
614
- const update3 = (await queue.next()).value;
615
- expect(update3.nested?.name).toEqual("nestedUpdated");
661
+ const updates: Person[] = [];
662
+ const spy = vi.fn((person) => updates.push(person));
616
663
 
617
- const oldTwiceNested = update3.nested!.twiceNested;
618
- expect(oldTwiceNested?.taste).toEqual("sour");
619
- });
664
+ Person.subscribe(person.id, {}, spy);
620
665
 
621
- test("replacing nested object triggers updates", async () => {
622
- const { meOnSecondPeer, queue } = await setupTest();
666
+ expect(spy).not.toHaveBeenCalled();
623
667
 
624
- // Skip initial updates
625
- await queue.next();
626
- await queue.next();
668
+ await waitFor(() => expect(spy).toHaveBeenCalled());
627
669
 
628
- const update3 = (await queue.next()).value;
670
+ expect(spy).toHaveBeenCalledTimes(1);
629
671
 
630
- const newTwiceNested = TwiceNestedMap.create(
631
- {
632
- taste: "sweet",
633
- },
634
- { owner: meOnSecondPeer },
635
- );
672
+ expect(updates[0]?.dog?.name).toEqual("Rex");
636
673
 
637
- const newNested = NestedMap.create(
638
- {
639
- name: "newNested",
640
- twiceNested: newTwiceNested,
641
- },
642
- { owner: meOnSecondPeer },
643
- );
674
+ person.dog!.name = "Fido";
644
675
 
645
- update3.nested = newNested;
676
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
646
677
 
647
- await queue.next(); // Skip intermediate update
648
- const update4 = (await queue.next()).value;
678
+ expect(updates[1]?.dog?.name).toEqual("Fido");
649
679
 
650
- expect(update4.nested?.name).toEqual("newNested");
651
- expect(update4.nested?.twiceNested?.taste).toEqual("sweet");
680
+ expect(spy).toHaveBeenCalledTimes(2);
652
681
  });
653
682
 
654
- test("updates to deeply nested properties are received", async () => {
655
- const { queue } = await setupTest();
683
+ test("subscription on a locally available map with syncResolution", async () => {
684
+ class Dog extends CoMap {
685
+ name = co.string;
686
+ breed = co.string;
687
+ }
656
688
 
657
- // Skip to the point where we have the nested object
658
- await queue.next();
659
- await queue.next();
660
- const update3 = (await queue.next()).value;
689
+ class Person extends CoMap {
690
+ name = co.string;
691
+ age = co.number;
692
+ dog = co.ref(Dog);
693
+ }
661
694
 
662
- const newTwiceNested = TwiceNestedMap.create(
663
- { taste: "sweet" },
664
- { owner: update3.nested!._raw.owner },
665
- );
695
+ const person = Person.create({
696
+ name: "John",
697
+ age: 20,
698
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
699
+ });
700
+
701
+ const updates: Person[] = [];
702
+ const spy = vi.fn((person) => updates.push(person));
666
703
 
667
- const newNested = NestedMap.create(
704
+ subscribeToCoValue(
705
+ Person,
706
+ person.id,
668
707
  {
669
- name: "newNested",
670
- twiceNested: newTwiceNested,
708
+ syncResolution: true,
709
+ loadAs: Account.getMe(),
671
710
  },
672
- { owner: update3.nested!._raw.owner },
711
+ spy,
673
712
  );
674
713
 
675
- update3.nested = newNested;
714
+ expect(spy).toHaveBeenCalled();
715
+ expect(spy).toHaveBeenCalledTimes(1);
716
+
717
+ expect(updates[0]?.dog?.name).toEqual("Rex");
718
+
719
+ expect(spy).toHaveBeenCalledTimes(1);
720
+
721
+ person.dog!.name = "Fido";
676
722
 
677
- // Skip intermediate updates
678
- await queue.next();
679
- await queue.next();
723
+ expect(spy).toHaveBeenCalledTimes(2);
680
724
 
681
- newTwiceNested.taste = "salty";
682
- const update5 = (await queue.next()).value;
683
- expect(update5.nested?.twiceNested?.taste).toEqual("salty");
725
+ expect(updates[1]?.dog?.name).toEqual("Fido");
684
726
 
685
- newTwiceNested.taste = "umami";
686
- const update6 = (await queue.next()).value;
687
- expect(update6.nested?.twiceNested?.taste).toEqual("umami");
727
+ expect(spy).toHaveBeenCalledTimes(2);
688
728
  });
689
729
 
690
- class TestMapWithOptionalRef extends CoMap {
691
- color = co.string;
692
- nested = co.optional.ref(NestedMap);
693
- }
730
+ test("subscription on a remotely available map with deep resolve", async () => {
731
+ class Dog extends CoMap {
732
+ name = co.string;
733
+ breed = co.string;
734
+ }
694
735
 
695
- test("Construction with optional", async () => {
696
- const me = await Account.create({
697
- creationProps: { name: "Hermes Puggington" },
698
- crypto: Crypto,
699
- });
736
+ class Person extends CoMap {
737
+ name = co.string;
738
+ age = co.number;
739
+ dog = co.ref(Dog);
740
+ }
741
+
742
+ const group = Group.create();
743
+ group.addMember("everyone", "writer");
700
744
 
701
- const mapWithout = TestMapWithOptionalRef.create(
745
+ const person = Person.create(
702
746
  {
703
- color: "red",
747
+ name: "John",
748
+ age: 20,
749
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
704
750
  },
705
- { owner: me },
751
+ group,
706
752
  );
707
753
 
708
- expect(mapWithout.color).toEqual("red");
709
- expect(mapWithout.nested).toEqual(undefined);
754
+ const userB = await createJazzTestAccount();
710
755
 
711
- const mapWith = TestMapWithOptionalRef.create(
756
+ const updates: Resolved<Person, { dog: true }>[] = [];
757
+ const spy = vi.fn((person) => updates.push(person));
758
+
759
+ Person.subscribe(
760
+ person.id,
712
761
  {
713
- color: "red",
714
- nested: NestedMap.create(
715
- {
716
- name: "wow!",
717
- twiceNested: TwiceNestedMap.create(
718
- { taste: "sour" },
719
- { owner: me },
720
- ),
721
- },
722
- { owner: me },
723
- ),
762
+ resolve: {
763
+ dog: true,
764
+ },
765
+ loadAs: userB,
724
766
  },
725
- { owner: me },
767
+ spy,
726
768
  );
727
769
 
728
- expect(mapWith.color).toEqual("red");
729
- expect(mapWith.nested?.name).toEqual("wow!");
730
- expect(mapWith.nested?._fancyName).toEqual("Sir wow!");
731
- expect(mapWith.nested?._raw).toBeDefined();
770
+ expect(spy).not.toHaveBeenCalled();
771
+
772
+ await waitFor(() => expect(spy).toHaveBeenCalled());
773
+
774
+ expect(spy).toHaveBeenCalledTimes(1);
775
+
776
+ expect(updates[0]?.dog.name).toEqual("Rex");
777
+
778
+ person.dog!.name = "Fido";
779
+
780
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
781
+
782
+ expect(updates[1]?.dog.name).toEqual("Fido");
783
+
784
+ expect(spy).toHaveBeenCalledTimes(2);
732
785
  });
733
786
 
734
- // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
735
- class TestRecord extends CoMap {
736
- [co.items] = co.number;
737
- }
738
- // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
739
- interface TestRecord extends Record<string, number> {}
787
+ test("subscription on a remotely available map with autoload", async () => {
788
+ class Dog extends CoMap {
789
+ name = co.string;
790
+ breed = co.string;
791
+ }
740
792
 
741
- test("Construction with index signature", async () => {
742
- const me = await Account.create({
743
- creationProps: { name: "Hermes Puggington" },
744
- crypto: Crypto,
745
- });
793
+ class Person extends CoMap {
794
+ name = co.string;
795
+ age = co.number;
796
+ dog = co.ref(Dog);
797
+ }
798
+
799
+ const group = Group.create();
800
+ group.addMember("everyone", "writer");
746
801
 
747
- const record = TestRecord.create(
802
+ const person = Person.create(
748
803
  {
749
- height: 5,
750
- other: 3,
804
+ name: "John",
805
+ age: 20,
806
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
751
807
  },
752
- { owner: me },
808
+ group,
753
809
  );
754
810
 
755
- expect(record.height).toEqual(5);
756
- expect(record._raw.get("height")).toEqual(5);
757
- expect(record.other).toEqual(3);
758
- expect(record._raw.get("other")).toEqual(3);
759
- expect(Object.keys(record)).toEqual(["height", "other"]);
760
- expect(record.toJSON()).toMatchObject({
761
- _type: "CoMap",
762
- height: 5,
763
- id: expect.any(String),
764
- other: 3,
765
- });
766
- });
811
+ const updates: Person[] = [];
812
+ const spy = vi.fn((person) => updates.push(person));
767
813
 
768
- class TestRecord2 extends CoMap.Record(co.number) {}
814
+ const userB = await createJazzTestAccount();
769
815
 
770
- test("Construction with index signature (shorthand)", async () => {
771
- const me = await Account.create({
772
- creationProps: { name: "Hermes Puggington" },
773
- crypto: Crypto,
774
- });
775
-
776
- const record = TestRecord2.create(
816
+ Person.subscribe(
817
+ person.id,
777
818
  {
778
- height: 5,
779
- other: 3,
819
+ loadAs: userB,
780
820
  },
781
- { owner: me },
821
+ spy,
782
822
  );
783
823
 
784
- expect(record.height).toEqual(5);
785
- expect(record._raw.get("height")).toEqual(5);
786
- expect(record.other).toEqual(3);
787
- expect(record._raw.get("other")).toEqual(3);
788
- expect(Object.keys(record)).toEqual(["height", "other"]);
824
+ expect(spy).not.toHaveBeenCalled();
825
+
826
+ await waitFor(() => expect(spy).toHaveBeenCalled());
827
+
828
+ expect(spy).toHaveBeenCalledTimes(1);
829
+
830
+ expect(updates[0]?.dog?.name).toEqual("Rex");
831
+
832
+ person.dog!.name = "Fido";
833
+
834
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
835
+
836
+ expect(updates[1]?.dog?.name).toEqual("Fido");
837
+
838
+ expect(spy).toHaveBeenCalledTimes(2);
789
839
  });
790
840
 
791
- class TestRecordRef extends CoMap.Record(co.ref(TwiceNestedMap)) {}
841
+ test("replacing nested object triggers updates", async () => {
842
+ class Dog extends CoMap {
843
+ name = co.string;
844
+ breed = co.string;
845
+ }
792
846
 
793
- test("Construction with index signature ref", async () => {
794
- const me = await Account.create({
795
- creationProps: { name: "Hermes Puggington" },
796
- crypto: Crypto,
847
+ class Person extends CoMap {
848
+ name = co.string;
849
+ age = co.number;
850
+ dog = co.ref(Dog);
851
+ }
852
+
853
+ const person = Person.create({
854
+ name: "John",
855
+ age: 20,
856
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }),
797
857
  });
798
858
 
799
- const record = TestRecordRef.create(
859
+ const updates: Resolved<Person, { dog: true }>[] = [];
860
+ const spy = vi.fn((person) => updates.push(person));
861
+
862
+ Person.subscribe(
863
+ person.id,
800
864
  {
801
- firstNested: TwiceNestedMap.create({ taste: "sour" }, { owner: me }),
802
- secondNested: TwiceNestedMap.create({ taste: "sweet" }, { owner: me }),
865
+ resolve: {
866
+ dog: true,
867
+ },
803
868
  },
804
- { owner: me },
869
+ spy,
805
870
  );
806
871
 
807
- expect(record.firstNested?.taste).toEqual("sour");
808
- expect(record.firstNested?.id).toBeDefined();
809
- expect(record.secondNested?.taste).toEqual("sweet");
810
- expect(record.secondNested?.id).toBeDefined();
811
- expect(Object.keys(record)).toEqual(["firstNested", "secondNested"]);
812
- expect(Object.keys(record._refs)).toEqual(["firstNested", "secondNested"]);
872
+ expect(spy).not.toHaveBeenCalled();
873
+
874
+ await waitFor(() => expect(spy).toHaveBeenCalled());
875
+
876
+ expect(spy).toHaveBeenCalledTimes(1);
877
+
878
+ expect(updates[0]?.dog.name).toEqual("Rex");
879
+
880
+ person.dog!.name = "Fido";
881
+
882
+ await waitFor(() => expect(spy).toHaveBeenCalledTimes(2));
883
+
884
+ expect(updates[1]?.dog.name).toEqual("Fido");
885
+
886
+ expect(spy).toHaveBeenCalledTimes(2);
813
887
  });
814
888
  });
815
889
 
@@ -1134,16 +1208,16 @@ describe("CoMap Typescript validation", async () => {
1134
1208
 
1135
1209
  describe("Creating and finding unique CoMaps", async () => {
1136
1210
  test("Creating and finding unique CoMaps", async () => {
1137
- const me = await Account.create({
1138
- creationProps: { name: "Tester McTesterson" },
1139
- crypto: Crypto,
1140
- });
1211
+ const group = Group.create();
1141
1212
 
1142
- const group = await Group.create({
1143
- owner: me,
1144
- });
1213
+ class Person extends CoMap {
1214
+ name = co.string;
1215
+ _height = co.number;
1216
+ birthday = co.Date;
1217
+ color = co.string;
1218
+ }
1145
1219
 
1146
- const alice = TestMap.create(
1220
+ const alice = Person.create(
1147
1221
  {
1148
1222
  name: "Alice",
1149
1223
  _height: 100,
@@ -1153,8 +1227,7 @@ describe("Creating and finding unique CoMaps", async () => {
1153
1227
  { owner: group, unique: { name: "Alice" } },
1154
1228
  );
1155
1229
 
1156
- const foundAlice = TestMap.findUnique({ name: "Alice" }, group.id, me);
1157
-
1230
+ const foundAlice = Person.findUnique({ name: "Alice" }, group.id);
1158
1231
  expect(foundAlice).toEqual(alice.id);
1159
1232
  });
1160
1233
  });