jazz-tools 0.7.0-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. package/.eslintrc.cjs +24 -0
  2. package/.turbo/turbo-build.log +24 -0
  3. package/CHANGELOG.md +42 -0
  4. package/LICENSE.txt +19 -0
  5. package/README.md +3 -0
  6. package/dist/coValueInterfaces.js +8 -0
  7. package/dist/coValueInterfaces.js.map +1 -0
  8. package/dist/coValues/account/account.js +11 -0
  9. package/dist/coValues/account/account.js.map +1 -0
  10. package/dist/coValues/account/accountOf.js +150 -0
  11. package/dist/coValues/account/accountOf.js.map +1 -0
  12. package/dist/coValues/account/migration.js +4 -0
  13. package/dist/coValues/account/migration.js.map +1 -0
  14. package/dist/coValues/coList/coList.js +2 -0
  15. package/dist/coValues/coList/coList.js.map +1 -0
  16. package/dist/coValues/coList/coListOf.js +235 -0
  17. package/dist/coValues/coList/coListOf.js.map +1 -0
  18. package/dist/coValues/coList/internalDocs.js +2 -0
  19. package/dist/coValues/coList/internalDocs.js.map +1 -0
  20. package/dist/coValues/coMap/coMap.js +2 -0
  21. package/dist/coValues/coMap/coMap.js.map +1 -0
  22. package/dist/coValues/coMap/coMapOf.js +262 -0
  23. package/dist/coValues/coMap/coMapOf.js.map +1 -0
  24. package/dist/coValues/coMap/internalDocs.js +2 -0
  25. package/dist/coValues/coMap/internalDocs.js.map +1 -0
  26. package/dist/coValues/coStream/coStream.js +2 -0
  27. package/dist/coValues/coStream/coStream.js.map +1 -0
  28. package/dist/coValues/coStream/coStreamOf.js +244 -0
  29. package/dist/coValues/coStream/coStreamOf.js.map +1 -0
  30. package/dist/coValues/construction.js +34 -0
  31. package/dist/coValues/construction.js.map +1 -0
  32. package/dist/coValues/extensions/imageDef.js +36 -0
  33. package/dist/coValues/extensions/imageDef.js.map +1 -0
  34. package/dist/coValues/group/group.js +2 -0
  35. package/dist/coValues/group/group.js.map +1 -0
  36. package/dist/coValues/group/groupOf.js +109 -0
  37. package/dist/coValues/group/groupOf.js.map +1 -0
  38. package/dist/coValues/resolution.js +66 -0
  39. package/dist/coValues/resolution.js.map +1 -0
  40. package/dist/errors.js +2 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/index.js +31 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/refs.js +95 -0
  45. package/dist/refs.js.map +1 -0
  46. package/dist/schemaHelpers.js +14 -0
  47. package/dist/schemaHelpers.js.map +1 -0
  48. package/dist/subscriptionScope.js +81 -0
  49. package/dist/subscriptionScope.js.map +1 -0
  50. package/dist/tests/coList.test.js +207 -0
  51. package/dist/tests/coList.test.js.map +1 -0
  52. package/dist/tests/coMap.test.js +238 -0
  53. package/dist/tests/coMap.test.js.map +1 -0
  54. package/dist/tests/coStream.test.js +263 -0
  55. package/dist/tests/coStream.test.js.map +1 -0
  56. package/dist/tests/types.test.js +33 -0
  57. package/dist/tests/types.test.js.map +1 -0
  58. package/package.json +23 -0
  59. package/src/coValueInterfaces.ts +105 -0
  60. package/src/coValues/account/account.ts +106 -0
  61. package/src/coValues/account/accountOf.ts +284 -0
  62. package/src/coValues/account/migration.ts +12 -0
  63. package/src/coValues/coList/coList.ts +57 -0
  64. package/src/coValues/coList/coListOf.ts +377 -0
  65. package/src/coValues/coList/internalDocs.ts +1 -0
  66. package/src/coValues/coMap/coMap.ts +110 -0
  67. package/src/coValues/coMap/coMapOf.ts +451 -0
  68. package/src/coValues/coMap/internalDocs.ts +1 -0
  69. package/src/coValues/coStream/coStream.ts +63 -0
  70. package/src/coValues/coStream/coStreamOf.ts +404 -0
  71. package/src/coValues/construction.ts +110 -0
  72. package/src/coValues/extensions/imageDef.ts +51 -0
  73. package/src/coValues/group/group.ts +27 -0
  74. package/src/coValues/group/groupOf.ts +183 -0
  75. package/src/coValues/resolution.ts +111 -0
  76. package/src/errors.ts +1 -0
  77. package/src/index.ts +68 -0
  78. package/src/refs.ts +128 -0
  79. package/src/schemaHelpers.ts +72 -0
  80. package/src/subscriptionScope.ts +118 -0
  81. package/src/tests/coList.test.ts +283 -0
  82. package/src/tests/coMap.test.ts +357 -0
  83. package/src/tests/coStream.test.ts +415 -0
  84. package/src/tests/types.test.ts +37 -0
  85. package/tsconfig.json +15 -0
@@ -0,0 +1,283 @@
1
+ import { expect, describe, test, beforeEach } from "vitest";
2
+
3
+ import { webcrypto } from "node:crypto";
4
+ import { connectedPeers } from "cojson/src/streamUtils.js";
5
+ import { newRandomSessionID } from "cojson/src/coValueCore.js";
6
+ import { Effect, Queue } from "effect";
7
+ import { Co, S, Account, jazzReady } from "..";
8
+
9
+ if (!("crypto" in globalThis)) {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ (globalThis as any).crypto = webcrypto;
12
+ }
13
+
14
+ beforeEach(async () => {
15
+ await jazzReady;
16
+ });
17
+
18
+ describe("Simple CoList operations", async () => {
19
+ const me = await Account.create({
20
+ name: "Hermes Puggington",
21
+ });
22
+
23
+ class TestList extends Co.list(S.string).as<TestList>() {}
24
+
25
+ const list = new TestList(["bread", "butter", "onion"], { owner: me });
26
+
27
+ test("Construction", () => {
28
+ expect(list[0]).toBe("bread");
29
+ expect(list[1]).toBe("butter");
30
+ expect(list[2]).toBe("onion");
31
+ expect(list._raw.asArray()).toEqual(["bread", "butter", "onion"]);
32
+ expect(list.length).toBe(3);
33
+ expect(list.map((item) => item.toUpperCase())).toEqual([
34
+ "BREAD",
35
+ "BUTTER",
36
+ "ONION",
37
+ ]);
38
+ });
39
+
40
+ describe("Mutation", () => {
41
+ test("assignment", () => {
42
+ const list = new TestList(["bread", "butter", "onion"], {
43
+ owner: me,
44
+ });
45
+ list[1] = "margarine";
46
+ expect(list._raw.asArray()).toEqual([
47
+ "bread",
48
+ "margarine",
49
+ "onion",
50
+ ]);
51
+ expect(list[1]).toBe("margarine");
52
+ });
53
+
54
+ test("push", () => {
55
+ const list = new TestList(["bread", "butter", "onion"], {
56
+ owner: me,
57
+ });
58
+ list.push("cheese");
59
+ expect(list[3]).toBe("cheese");
60
+ expect(list._raw.asArray()).toEqual([
61
+ "bread",
62
+ "butter",
63
+ "onion",
64
+ "cheese",
65
+ ]);
66
+ });
67
+
68
+ test("unshift", () => {
69
+ const list = new TestList(["bread", "butter", "onion"], {
70
+ owner: me,
71
+ });
72
+ list.unshift("lettuce");
73
+ expect(list[0]).toBe("lettuce");
74
+ expect(list._raw.asArray()).toEqual([
75
+ "lettuce",
76
+ "bread",
77
+ "butter",
78
+ "onion",
79
+ ]);
80
+ });
81
+
82
+ test("pop", () => {
83
+ const list = new TestList(["bread", "butter", "onion"], {
84
+ owner: me,
85
+ });
86
+ expect(list.pop()).toBe("onion");
87
+ expect(list.length).toBe(2);
88
+ expect(list._raw.asArray()).toEqual(["bread", "butter"]);
89
+ });
90
+
91
+ test("shift", () => {
92
+ const list = new TestList(["bread", "butter", "onion"], {
93
+ owner: me,
94
+ });
95
+ expect(list.shift()).toBe("bread");
96
+ expect(list.length).toBe(2);
97
+ expect(list._raw.asArray()).toEqual(["butter", "onion"]);
98
+ });
99
+
100
+ test("splice", () => {
101
+ const list = new TestList(["bread", "butter", "onion"], {
102
+ owner: me,
103
+ });
104
+ list.splice(1, 1, "salt", "pepper");
105
+ expect(list.length).toBe(4);
106
+ expect(list._raw.asArray()).toEqual([
107
+ "bread",
108
+ "salt",
109
+ "pepper",
110
+ "onion",
111
+ ]);
112
+ });
113
+ });
114
+ });
115
+
116
+ describe("CoList resolution", async () => {
117
+ class TwiceNestedList extends Co.list(S.string).as<TwiceNestedList>() {
118
+ joined() {
119
+ return this.join(",");
120
+ }
121
+ }
122
+
123
+ class NestedList extends Co.list(TwiceNestedList).as<NestedList>() {}
124
+
125
+ class TestList extends Co.list(NestedList).as<TestList>() {}
126
+
127
+ const initNodeAndList = async () => {
128
+ const me = await Account.create({
129
+ name: "Hermes Puggington",
130
+ });
131
+
132
+ const list = new TestList(
133
+ [
134
+ new NestedList(
135
+ [new TwiceNestedList(["a", "b"], { owner: me })],
136
+ { owner: me }
137
+ ),
138
+ new NestedList(
139
+ [new TwiceNestedList(["c", "d"], { owner: me })],
140
+ { owner: me }
141
+ ),
142
+ ],
143
+ { owner: me }
144
+ );
145
+
146
+ return { me, list };
147
+ };
148
+
149
+ test("Construction", async () => {
150
+ const { list } = await initNodeAndList();
151
+
152
+ expect(list[0]?.[0]?.[0]).toBe("a");
153
+ expect(list[0]?.[0]?.joined()).toBe("a,b");
154
+ expect(list[0]?.[0]?.id).toBeDefined();
155
+ expect(list[1]?.[0]?.[0]).toBe("c");
156
+ });
157
+
158
+ test("Loading and availability", async () => {
159
+ const { me, list } = await initNodeAndList();
160
+
161
+ const [initialAsPeer, secondPeer] = connectedPeers(
162
+ "initial",
163
+ "second",
164
+ { peer1role: "server", peer2role: "client" }
165
+ );
166
+ me._raw.core.node.syncManager.addPeer(secondPeer);
167
+ const meOnSecondPeer = await Account.become({
168
+ accountID: me.id,
169
+ accountSecret: me._raw.agentSecret,
170
+ peersToLoadFrom: [initialAsPeer],
171
+ sessionID: newRandomSessionID(me.id as any),
172
+ });
173
+
174
+ const loadedList = await TestList.load(list.id, { as: meOnSecondPeer });
175
+
176
+ expect(loadedList?.[0]).toBe(undefined);
177
+ expect(loadedList?._refs[0]?.id).toEqual(list[0]!.id);
178
+
179
+ const loadedNestedList = await NestedList.load(list[0]!.id, {
180
+ as: meOnSecondPeer,
181
+ });
182
+
183
+ expect(loadedList?.[0]).toBeDefined();
184
+ expect(loadedList?.[0]?.[0]).toBeUndefined();
185
+ expect(loadedList?.[0]?._refs[0]?.id).toEqual(list[0]![0]!.id);
186
+ expect(loadedList?._refs[0]?.value).toEqual(loadedNestedList);
187
+
188
+ const loadedTwiceNestedList = await TwiceNestedList.load(
189
+ list[0]![0]!.id,
190
+ { as: meOnSecondPeer }
191
+ );
192
+
193
+ expect(loadedList?.[0]?.[0]).toBeDefined();
194
+ expect(loadedList?.[0]?.[0]?.[0]).toBe("a");
195
+ expect(loadedList?.[0]?.[0]?.joined()).toBe("a,b");
196
+ expect(loadedList?.[0]?._refs[0]?.id).toEqual(list[0]?.[0]?.id);
197
+ expect(loadedList?.[0]?._refs[0]?.value).toEqual(
198
+ loadedTwiceNestedList
199
+ );
200
+
201
+ const otherNestedList = new NestedList(
202
+ [new TwiceNestedList(["e", "f"], { owner: meOnSecondPeer })],
203
+ { owner: meOnSecondPeer }
204
+ );
205
+
206
+ loadedList![0] = otherNestedList;
207
+ expect(loadedList?.[0]).toEqual(otherNestedList);
208
+ expect(loadedList?._refs[0]?.id).toEqual(otherNestedList.id);
209
+ });
210
+
211
+ test("Subscription & auto-resolution", async () => {
212
+ const { me, list } = await initNodeAndList();
213
+
214
+ const [initialAsPeer, secondPeer] = connectedPeers(
215
+ "initial",
216
+ "second",
217
+ { peer1role: "server", peer2role: "client" }
218
+ );
219
+ me._raw.core.node.syncManager.addPeer(secondPeer);
220
+ const meOnSecondPeer = await Account.become({
221
+ accountID: me.id,
222
+ accountSecret: me._raw.agentSecret,
223
+ peersToLoadFrom: [initialAsPeer],
224
+ sessionID: newRandomSessionID(me.id as any),
225
+ });
226
+
227
+ await Effect.runPromise(
228
+ Effect.gen(function* ($) {
229
+ const queue = yield* $(Queue.unbounded<TestList>());
230
+
231
+ TestList.subscribe(
232
+ list.id,
233
+ { as: meOnSecondPeer },
234
+ (subscribedList) => {
235
+ console.log(
236
+ "subscribedList?.[0]?.[0]?.[0]",
237
+ subscribedList?.[0]?.[0]?.[0]
238
+ );
239
+ Effect.runPromise(Queue.offer(queue, subscribedList));
240
+ }
241
+ );
242
+
243
+ const update1 = yield* $(Queue.take(queue));
244
+ expect(update1?.[0]).toEqual(undefined);
245
+
246
+ const update2 = yield* $(Queue.take(queue));
247
+ expect(update2?.[0]).toBeDefined();
248
+ expect(update2?.[0]?.[0]).toBeUndefined();
249
+
250
+ const update3 = yield* $(Queue.take(queue));
251
+ expect(update3?.[0]?.[0]).toBeDefined();
252
+ expect(update3?.[0]?.[0]?.[0]).toBe("a");
253
+ expect(update3?.[0]?.[0]?.joined()).toBe("a,b");
254
+
255
+ update3[0]![0]![0] = "x";
256
+
257
+ const update4 = yield* $(Queue.take(queue));
258
+ expect(update4?.[0]?.[0]?.[0]).toBe("x");
259
+
260
+ // When assigning a new nested value, we get an update
261
+
262
+ const newTwiceNestedList = new TwiceNestedList(["y", "z"], {
263
+ owner: meOnSecondPeer,
264
+ });
265
+
266
+ const newNestedList = new NestedList([newTwiceNestedList], {
267
+ owner: meOnSecondPeer,
268
+ });
269
+
270
+ update4[0] = newNestedList;
271
+
272
+ const update5 = yield* $(Queue.take(queue));
273
+ expect(update5?.[0]?.[0]?.[0]).toBe("y");
274
+ expect(update5?.[0]?.[0]?.joined()).toBe("y,z");
275
+
276
+ // we get updates when the new nested value changes
277
+ newTwiceNestedList[0] = "w";
278
+ const update6 = yield* $(Queue.take(queue));
279
+ expect(update6?.[0]?.[0]?.[0]).toBe("w");
280
+ })
281
+ );
282
+ });
283
+ });
@@ -0,0 +1,357 @@
1
+ import { expect, describe, test, beforeEach } from "vitest";
2
+
3
+ import { webcrypto } from "node:crypto";
4
+ import { connectedPeers } from "cojson/src/streamUtils.js";
5
+ import { newRandomSessionID } from "cojson/src/coValueCore.js";
6
+ import { Effect, Queue } from "effect";
7
+ import { Co, S, Account, jazzReady } from "..";
8
+
9
+ if (!("crypto" in globalThis)) {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ (globalThis as any).crypto = webcrypto;
12
+ }
13
+
14
+ beforeEach(async () => {
15
+ await jazzReady;
16
+ });
17
+
18
+ describe("Simple CoMap operations", async () => {
19
+ const me = await Account.create({
20
+ name: "Hermes Puggington",
21
+ });
22
+
23
+ class TestMap extends Co.map({
24
+ color: S.string,
25
+ height: S.number,
26
+ birthday: S.Date,
27
+ name: S.optional(S.string),
28
+ }).as<TestMap>() {
29
+ get roughColor() {
30
+ return this.color + "ish";
31
+ }
32
+ }
33
+
34
+ const birthday = new Date();
35
+
36
+ const map = new TestMap(
37
+ {
38
+ color: "red",
39
+ height: 10,
40
+ birthday: birthday,
41
+ },
42
+ { owner: me }
43
+ );
44
+
45
+ test("Construction", () => {
46
+ expect(map.color).toEqual("red");
47
+ expect(map.roughColor).toEqual("redish");
48
+ expect(map.height).toEqual(10);
49
+ expect(map.birthday).toEqual(birthday);
50
+ expect(map._raw.get("birthday")).toEqual(birthday.toISOString());
51
+ });
52
+
53
+ describe("Mutation", () => {
54
+ test("assignment", () => {
55
+ map.color = "blue";
56
+ expect(map.color).toEqual("blue");
57
+ expect(map._raw.get("color")).toEqual("blue");
58
+ const newBirthday = new Date();
59
+ map.birthday = newBirthday;
60
+ expect(map.birthday).toEqual(newBirthday);
61
+ expect(map._raw.get("birthday")).toEqual(
62
+ newBirthday.toISOString()
63
+ );
64
+
65
+ Object.assign(map, { color: "green", height: 20 });
66
+ expect(map.color).toEqual("green");
67
+ expect(map._raw.get("color")).toEqual("green");
68
+ expect(map.height).toEqual(20);
69
+ expect(map._raw.get("height")).toEqual(20);
70
+
71
+ map.name = "Secret name";
72
+ expect(map.name).toEqual("Secret name");
73
+ delete map.name;
74
+ expect(map.name).toEqual(undefined);
75
+ });
76
+ });
77
+ });
78
+
79
+ describe("CoMap resolution", async () => {
80
+ class TwiceNestedMap extends Co.map({
81
+ taste: S.string,
82
+ }).as<TwiceNestedMap>() {}
83
+
84
+ class NestedMap extends Co.map({
85
+ name: S.string,
86
+ twiceNested: TwiceNestedMap,
87
+ }).as<NestedMap>() {
88
+ get fancyName() {
89
+ return "Sir " + this.name;
90
+ }
91
+ }
92
+
93
+ class TestMap extends Co.map({
94
+ color: S.string,
95
+ height: S.number,
96
+ nested: NestedMap,
97
+ }).as<TestMap>() {
98
+ get roughColor() {
99
+ return this.color + "ish";
100
+ }
101
+ }
102
+
103
+ const initNodeAndMap = async () => {
104
+ const me = await Account.create({
105
+ name: "Hermes Puggington",
106
+ });
107
+
108
+ const map = new TestMap(
109
+ {
110
+ color: "red",
111
+ height: 10,
112
+ nested: new NestedMap(
113
+ {
114
+ name: "nested",
115
+ twiceNested: new TwiceNestedMap(
116
+ { taste: "sour" },
117
+ { owner: me }
118
+ ),
119
+ },
120
+ { owner: me }
121
+ ),
122
+ },
123
+ { owner: me }
124
+ );
125
+
126
+ return { me, map };
127
+ };
128
+
129
+ test("Construction", async () => {
130
+ const { map } = await initNodeAndMap();
131
+
132
+ // const test: Schema.Schema.To<typeof NestedMap>
133
+
134
+ expect(map.color).toEqual("red");
135
+ expect(map.roughColor).toEqual("redish");
136
+ expect(map.height).toEqual(10);
137
+ expect(map.nested?.name).toEqual("nested");
138
+ expect(map.nested?.fancyName).toEqual("Sir nested");
139
+ expect(map.nested?.id).toBeDefined();
140
+ expect(map.nested?.twiceNested?.taste).toEqual("sour");
141
+ });
142
+
143
+ test("Loading and availability", async () => {
144
+ const { me, map } = await initNodeAndMap();
145
+ const [initialAsPeer, secondPeer] = connectedPeers(
146
+ "initial",
147
+ "second",
148
+ { peer1role: "server", peer2role: "client" }
149
+ );
150
+ me._raw.core.node.syncManager.addPeer(secondPeer);
151
+ const meOnSecondPeer = await Account.become({
152
+ accountID: me.id,
153
+ accountSecret: me._raw.agentSecret,
154
+ peersToLoadFrom: [initialAsPeer],
155
+ sessionID: newRandomSessionID(me.id as any),
156
+ });
157
+
158
+ const loadedMap = await TestMap.load(map.id, { as: meOnSecondPeer });
159
+
160
+ expect(loadedMap?.color).toEqual("red");
161
+ expect(loadedMap?.height).toEqual(10);
162
+ expect(loadedMap?.nested).toEqual(undefined);
163
+ expect(loadedMap?._refs.nested?.id).toEqual(map.nested?.id);
164
+ expect(loadedMap?._refs.nested?.value).toEqual(undefined);
165
+
166
+ const loadedNestedMap = await NestedMap.load(map.nested!.id, {
167
+ as: meOnSecondPeer,
168
+ });
169
+
170
+ expect(loadedMap?.nested?.name).toEqual("nested");
171
+ expect(loadedMap?.nested.fancyName).toEqual("Sir nested");
172
+ expect(loadedMap?._refs.nested?.value).toEqual(loadedNestedMap);
173
+ expect(loadedMap?.nested?.twiceNested?.taste).toEqual(undefined);
174
+
175
+ const loadedTwiceNestedMap = await TwiceNestedMap.load(
176
+ map.nested!.twiceNested!.id,
177
+ { as: meOnSecondPeer }
178
+ );
179
+
180
+ expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sour");
181
+ expect(loadedMap?.nested?._refs.twiceNested?.value).toEqual(
182
+ loadedTwiceNestedMap
183
+ );
184
+
185
+ const otherNestedMap = new NestedMap(
186
+ {
187
+ name: "otherNested",
188
+ twiceNested: new TwiceNestedMap(
189
+ { taste: "sweet" },
190
+ { owner: meOnSecondPeer }
191
+ ),
192
+ },
193
+ { owner: meOnSecondPeer }
194
+ );
195
+
196
+ loadedMap!.nested = otherNestedMap;
197
+ expect(loadedMap?.nested?.name).toEqual("otherNested");
198
+ expect(loadedMap?._refs.nested?.id).toEqual(otherNestedMap.id);
199
+ expect(loadedMap?._refs.nested?.value).toEqual(otherNestedMap);
200
+ expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sweet");
201
+ expect(loadedMap?.nested?._refs.twiceNested?.value).toBeDefined();
202
+ });
203
+
204
+ test("Subscription & auto-resolution", async () => {
205
+ const { me, map } = await initNodeAndMap();
206
+
207
+ const [initialAsPeer, secondAsPeer] = connectedPeers(
208
+ "initial",
209
+ "second",
210
+ { peer1role: "server", peer2role: "client" }
211
+ );
212
+
213
+ me._raw.core.node.syncManager.addPeer(secondAsPeer);
214
+
215
+ const meOnSecondPeer = await Account.become({
216
+ accountID: me.id,
217
+ accountSecret: me._raw.agentSecret,
218
+ peersToLoadFrom: [initialAsPeer],
219
+ sessionID: newRandomSessionID(me.id as any),
220
+ });
221
+
222
+ await Effect.runPromise(
223
+ Effect.gen(function* ($) {
224
+ const queue = yield* $(Queue.unbounded<TestMap>());
225
+
226
+ TestMap.subscribe(
227
+ map.id,
228
+ { as: meOnSecondPeer },
229
+ (subscribedMap) => {
230
+ console.log(
231
+ "subscribedMap.nested?.twiceNested?.taste",
232
+ subscribedMap.nested?.twiceNested?.taste
233
+ );
234
+ Effect.runPromise(Queue.offer(queue, subscribedMap));
235
+ }
236
+ );
237
+
238
+ const update1 = yield* $(Queue.take(queue));
239
+ expect(update1.nested).toEqual(undefined);
240
+
241
+ const update2 = yield* $(Queue.take(queue));
242
+ expect(update2.nested?.name).toEqual("nested");
243
+
244
+ map.nested!.name = "nestedUpdated";
245
+
246
+ const _ = yield* $(Queue.take(queue));
247
+ const update3 = yield* $(Queue.take(queue));
248
+ expect(update3.nested?.name).toEqual("nestedUpdated");
249
+
250
+ const oldTwiceNested = update3.nested!.twiceNested;
251
+ expect(oldTwiceNested?.taste).toEqual("sour");
252
+
253
+ // When assigning a new nested value, we get an update
254
+ const newTwiceNested = new TwiceNestedMap(
255
+ {
256
+ taste: "sweet",
257
+ },
258
+ { owner: meOnSecondPeer }
259
+ );
260
+
261
+ const newNested = new NestedMap(
262
+ {
263
+ name: "newNested",
264
+ twiceNested: newTwiceNested,
265
+ },
266
+ { owner: meOnSecondPeer }
267
+ );
268
+
269
+ update3.nested = newNested;
270
+
271
+ yield* $(Queue.take(queue));
272
+ // const update4 = yield* $(Queue.take(queue));
273
+ const update4b = yield* $(Queue.take(queue));
274
+
275
+ expect(update4b.nested?.name).toEqual("newNested");
276
+ expect(update4b.nested?.twiceNested?.taste).toEqual("sweet");
277
+
278
+ // we get updates when the new nested value changes
279
+ newTwiceNested.taste = "salty";
280
+ const update5 = yield* $(Queue.take(queue));
281
+ expect(update5.nested?.twiceNested?.taste).toEqual("salty");
282
+
283
+ newTwiceNested.taste = "umami";
284
+ const update6 = yield* $(Queue.take(queue));
285
+ expect(update6.nested?.twiceNested?.taste).toEqual("umami");
286
+ })
287
+ );
288
+ });
289
+
290
+ class TestMapWithOptionalRef extends Co.map({
291
+ color: S.string,
292
+ nested: S.optional(NestedMap),
293
+ }).as<TestMapWithOptionalRef>() {}
294
+
295
+ test("Construction with optional", async () => {
296
+ const me = await Account.create({
297
+ name: "Hermes Puggington",
298
+ });
299
+
300
+ const mapWithout = new TestMapWithOptionalRef(
301
+ {
302
+ color: "red",
303
+ },
304
+ { owner: me }
305
+ );
306
+
307
+ expect(mapWithout.color).toEqual("red");
308
+ expect(mapWithout.nested).toEqual(undefined);
309
+
310
+ const mapWith = new TestMapWithOptionalRef(
311
+ {
312
+ color: "red",
313
+ nested: new NestedMap(
314
+ {
315
+ name: "wow!",
316
+ twiceNested: new TwiceNestedMap(
317
+ { taste: "sour" },
318
+ { owner: me }
319
+ ),
320
+ },
321
+ { owner: me }
322
+ ),
323
+ },
324
+ { owner: me }
325
+ );
326
+
327
+ expect(mapWith.color).toEqual("red");
328
+ expect(mapWith.nested?.name).toEqual("wow!");
329
+ expect(mapWith.nested?.fancyName).toEqual("Sir wow!");
330
+ expect(mapWith.nested?._raw).toBeDefined();
331
+ });
332
+
333
+ class TestRecord extends Co.map(
334
+ { color: S.string },
335
+ { key: S.string, value: S.string }
336
+ ).as<TestRecord>() {}
337
+
338
+ test("Construction with index signature", async () => {
339
+ const me = await Account.create({
340
+ name: "Hermes Puggington",
341
+ });
342
+
343
+ const record = new TestRecord(
344
+ {
345
+ color: "red",
346
+ other: "wild",
347
+ },
348
+ { owner: me }
349
+ );
350
+
351
+ expect(record.color).toEqual("red");
352
+ expect(record._raw.get("color")).toEqual("red");
353
+ expect(record.other).toEqual("wild");
354
+ expect(record._raw.get("other")).toEqual("wild");
355
+ expect(Object.keys(record)).toEqual(["color", "other"]);
356
+ });
357
+ });