cojson 0.7.11 → 0.7.14

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.
@@ -1,500 +1,376 @@
1
1
  import { expect, test } from "vitest";
2
- import { newRandomSessionID } from "../coValueCore.js";
3
2
  import { LocalNode } from "../localNode.js";
4
3
  import { SyncMessage } from "../sync.js";
5
- import { expectMap } from "../coValue.js";
6
4
  import { MapOpPayload } from "../coValues/coMap.js";
7
5
  import { RawGroup } from "../coValues/group.js";
8
- import {
9
- randomAnonymousAccountAndSessionID,
10
- shouldNotResolve,
11
- } from "./testUtils.js";
12
- import { connectedPeers, newStreamPair } from "../streamUtils.js";
6
+ import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
7
+ import { connectedPeers, newQueuePair } from "../streamUtils.js";
13
8
  import { AccountID } from "../coValues/account.js";
14
9
  import { stableStringify } from "../jsonStringify.js";
15
10
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
11
+ import { Effect, Queue, Sink, Stream } from "effect";
12
+ import { newRandomSessionID } from "../coValueCore.js";
13
+ import { expectMap } from "../coValue.js";
16
14
 
17
15
  const Crypto = await WasmCrypto.create();
18
16
 
19
- test("Node replies with initial tx and header to empty subscribe", async () => {
20
- const [admin, session] = randomAnonymousAccountAndSessionID();
21
- const node = new LocalNode(admin, session, Crypto);
22
-
23
- const group = node.createGroup();
24
-
25
- const map = group.createMap();
17
+ test("Node replies with initial tx and header to empty subscribe", () =>
18
+ Effect.gen(function* () {
19
+ const [admin, session] = randomAnonymousAccountAndSessionID();
20
+ const node = new LocalNode(admin, session, Crypto);
26
21
 
27
- map.set("hello", "world", "trusting");
22
+ const group = node.createGroup();
28
23
 
29
- const [inRx, inTx] = newStreamPair<SyncMessage>();
30
- const [outRx, outTx] = newStreamPair<SyncMessage>();
24
+ const map = group.createMap();
31
25
 
32
- node.syncManager.addPeer({
33
- id: "test",
34
- incoming: inRx,
35
- outgoing: outTx,
36
- role: "peer",
37
- });
26
+ map.set("hello", "world", "trusting");
38
27
 
39
- const writer = inTx.getWriter();
28
+ const [inRx, inTx] = yield* newQueuePair();
29
+ const [outRx, outTx] = yield* newQueuePair();
30
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
31
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
40
32
 
41
- await writer.write({
42
- action: "load",
43
- id: map.core.id,
44
- header: false,
45
- sessions: {},
46
- });
33
+ node.syncManager.addPeer({
34
+ id: "test",
35
+ incoming: inRx,
36
+ outgoing: outTx,
37
+ role: "peer",
38
+ });
47
39
 
48
- const reader = outRx.getReader();
40
+ yield* Queue.offer(inTx, {
41
+ action: "load",
42
+ id: map.core.id,
43
+ header: false,
44
+ sessions: {},
45
+ });
49
46
 
50
- // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
51
- expect((await reader.read()).value).toMatchObject(groupStateEx(group));
47
+ // expect((yield* Queue.take(outRxQ))).toMatchObject(admStateEx(admin.id));
48
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
52
49
 
53
- const mapTellKnownStateMsg = await reader.read();
54
- expect(mapTellKnownStateMsg.value).toEqual({
55
- action: "known",
56
- ...map.core.knownState(),
57
- } satisfies SyncMessage);
50
+ const mapTellKnownStateMsg = yield* Queue.take(outRxQ);
51
+ expect(mapTellKnownStateMsg).toEqual({
52
+ action: "known",
53
+ ...map.core.knownState(),
54
+ } satisfies SyncMessage);
58
55
 
59
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
60
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
56
+ // expect((yield * Queue.take(outRxQ))).toMatchObject(admContEx(admin.id));
57
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
61
58
 
62
- const newContentMsg = await reader.read();
59
+ const newContentMsg = yield* Queue.take(outRxQ);
63
60
 
64
- expect(newContentMsg.value).toEqual({
65
- action: "content",
66
- id: map.core.id,
67
- header: {
68
- type: "comap",
69
- ruleset: { type: "ownedByGroup", group: group.id },
70
- meta: null,
71
- createdAt: map.core.header.createdAt,
72
- uniqueness: map.core.header.uniqueness,
73
- },
74
- new: {
75
- [node.currentSessionID]: {
76
- after: 0,
77
- newTransactions: [
78
- {
79
- privacy: "trusting" as const,
80
- madeAt: map.core.sessionLogs.get(node.currentSessionID)!
81
- .transactions[0]!.madeAt,
82
- changes: stableStringify([
83
- {
84
- op: "set",
85
- key: "hello",
86
- value: "world",
87
- } satisfies MapOpPayload<string, string>,
88
- ]),
89
- },
90
- ],
91
- lastSignature: map.core.sessionLogs.get(node.currentSessionID)!
92
- .lastSignature!,
61
+ expect(newContentMsg).toEqual({
62
+ action: "content",
63
+ id: map.core.id,
64
+ header: {
65
+ type: "comap",
66
+ ruleset: { type: "ownedByGroup", group: group.id },
67
+ meta: null,
68
+ createdAt: map.core.header.createdAt,
69
+ uniqueness: map.core.header.uniqueness,
93
70
  },
94
- },
95
- } satisfies SyncMessage);
96
- });
97
-
98
- test("Node replies with only new tx to subscribe with some known state", async () => {
99
- const [admin, session] = randomAnonymousAccountAndSessionID();
100
- const node = new LocalNode(admin, session, Crypto);
101
-
102
- const group = node.createGroup();
71
+ new: {
72
+ [node.currentSessionID]: {
73
+ after: 0,
74
+ newTransactions: [
75
+ {
76
+ privacy: "trusting" as const,
77
+ madeAt: map.core.sessionLogs.get(
78
+ node.currentSessionID,
79
+ )!.transactions[0]!.madeAt,
80
+ changes: stableStringify([
81
+ {
82
+ op: "set",
83
+ key: "hello",
84
+ value: "world",
85
+ } satisfies MapOpPayload<string, string>,
86
+ ]),
87
+ },
88
+ ],
89
+ lastSignature: map.core.sessionLogs.get(
90
+ node.currentSessionID,
91
+ )!.lastSignature!,
92
+ },
93
+ },
94
+ } satisfies SyncMessage);
95
+ }).pipe(Effect.scoped, Effect.runPromise));
103
96
 
104
- const map = group.createMap();
97
+ test("Node replies with only new tx to subscribe with some known state", () =>
98
+ Effect.gen(function* () {
99
+ const [admin, session] = randomAnonymousAccountAndSessionID();
100
+ const node = new LocalNode(admin, session, Crypto);
105
101
 
106
- map.set("hello", "world", "trusting");
107
- map.set("goodbye", "world", "trusting");
102
+ const group = node.createGroup();
108
103
 
109
- const [inRx, inTx] = newStreamPair<SyncMessage>();
110
- const [outRx, outTx] = newStreamPair<SyncMessage>();
104
+ const map = group.createMap();
111
105
 
112
- node.syncManager.addPeer({
113
- id: "test",
114
- incoming: inRx,
115
- outgoing: outTx,
116
- role: "peer",
117
- });
106
+ map.set("hello", "world", "trusting");
107
+ map.set("goodbye", "world", "trusting");
118
108
 
119
- const writer = inTx.getWriter();
109
+ const [inRx, inTx] = yield* newQueuePair();
110
+ const [outRx, outTx] = yield* newQueuePair();
111
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
112
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
120
113
 
121
- await writer.write({
122
- action: "load",
123
- id: map.core.id,
124
- header: true,
125
- sessions: {
126
- [node.currentSessionID]: 1,
127
- },
128
- });
114
+ node.syncManager.addPeer({
115
+ id: "test",
116
+ incoming: inRx,
117
+ outgoing: outTx,
118
+ role: "peer",
119
+ });
129
120
 
130
- const reader = outRx.getReader();
121
+ yield* Queue.offer(inTx, {
122
+ action: "load",
123
+ id: map.core.id,
124
+ header: true,
125
+ sessions: {
126
+ [node.currentSessionID]: 1,
127
+ },
128
+ });
131
129
 
132
- // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
133
- expect((await reader.read()).value).toMatchObject(groupStateEx(group));
130
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admStateEx(admin.id));
131
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
134
132
 
135
- const mapTellKnownStateMsg = await reader.read();
136
- expect(mapTellKnownStateMsg.value).toEqual({
137
- action: "known",
138
- ...map.core.knownState(),
139
- } satisfies SyncMessage);
133
+ const mapTellKnownStateMsg = yield* Queue.take(outRxQ);
134
+ expect(mapTellKnownStateMsg).toEqual({
135
+ action: "known",
136
+ ...map.core.knownState(),
137
+ } satisfies SyncMessage);
140
138
 
141
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
142
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
139
+ // expect(yield* Queue.take(outRxQ))).toMatchObject(admContEx(admin.id));
140
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
143
141
 
144
- const mapNewContentMsg = await reader.read();
142
+ const mapNewContentMsg = yield* Queue.take(outRxQ);
145
143
 
146
- expect(mapNewContentMsg.value).toEqual({
147
- action: "content",
148
- id: map.core.id,
149
- header: undefined,
150
- new: {
151
- [node.currentSessionID]: {
152
- after: 1,
153
- newTransactions: [
154
- {
155
- privacy: "trusting" as const,
156
- madeAt: map.core.sessionLogs.get(node.currentSessionID)!
157
- .transactions[1]!.madeAt,
158
- changes: stableStringify([
159
- {
160
- op: "set",
161
- key: "goodbye",
162
- value: "world",
163
- } satisfies MapOpPayload<string, string>,
164
- ]),
165
- },
166
- ],
167
- lastSignature: map.core.sessionLogs.get(node.currentSessionID)!
168
- .lastSignature!,
144
+ expect(mapNewContentMsg).toEqual({
145
+ action: "content",
146
+ id: map.core.id,
147
+ header: undefined,
148
+ new: {
149
+ [node.currentSessionID]: {
150
+ after: 1,
151
+ newTransactions: [
152
+ {
153
+ privacy: "trusting" as const,
154
+ madeAt: map.core.sessionLogs.get(
155
+ node.currentSessionID,
156
+ )!.transactions[1]!.madeAt,
157
+ changes: stableStringify([
158
+ {
159
+ op: "set",
160
+ key: "goodbye",
161
+ value: "world",
162
+ } satisfies MapOpPayload<string, string>,
163
+ ]),
164
+ },
165
+ ],
166
+ lastSignature: map.core.sessionLogs.get(
167
+ node.currentSessionID,
168
+ )!.lastSignature!,
169
+ },
169
170
  },
170
- },
171
- } satisfies SyncMessage);
172
- });
171
+ } satisfies SyncMessage);
172
+ }).pipe(Effect.scoped, Effect.runPromise));
173
173
 
174
174
  test.todo(
175
175
  "TODO: node only replies with new tx to subscribe with some known state, even in the depended on coValues",
176
176
  );
177
177
 
178
- test("After subscribing, node sends own known state and new txs to peer", async () => {
179
- const [admin, session] = randomAnonymousAccountAndSessionID();
180
- const node = new LocalNode(admin, session, Crypto);
181
-
182
- const group = node.createGroup();
183
-
184
- const map = group.createMap();
178
+ test("After subscribing, node sends own known state and new txs to peer", () =>
179
+ Effect.gen(function* () {
180
+ const [admin, session] = randomAnonymousAccountAndSessionID();
181
+ const node = new LocalNode(admin, session, Crypto);
185
182
 
186
- const [inRx, inTx] = newStreamPair<SyncMessage>();
187
- const [outRx, outTx] = newStreamPair<SyncMessage>();
183
+ const group = node.createGroup();
188
184
 
189
- node.syncManager.addPeer({
190
- id: "test",
191
- incoming: inRx,
192
- outgoing: outTx,
193
- role: "peer",
194
- });
185
+ const map = group.createMap();
195
186
 
196
- const writer = inTx.getWriter();
187
+ const [inRx, inTx] = yield* newQueuePair();
188
+ const [outRx, outTx] = yield* newQueuePair();
189
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
190
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
197
191
 
198
- await writer.write({
199
- action: "load",
200
- id: map.core.id,
201
- header: false,
202
- sessions: {
203
- [node.currentSessionID]: 0,
204
- },
205
- });
192
+ node.syncManager.addPeer({
193
+ id: "test",
194
+ incoming: inRx,
195
+ outgoing: outTx,
196
+ role: "peer",
197
+ });
206
198
 
207
- const reader = outRx.getReader();
199
+ yield* Queue.offer(inTx, {
200
+ action: "load",
201
+ id: map.core.id,
202
+ header: false,
203
+ sessions: {
204
+ [node.currentSessionID]: 0,
205
+ },
206
+ });
208
207
 
209
- // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
210
- expect((await reader.read()).value).toMatchObject(groupStateEx(group));
208
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admStateEx(admin.id));
209
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
211
210
 
212
- const mapTellKnownStateMsg = await reader.read();
213
- expect(mapTellKnownStateMsg.value).toEqual({
214
- action: "known",
215
- ...map.core.knownState(),
216
- } satisfies SyncMessage);
211
+ const mapTellKnownStateMsg = yield* Queue.take(outRxQ);
212
+ expect(mapTellKnownStateMsg).toEqual({
213
+ action: "known",
214
+ ...map.core.knownState(),
215
+ } satisfies SyncMessage);
217
216
 
218
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
219
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
217
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
218
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
220
219
 
221
- const mapNewContentHeaderOnlyMsg = await reader.read();
220
+ const mapNewContentHeaderOnlyMsg = yield* Queue.take(outRxQ);
222
221
 
223
- expect(mapNewContentHeaderOnlyMsg.value).toEqual({
224
- action: "content",
225
- id: map.core.id,
226
- header: map.core.header,
227
- new: {},
228
- } satisfies SyncMessage);
222
+ expect(mapNewContentHeaderOnlyMsg).toEqual({
223
+ action: "content",
224
+ id: map.core.id,
225
+ header: map.core.header,
226
+ new: {},
227
+ } satisfies SyncMessage);
229
228
 
230
- map.set("hello", "world", "trusting");
229
+ map.set("hello", "world", "trusting");
231
230
 
232
- const mapEditMsg1 = await reader.read();
231
+ const mapEditMsg1 = yield* Queue.take(outRxQ);
233
232
 
234
- expect(mapEditMsg1.value).toEqual({
235
- action: "content",
236
- id: map.core.id,
237
- new: {
238
- [node.currentSessionID]: {
239
- after: 0,
240
- newTransactions: [
241
- {
242
- privacy: "trusting" as const,
243
- madeAt: map.core.sessionLogs.get(node.currentSessionID)!
244
- .transactions[0]!.madeAt,
245
- changes: stableStringify([
246
- {
247
- op: "set",
248
- key: "hello",
249
- value: "world",
250
- } satisfies MapOpPayload<string, string>,
251
- ]),
252
- },
253
- ],
254
- lastSignature: map.core.sessionLogs.get(node.currentSessionID)!
255
- .lastSignature!,
233
+ expect(mapEditMsg1).toEqual({
234
+ action: "content",
235
+ id: map.core.id,
236
+ new: {
237
+ [node.currentSessionID]: {
238
+ after: 0,
239
+ newTransactions: [
240
+ {
241
+ privacy: "trusting" as const,
242
+ madeAt: map.core.sessionLogs.get(
243
+ node.currentSessionID,
244
+ )!.transactions[0]!.madeAt,
245
+ changes: stableStringify([
246
+ {
247
+ op: "set",
248
+ key: "hello",
249
+ value: "world",
250
+ } satisfies MapOpPayload<string, string>,
251
+ ]),
252
+ },
253
+ ],
254
+ lastSignature: map.core.sessionLogs.get(
255
+ node.currentSessionID,
256
+ )!.lastSignature!,
257
+ },
256
258
  },
257
- },
258
- } satisfies SyncMessage);
259
+ } satisfies SyncMessage);
259
260
 
260
- map.set("goodbye", "world", "trusting");
261
+ map.set("goodbye", "world", "trusting");
261
262
 
262
- const mapEditMsg2 = await reader.read();
263
+ const mapEditMsg2 = yield* Queue.take(outRxQ);
263
264
 
264
- expect(mapEditMsg2.value).toEqual({
265
- action: "content",
266
- id: map.core.id,
267
- new: {
268
- [node.currentSessionID]: {
269
- after: 1,
270
- newTransactions: [
271
- {
272
- privacy: "trusting" as const,
273
- madeAt: map.core.sessionLogs.get(node.currentSessionID)!
274
- .transactions[1]!.madeAt,
275
- changes: stableStringify([
276
- {
277
- op: "set",
278
- key: "goodbye",
279
- value: "world",
280
- } satisfies MapOpPayload<string, string>,
281
- ]),
282
- },
283
- ],
284
- lastSignature: map.core.sessionLogs.get(node.currentSessionID)!
285
- .lastSignature!,
265
+ expect(mapEditMsg2).toEqual({
266
+ action: "content",
267
+ id: map.core.id,
268
+ new: {
269
+ [node.currentSessionID]: {
270
+ after: 1,
271
+ newTransactions: [
272
+ {
273
+ privacy: "trusting" as const,
274
+ madeAt: map.core.sessionLogs.get(
275
+ node.currentSessionID,
276
+ )!.transactions[1]!.madeAt,
277
+ changes: stableStringify([
278
+ {
279
+ op: "set",
280
+ key: "goodbye",
281
+ value: "world",
282
+ } satisfies MapOpPayload<string, string>,
283
+ ]),
284
+ },
285
+ ],
286
+ lastSignature: map.core.sessionLogs.get(
287
+ node.currentSessionID,
288
+ )!.lastSignature!,
289
+ },
286
290
  },
287
- },
288
- } satisfies SyncMessage);
289
- });
290
-
291
- test("Client replies with known new content to tellKnownState from server", async () => {
292
- const [admin, session] = randomAnonymousAccountAndSessionID();
293
- const node = new LocalNode(admin, session, Crypto);
294
-
295
- const group = node.createGroup();
296
-
297
- const map = group.createMap();
298
-
299
- map.set("hello", "world", "trusting");
300
-
301
- const [inRx, inTx] = newStreamPair<SyncMessage>();
302
- const [outRx, outTx] = newStreamPair<SyncMessage>();
303
-
304
- node.syncManager.addPeer({
305
- id: "test",
306
- incoming: inRx,
307
- outgoing: outTx,
308
- role: "peer",
309
- });
310
-
311
- const reader = outRx.getReader();
291
+ } satisfies SyncMessage);
292
+ }).pipe(Effect.scoped, Effect.runPromise));
312
293
 
313
- // expect((await reader.read()).value).toMatchObject(groupStateEx(group));
294
+ test("Client replies with known new content to tellKnownState from server", () =>
295
+ Effect.gen(function* () {
296
+ const [admin, session] = randomAnonymousAccountAndSessionID();
297
+ const node = new LocalNode(admin, session, Crypto);
314
298
 
315
- const writer = inTx.getWriter();
299
+ const group = node.createGroup();
316
300
 
317
- await writer.write({
318
- action: "known",
319
- id: map.core.id,
320
- header: false,
321
- sessions: {
322
- [node.currentSessionID]: 0,
323
- },
324
- });
301
+ const map = group.createMap();
325
302
 
326
- // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
327
- expect((await reader.read()).value).toMatchObject(groupStateEx(group));
303
+ map.set("hello", "world", "trusting");
328
304
 
329
- const mapTellKnownStateMsg = await reader.read();
330
- expect(mapTellKnownStateMsg.value).toEqual({
331
- action: "known",
332
- ...map.core.knownState(),
333
- } satisfies SyncMessage);
305
+ const [inRx, inTx] = yield* newQueuePair();
306
+ const [outRx, outTx] = yield* newQueuePair();
307
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
308
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
334
309
 
335
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
336
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
310
+ node.syncManager.addPeer({
311
+ id: "test",
312
+ incoming: inRx,
313
+ outgoing: outTx,
314
+ role: "peer",
315
+ });
337
316
 
338
- const mapNewContentMsg = await reader.read();
317
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
339
318
 
340
- expect(mapNewContentMsg.value).toEqual({
341
- action: "content",
342
- id: map.core.id,
343
- header: map.core.header,
344
- new: {
345
- [node.currentSessionID]: {
346
- after: 0,
347
- newTransactions: [
348
- {
349
- privacy: "trusting" as const,
350
- madeAt: map.core.sessionLogs.get(node.currentSessionID)!
351
- .transactions[0]!.madeAt,
352
- changes: stableStringify([
353
- {
354
- op: "set",
355
- key: "hello",
356
- value: "world",
357
- } satisfies MapOpPayload<string, string>,
358
- ]),
359
- },
360
- ],
361
- lastSignature: map.core.sessionLogs.get(node.currentSessionID)!
362
- .lastSignature!,
319
+ yield* Queue.offer(inTx, {
320
+ action: "known",
321
+ id: map.core.id,
322
+ header: false,
323
+ sessions: {
324
+ [node.currentSessionID]: 0,
363
325
  },
364
- },
365
- } satisfies SyncMessage);
366
- });
367
-
368
- test("No matter the optimistic known state, node respects invalid known state messages and resyncs", async () => {
369
- const [admin, session] = randomAnonymousAccountAndSessionID();
370
- const node = new LocalNode(admin, session, Crypto);
371
-
372
- const group = node.createGroup();
373
-
374
- const map = group.createMap();
375
-
376
- const [inRx, inTx] = newStreamPair<SyncMessage>();
377
- const [outRx, outTx] = newStreamPair<SyncMessage>();
378
-
379
- node.syncManager.addPeer({
380
- id: "test",
381
- incoming: inRx,
382
- outgoing: outTx,
383
- role: "peer",
384
- });
385
-
386
- const writer = inTx.getWriter();
387
-
388
- await writer.write({
389
- action: "load",
390
- id: map.core.id,
391
- header: false,
392
- sessions: {
393
- [node.currentSessionID]: 0,
394
- },
395
- });
396
-
397
- const reader = outRx.getReader();
398
-
399
- // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
400
- expect((await reader.read()).value).toMatchObject(groupStateEx(group));
401
-
402
- const mapTellKnownStateMsg = await reader.read();
403
- expect(mapTellKnownStateMsg.value).toEqual({
404
- action: "known",
405
- ...map.core.knownState(),
406
- } satisfies SyncMessage);
407
-
408
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
409
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
410
-
411
- const mapNewContentHeaderOnlyMsg = await reader.read();
412
-
413
- expect(mapNewContentHeaderOnlyMsg.value).toEqual({
414
- action: "content",
415
- id: map.core.id,
416
- header: map.core.header,
417
- new: {},
418
- } satisfies SyncMessage);
419
-
420
- map.set("hello", "world", "trusting");
421
-
422
- map.set("goodbye", "world", "trusting");
326
+ });
423
327
 
424
- const _mapEditMsgs = await reader.read();
328
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admStateEx(admin.id));
329
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
425
330
 
426
- console.log("Sending correction");
331
+ const mapTellKnownStateMsg = yield* Queue.take(outRxQ);
332
+ expect(mapTellKnownStateMsg).toEqual({
333
+ action: "known",
334
+ ...map.core.knownState(),
335
+ } satisfies SyncMessage);
427
336
 
428
- await writer.write({
429
- action: "known",
430
- isCorrection: true,
431
- id: map.core.id,
432
- header: true,
433
- sessions: {
434
- [node.currentSessionID]: 1,
435
- },
436
- } satisfies SyncMessage);
337
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
338
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
437
339
 
438
- const newContentAfterWrongAssumedState = await reader.read();
340
+ const mapNewContentMsg = yield* Queue.take(outRxQ);
439
341
 
440
- expect(newContentAfterWrongAssumedState.value).toEqual({
441
- action: "content",
442
- id: map.core.id,
443
- header: undefined,
444
- new: {
445
- [node.currentSessionID]: {
446
- after: 1,
447
- newTransactions: [
448
- {
449
- privacy: "trusting" as const,
450
- madeAt: map.core.sessionLogs.get(node.currentSessionID)!
451
- .transactions[1]!.madeAt,
452
- changes: stableStringify([
453
- {
454
- op: "set",
455
- key: "goodbye",
456
- value: "world",
457
- } satisfies MapOpPayload<string, string>,
458
- ]),
459
- },
460
- ],
461
- lastSignature: map.core.sessionLogs.get(node.currentSessionID)!
462
- .lastSignature!,
342
+ expect(mapNewContentMsg).toEqual({
343
+ action: "content",
344
+ id: map.core.id,
345
+ header: map.core.header,
346
+ new: {
347
+ [node.currentSessionID]: {
348
+ after: 0,
349
+ newTransactions: [
350
+ {
351
+ privacy: "trusting" as const,
352
+ madeAt: map.core.sessionLogs.get(
353
+ node.currentSessionID,
354
+ )!.transactions[0]!.madeAt,
355
+ changes: stableStringify([
356
+ {
357
+ op: "set",
358
+ key: "hello",
359
+ value: "world",
360
+ } satisfies MapOpPayload<string, string>,
361
+ ]),
362
+ },
363
+ ],
364
+ lastSignature: map.core.sessionLogs.get(
365
+ node.currentSessionID,
366
+ )!.lastSignature!,
367
+ },
463
368
  },
464
- },
465
- } satisfies SyncMessage);
466
- });
467
-
468
- test("If we add a peer, but it never subscribes to a coValue, it won't get any messages", async () => {
469
- const [admin, session] = randomAnonymousAccountAndSessionID();
470
- const node = new LocalNode(admin, session, Crypto);
471
-
472
- const group = node.createGroup();
473
-
474
- const map = group.createMap();
475
-
476
- const [inRx, _inTx] = newStreamPair<SyncMessage>();
477
- const [outRx, outTx] = newStreamPair<SyncMessage>();
478
-
479
- node.syncManager.addPeer({
480
- id: "test",
481
- incoming: inRx,
482
- outgoing: outTx,
483
- role: "peer",
484
- });
485
-
486
- map.set("hello", "world", "trusting");
487
-
488
- const reader = outRx.getReader();
489
-
490
- await expect(
491
- shouldNotResolve(reader.read(), { timeout: 100 }),
492
- ).resolves.toBeUndefined();
493
- });
369
+ } satisfies SyncMessage);
370
+ }).pipe(Effect.scoped, Effect.runPromise));
494
371
 
495
- test.todo(
496
- "If we add a server peer, all updates to all coValues are sent to it, even if it doesn't subscribe",
497
- async () => {
372
+ test("No matter the optimistic known state, node respects invalid known state messages and resyncs", () =>
373
+ Effect.gen(function* () {
498
374
  const [admin, session] = randomAnonymousAccountAndSessionID();
499
375
  const node = new LocalNode(admin, session, Crypto);
500
376
 
@@ -502,61 +378,85 @@ test.todo(
502
378
 
503
379
  const map = group.createMap();
504
380
 
505
- const [inRx, _inTx] = newStreamPair<SyncMessage>();
506
- const [outRx, outTx] = newStreamPair<SyncMessage>();
381
+ const [inRx, inTx] = yield* newQueuePair();
382
+ const [outRx, outTx] = yield* newQueuePair();
383
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
384
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
507
385
 
508
386
  node.syncManager.addPeer({
509
387
  id: "test",
510
388
  incoming: inRx,
511
389
  outgoing: outTx,
512
- role: "server",
390
+ role: "peer",
513
391
  });
514
392
 
515
- const reader = outRx.getReader();
516
- // expect((await reader.read()).value).toMatchObject({
517
- // action: "load",
518
- // id: adminID,
519
- // });
520
- expect((await reader.read()).value).toMatchObject({
393
+ yield* Queue.offer(inTx, {
521
394
  action: "load",
522
- id: group.core.id,
395
+ id: map.core.id,
396
+ header: false,
397
+ sessions: {
398
+ [node.currentSessionID]: 0,
399
+ },
523
400
  });
524
401
 
525
- const mapSubscribeMsg = await reader.read();
402
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admStateEx(admin.id));
403
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
404
+
405
+ const mapTellKnownStateMsg = yield* Queue.take(outRxQ);
406
+ expect(mapTellKnownStateMsg).toEqual({
407
+ action: "known",
408
+ ...map.core.knownState(),
409
+ } satisfies SyncMessage);
526
410
 
527
- expect(mapSubscribeMsg.value).toEqual({
528
- action: "load",
411
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
412
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
413
+
414
+ const mapNewContentHeaderOnlyMsg = yield* Queue.take(outRxQ);
415
+
416
+ expect(mapNewContentHeaderOnlyMsg).toEqual({
417
+ action: "content",
529
418
  id: map.core.id,
530
- header: true,
531
- sessions: {},
419
+ header: map.core.header,
420
+ new: {},
532
421
  } satisfies SyncMessage);
533
422
 
534
423
  map.set("hello", "world", "trusting");
535
424
 
536
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
537
- expect((await reader.read()).value).toMatchObject(
538
- groupContentEx(group),
539
- );
425
+ map.set("goodbye", "world", "trusting");
426
+
427
+ const _mapEditMsgs = yield* Queue.take(outRxQ);
428
+
429
+ console.log("Sending correction");
430
+
431
+ yield* Queue.offer(inTx, {
432
+ action: "known",
433
+ isCorrection: true,
434
+ id: map.core.id,
435
+ header: true,
436
+ sessions: {
437
+ [node.currentSessionID]: 1,
438
+ },
439
+ } satisfies SyncMessage);
540
440
 
541
- const mapNewContentMsg = await reader.read();
441
+ const newContentAfterWrongAssumedState = yield* Queue.take(outRxQ);
542
442
 
543
- expect(mapNewContentMsg.value).toEqual({
443
+ expect(newContentAfterWrongAssumedState).toEqual({
544
444
  action: "content",
545
445
  id: map.core.id,
546
- header: map.core.header,
446
+ header: undefined,
547
447
  new: {
548
448
  [node.currentSessionID]: {
549
- after: 0,
449
+ after: 1,
550
450
  newTransactions: [
551
451
  {
552
452
  privacy: "trusting" as const,
553
453
  madeAt: map.core.sessionLogs.get(
554
454
  node.currentSessionID,
555
- )!.transactions[0]!.madeAt,
455
+ )!.transactions[1]!.madeAt,
556
456
  changes: stableStringify([
557
457
  {
558
458
  op: "set",
559
- key: "hello",
459
+ key: "goodbye",
560
460
  value: "world",
561
461
  } satisfies MapOpPayload<string, string>,
562
462
  ]),
@@ -568,256 +468,378 @@ test.todo(
568
468
  },
569
469
  },
570
470
  } satisfies SyncMessage);
571
- },
572
- );
471
+ }).pipe(Effect.scoped, Effect.runPromise));
573
472
 
574
- test.skip("If we add a server peer, newly created coValues are auto-subscribed to", async () => {
575
- const [admin, session] = randomAnonymousAccountAndSessionID();
576
- const node = new LocalNode(admin, session, Crypto);
473
+ test("If we add a peer, but it never subscribes to a coValue, it won't get any messages", () =>
474
+ Effect.gen(function* () {
475
+ const [admin, session] = randomAnonymousAccountAndSessionID();
476
+ const node = new LocalNode(admin, session, Crypto);
577
477
 
578
- const group = node.createGroup();
478
+ const group = node.createGroup();
579
479
 
580
- const [inRx, _inTx] = newStreamPair<SyncMessage>();
581
- const [outRx, outTx] = newStreamPair<SyncMessage>();
480
+ const map = group.createMap();
582
481
 
583
- node.syncManager.addPeer({
584
- id: "test",
585
- incoming: inRx,
586
- outgoing: outTx,
587
- role: "server",
588
- });
482
+ const [inRx, _inTx] = yield* newQueuePair();
483
+ const [outRx, outTx] = yield* newQueuePair();
484
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
485
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
589
486
 
590
- const reader = outRx.getReader();
591
- // expect((await reader.read()).value).toMatchObject({
592
- // action: "load",
593
- // id: admin.id,
594
- // });
595
- expect((await reader.read()).value).toMatchObject({
596
- action: "load",
597
- id: group.core.id,
598
- });
487
+ node.syncManager.addPeer({
488
+ id: "test",
489
+ incoming: inRx,
490
+ outgoing: outTx,
491
+ role: "peer",
492
+ });
599
493
 
600
- const map = group.createMap();
494
+ map.set("hello", "world", "trusting");
601
495
 
602
- const mapSubscribeMsg = await reader.read();
496
+ expect(
497
+ yield* Queue.take(outRxQ).pipe(
498
+ Effect.timeout(100),
499
+ Effect.catch("_tag", {
500
+ failure: "TimeoutException",
501
+ onFailure: () => Effect.succeed("neverHappened"),
502
+ }),
503
+ ),
504
+ ).toEqual("neverHappened");
505
+ }).pipe(Effect.scoped, Effect.runPromise));
603
506
 
604
- expect(mapSubscribeMsg.value).toEqual({
605
- action: "load",
606
- ...map.core.knownState(),
607
- } satisfies SyncMessage);
507
+ test.todo(
508
+ "If we add a server peer, all updates to all coValues are sent to it, even if it doesn't subscribe",
509
+ () =>
510
+ Effect.gen(function* () {
511
+ const [admin, session] = randomAnonymousAccountAndSessionID();
512
+ const node = new LocalNode(admin, session, Crypto);
513
+
514
+ const group = node.createGroup();
515
+
516
+ const map = group.createMap();
517
+
518
+ const [inRx, _inTx] = yield* newQueuePair();
519
+ const [outRx, outTx] = yield* newQueuePair();
520
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
521
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
522
+
523
+ node.syncManager.addPeer({
524
+ id: "test",
525
+ incoming: inRx,
526
+ outgoing: outTx,
527
+ role: "server",
528
+ });
529
+
530
+ // expect(yield* Queue.take(outRxQ)).toMatchObject({
531
+ // action: "load",
532
+ // id: adminID,
533
+ // });
534
+ expect(yield* Queue.take(outRxQ)).toMatchObject({
535
+ action: "load",
536
+ id: group.core.id,
537
+ });
538
+
539
+ const mapSubscribeMsg = Queue.take(outRxQ);
540
+
541
+ expect(mapSubscribeMsg).toEqual({
542
+ action: "load",
543
+ id: map.core.id,
544
+ header: true,
545
+ sessions: {},
546
+ } satisfies SyncMessage);
547
+
548
+ map.set("hello", "world", "trusting");
549
+
550
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
551
+ expect(yield* Queue.take(outRxQ)).toMatchObject(
552
+ groupContentEx(group),
553
+ );
554
+
555
+ const mapNewContentMsg = Queue.take(outRxQ);
556
+
557
+ expect(mapNewContentMsg).toEqual({
558
+ action: "content",
559
+ id: map.core.id,
560
+ header: map.core.header,
561
+ new: {
562
+ [node.currentSessionID]: {
563
+ after: 0,
564
+ newTransactions: [
565
+ {
566
+ privacy: "trusting" as const,
567
+ madeAt: map.core.sessionLogs.get(
568
+ node.currentSessionID,
569
+ )!.transactions[0]!.madeAt,
570
+ changes: stableStringify([
571
+ {
572
+ op: "set",
573
+ key: "hello",
574
+ value: "world",
575
+ } satisfies MapOpPayload<string, string>,
576
+ ]),
577
+ },
578
+ ],
579
+ lastSignature: map.core.sessionLogs.get(
580
+ node.currentSessionID,
581
+ )!.lastSignature!,
582
+ },
583
+ },
584
+ } satisfies SyncMessage);
585
+ }).pipe(Effect.scoped, Effect.runPromise),
586
+ );
608
587
 
609
- // expect((await reader.read()).value).toMatchObject(admContEx(adminID));
610
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
588
+ test.skip("If we add a server peer, newly created coValues are auto-subscribed to", () =>
589
+ Effect.gen(function* () {
590
+ const [admin, session] = randomAnonymousAccountAndSessionID();
591
+ const node = new LocalNode(admin, session, Crypto);
611
592
 
612
- const mapContentMsg = await reader.read();
593
+ const group = node.createGroup();
613
594
 
614
- expect(mapContentMsg.value).toEqual({
615
- action: "content",
616
- id: map.core.id,
617
- header: map.core.header,
618
- new: {},
619
- } satisfies SyncMessage);
620
- });
595
+ const [inRx, _inTx] = yield* newQueuePair();
596
+ const [outRx, outTx] = yield* newQueuePair();
597
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
598
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
621
599
 
622
- test.todo(
623
- "TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it",
624
- );
600
+ node.syncManager.addPeer({
601
+ id: "test",
602
+ incoming: inRx,
603
+ outgoing: outTx,
604
+ role: "server",
605
+ });
625
606
 
626
- test("When we connect a new server peer, we try to sync all existing coValues to it", async () => {
627
- const [admin, session] = randomAnonymousAccountAndSessionID();
628
- const node = new LocalNode(admin, session, Crypto);
607
+ // expect(yield* Queue.take(outRxQ)).toMatchObject({
608
+ // action: "load",
609
+ // id: admin.id,
610
+ // });
611
+ expect(yield* Queue.take(outRxQ)).toMatchObject({
612
+ action: "load",
613
+ id: group.core.id,
614
+ });
629
615
 
630
- const group = node.createGroup();
616
+ const map = group.createMap();
631
617
 
632
- const map = group.createMap();
618
+ const mapSubscribeMsg = Queue.take(outRxQ);
633
619
 
634
- const [inRx, _inTx] = newStreamPair<SyncMessage>();
635
- const [outRx, outTx] = newStreamPair<SyncMessage>();
620
+ expect(mapSubscribeMsg).toEqual({
621
+ action: "load",
622
+ ...map.core.knownState(),
623
+ } satisfies SyncMessage);
636
624
 
637
- node.syncManager.addPeer({
638
- id: "test",
639
- incoming: inRx,
640
- outgoing: outTx,
641
- role: "server",
642
- });
625
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(adminID));
626
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
643
627
 
644
- const reader = outRx.getReader();
628
+ const mapContentMsg = Queue.take(outRxQ);
645
629
 
646
- // const _adminSubscribeMessage = await reader.read();
647
- const groupSubscribeMessage = await reader.read();
630
+ expect(mapContentMsg).toEqual({
631
+ action: "content",
632
+ id: map.core.id,
633
+ header: map.core.header,
634
+ new: {},
635
+ } satisfies SyncMessage);
636
+ }).pipe(Effect.scoped, Effect.runPromise));
648
637
 
649
- expect(groupSubscribeMessage.value).toEqual({
650
- action: "load",
651
- ...group.core.knownState(),
652
- } satisfies SyncMessage);
638
+ test.todo(
639
+ "TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it",
640
+ );
653
641
 
654
- const secondMessage = await reader.read();
642
+ test("When we connect a new server peer, we try to sync all existing coValues to it", () =>
643
+ Effect.gen(function* () {
644
+ const [admin, session] = randomAnonymousAccountAndSessionID();
645
+ const node = new LocalNode(admin, session, Crypto);
655
646
 
656
- expect(secondMessage.value).toEqual({
657
- action: "load",
658
- ...map.core.knownState(),
659
- } satisfies SyncMessage);
660
- });
647
+ const group = node.createGroup();
661
648
 
662
- test("When receiving a subscribe with a known state that is ahead of our own, peers should respond with a corresponding subscribe response message", async () => {
663
- const [admin, session] = randomAnonymousAccountAndSessionID();
664
- const node = new LocalNode(admin, session, Crypto);
649
+ const map = group.createMap();
665
650
 
666
- const group = node.createGroup();
651
+ const [inRx, _inTx] = yield* newQueuePair();
652
+ const [outRx, outTx] = yield* newQueuePair();
653
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
654
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
667
655
 
668
- const map = group.createMap();
656
+ node.syncManager.addPeer({
657
+ id: "test",
658
+ incoming: inRx,
659
+ outgoing: outTx,
660
+ role: "server",
661
+ });
669
662
 
670
- const [inRx, inTx] = newStreamPair<SyncMessage>();
671
- const [outRx, outTx] = newStreamPair<SyncMessage>();
663
+ // const _adminSubscribeMessage = yield* Queue.take(outRxQ);
664
+ const groupSubscribeMessage = yield* Queue.take(outRxQ);
672
665
 
673
- node.syncManager.addPeer({
674
- id: "test",
675
- incoming: inRx,
676
- outgoing: outTx,
677
- role: "peer",
678
- });
666
+ expect(groupSubscribeMessage).toEqual({
667
+ action: "load",
668
+ ...group.core.knownState(),
669
+ } satisfies SyncMessage);
679
670
 
680
- const writer = inTx.getWriter();
671
+ const secondMessage = yield* Queue.take(outRxQ);
681
672
 
682
- await writer.write({
683
- action: "load",
684
- id: map.core.id,
685
- header: true,
686
- sessions: {
687
- [node.currentSessionID]: 1,
688
- },
689
- });
673
+ expect(secondMessage).toEqual({
674
+ action: "load",
675
+ ...map.core.knownState(),
676
+ } satisfies SyncMessage);
677
+ }).pipe(Effect.scoped, Effect.runPromise));
690
678
 
691
- const reader = outRx.getReader();
679
+ test("When receiving a subscribe with a known state that is ahead of our own, peers should respond with a corresponding subscribe response message", () =>
680
+ Effect.gen(function* () {
681
+ const [admin, session] = randomAnonymousAccountAndSessionID();
682
+ const node = new LocalNode(admin, session, Crypto);
692
683
 
693
- // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
694
- expect((await reader.read()).value).toMatchObject(groupStateEx(group));
695
- const mapTellKnownState = await reader.read();
684
+ const group = node.createGroup();
696
685
 
697
- expect(mapTellKnownState.value).toEqual({
698
- action: "known",
699
- ...map.core.knownState(),
700
- } satisfies SyncMessage);
701
- });
686
+ const map = group.createMap();
702
687
 
703
- test.skip("When replaying creation and transactions of a coValue as new content, the receiving peer integrates this information", async () => {
704
- // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work
705
- const [admin, session] = randomAnonymousAccountAndSessionID();
688
+ const [inRx, inTx] = yield* newQueuePair();
689
+ const [outRx, outTx] = yield* newQueuePair();
690
+ const outRxQ = yield* Queue.unbounded<SyncMessage>();
691
+ yield* Effect.fork(Stream.run(outRx, Sink.fromQueue(outRxQ)));
706
692
 
707
- const node1 = new LocalNode(admin, session, Crypto);
693
+ node.syncManager.addPeer({
694
+ id: "test",
695
+ incoming: inRx,
696
+ outgoing: outTx,
697
+ role: "peer",
698
+ });
708
699
 
709
- const group = node1.createGroup();
700
+ yield* Queue.offer(inTx, {
701
+ action: "load",
702
+ id: map.core.id,
703
+ header: true,
704
+ sessions: {
705
+ [node.currentSessionID]: 1,
706
+ },
707
+ });
710
708
 
711
- const [inRx1, inTx1] = newStreamPair<SyncMessage>();
712
- const [outRx1, outTx1] = newStreamPair<SyncMessage>();
709
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admStateEx(admin.id));
710
+ expect(yield* Queue.take(outRxQ)).toMatchObject(groupStateEx(group));
711
+ const mapTellKnownState = yield* Queue.take(outRxQ);
713
712
 
714
- node1.syncManager.addPeer({
715
- id: "test2",
716
- incoming: inRx1,
717
- outgoing: outTx1,
718
- role: "server",
719
- });
713
+ expect(mapTellKnownState).toEqual({
714
+ action: "known",
715
+ ...map.core.knownState(),
716
+ } satisfies SyncMessage);
717
+ }).pipe(Effect.scoped, Effect.runPromise));
720
718
 
721
- const to1 = inTx1.getWriter();
722
- const from1 = outRx1.getReader();
719
+ test.skip("When replaying creation and transactions of a coValue as new content, the receiving peer integrates this information", () =>
720
+ Effect.gen(function* () {
721
+ // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work
722
+ const [admin, session] = randomAnonymousAccountAndSessionID();
723
723
 
724
- const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
724
+ const node1 = new LocalNode(admin, session, Crypto);
725
725
 
726
- const [inRx2, inTx2] = newStreamPair<SyncMessage>();
727
- const [outRx2, outTx2] = newStreamPair<SyncMessage>();
726
+ const group = node1.createGroup();
728
727
 
729
- node2.syncManager.addPeer({
730
- id: "test1",
731
- incoming: inRx2,
732
- outgoing: outTx2,
733
- role: "client",
734
- });
728
+ const [inRx1, inTx1] = yield* newQueuePair();
729
+ const [outRx1, outTx1] = yield* newQueuePair();
730
+ const outRxQ1 = yield* Queue.unbounded<SyncMessage>();
731
+ yield* Effect.fork(Stream.run(outRx1, Sink.fromQueue(outRxQ1)));
735
732
 
736
- const to2 = inTx2.getWriter();
737
- const from2 = outRx2.getReader();
733
+ node1.syncManager.addPeer({
734
+ id: "test2",
735
+ incoming: inRx1,
736
+ outgoing: outTx1,
737
+ role: "server",
738
+ });
738
739
 
739
- const adminSubscribeMessage = await from1.read();
740
- expect(adminSubscribeMessage.value).toMatchObject({
741
- action: "load",
742
- id: admin.id,
743
- });
744
- const groupSubscribeMsg = await from1.read();
745
- expect(groupSubscribeMsg.value).toMatchObject({
746
- action: "load",
747
- id: group.core.id,
748
- });
740
+ const node2 = new LocalNode(
741
+ admin,
742
+ newRandomSessionID(admin.id),
743
+ Crypto,
744
+ );
749
745
 
750
- await to2.write(adminSubscribeMessage.value!);
751
- await to2.write(groupSubscribeMsg.value!);
746
+ const [inRx2, inTx2] = yield* newQueuePair();
747
+ const [outRx2, outTx2] = yield* newQueuePair();
748
+ const outRxQ2 = yield* Queue.unbounded<SyncMessage>();
749
+ yield* Effect.fork(Stream.run(outRx2, Sink.fromQueue(outRxQ1)));
752
750
 
753
- // const adminTellKnownStateMsg = await from2.read();
754
- // expect(adminTellKnownStateMsg.value).toMatchObject(admStateEx(admin.id));
751
+ node2.syncManager.addPeer({
752
+ id: "test1",
753
+ incoming: inRx2,
754
+ outgoing: outTx2,
755
+ role: "client",
756
+ });
755
757
 
756
- const groupTellKnownStateMsg = await from2.read();
757
- expect(groupTellKnownStateMsg.value).toMatchObject(groupStateEx(group));
758
+ const adminSubscribeMessage: SyncMessage = yield* Queue.take(outRxQ1);
759
+ expect(adminSubscribeMessage).toMatchObject({
760
+ action: "load",
761
+ id: admin.id,
762
+ });
763
+ const groupSubscribeMsg = yield* Queue.take(outRxQ1);
764
+ expect(groupSubscribeMsg).toMatchObject({
765
+ action: "load",
766
+ id: group.core.id,
767
+ });
758
768
 
759
- expect(
760
- node2.syncManager.peers["test1"]!.optimisticKnownStates[group.core.id],
761
- ).toBeDefined();
769
+ yield* Queue.offer(inTx2, adminSubscribeMessage);
770
+ yield* Queue.offer(inTx2, groupSubscribeMsg);
762
771
 
763
- // await to1.write(adminTellKnownStateMsg.value!);
764
- await to1.write(groupTellKnownStateMsg.value!);
772
+ // const adminTellKnownStateMsg = yield* Queue.take(outRxQ2);
773
+ // expect(adminTellKnownStateMsg).toMatchObject(admStateEx(admin.id));
765
774
 
766
- // const adminContentMsg = await from1.read();
767
- // expect(adminContentMsg.value).toMatchObject(admContEx(admin.id));
775
+ const groupTellKnownStateMsg = yield* Queue.take(outRxQ2);
776
+ expect(groupTellKnownStateMsg).toMatchObject(groupStateEx(group));
768
777
 
769
- const groupContentMsg = await from1.read();
770
- expect(groupContentMsg.value).toMatchObject(groupContentEx(group));
778
+ expect(
779
+ node2.syncManager.peers["test1"]!.optimisticKnownStates[
780
+ group.core.id
781
+ ],
782
+ ).toBeDefined();
771
783
 
772
- // await to2.write(adminContentMsg.value!);
773
- await to2.write(groupContentMsg.value!);
784
+ // yield* Queue.offer(inTx1, adminTellKnownStateMsg);
785
+ yield* Queue.offer(inTx1, groupTellKnownStateMsg);
774
786
 
775
- const map = group.createMap();
787
+ // const adminContentMsg = yield* Queue.take(outRxQ1);
788
+ // expect(adminContentMsg).toMatchObject(admContEx(admin.id));
776
789
 
777
- const mapSubscriptionMsg = await from1.read();
778
- expect(mapSubscriptionMsg.value).toMatchObject({
779
- action: "load",
780
- id: map.core.id,
781
- });
790
+ const groupContentMsg = yield* Queue.take(outRxQ1);
791
+ expect(groupContentMsg).toMatchObject(groupContentEx(group));
782
792
 
783
- const mapNewContentMsg = await from1.read();
784
- expect(mapNewContentMsg.value).toEqual({
785
- action: "content",
786
- id: map.core.id,
787
- header: map.core.header,
788
- new: {},
789
- } satisfies SyncMessage);
793
+ // yield* Queue.offer(inTx2, adminContentMsg);
794
+ yield* Queue.offer(inTx2, groupContentMsg);
790
795
 
791
- await to2.write(mapSubscriptionMsg.value!);
796
+ const map = group.createMap();
792
797
 
793
- const mapTellKnownStateMsg = await from2.read();
794
- expect(mapTellKnownStateMsg.value).toEqual({
795
- action: "known",
796
- id: map.core.id,
797
- header: false,
798
- sessions: {},
799
- } satisfies SyncMessage);
798
+ const mapSubscriptionMsg = yield* Queue.take(outRxQ1);
799
+ expect(mapSubscriptionMsg).toMatchObject({
800
+ action: "load",
801
+ id: map.core.id,
802
+ });
800
803
 
801
- expect(node2.coValues[map.core.id]?.state).toEqual("loading");
804
+ const mapNewContentMsg = yield* Queue.take(outRxQ1);
805
+ expect(mapNewContentMsg).toEqual({
806
+ action: "content",
807
+ id: map.core.id,
808
+ header: map.core.header,
809
+ new: {},
810
+ } satisfies SyncMessage);
802
811
 
803
- await to2.write(mapNewContentMsg.value!);
812
+ yield* Queue.offer(inTx2, mapSubscriptionMsg);
804
813
 
805
- map.set("hello", "world", "trusting");
814
+ const mapTellKnownStateMsg = yield* Queue.take(outRxQ2);
815
+ expect(mapTellKnownStateMsg).toEqual({
816
+ action: "known",
817
+ id: map.core.id,
818
+ header: false,
819
+ sessions: {},
820
+ } satisfies SyncMessage);
806
821
 
807
- const mapEditMsg = await from1.read();
822
+ expect(node2.coValues[map.core.id]?.state).toEqual("loading");
808
823
 
809
- await to2.write(mapEditMsg.value!);
824
+ yield* Queue.offer(inTx2, mapNewContentMsg);
810
825
 
811
- await new Promise((resolve) => setTimeout(resolve, 100));
826
+ map.set("hello", "world", "trusting");
812
827
 
813
- expect(
814
- expectMap(
815
- node2.expectCoValueLoaded(map.core.id).getCurrentContent(),
816
- ).get("hello"),
817
- ).toEqual("world");
818
- });
828
+ const mapEditMsg = yield* Queue.take(outRxQ1);
829
+
830
+ yield* Queue.offer(inTx2, mapEditMsg);
831
+
832
+ yield* Effect.sleep(100);
833
+
834
+ expect(
835
+ expectMap(
836
+ node2.expectCoValueLoaded(map.core.id).getCurrentContent(),
837
+ ).get("hello"),
838
+ ).toEqual("world");
839
+ }).pipe(Effect.scoped, Effect.runPromise));
819
840
 
820
841
  test.skip("When loading a coValue on one node, the server node it is requested from replies with all the necessary depended on coValues to make it work", async () => {
842
+ /*
821
843
  // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work
822
844
  const [admin, session] = randomAnonymousAccountAndSessionID();
823
845
 
@@ -842,104 +864,138 @@ test.skip("When loading a coValue on one node, the server node it is requested f
842
864
  node2.expectCoValueLoaded(map.core.id).getCurrentContent(),
843
865
  ).get("hello"),
844
866
  ).toEqual("world");
867
+ */
845
868
  });
846
869
 
847
- test("Can sync a coValue through a server to another client", async () => {
848
- const [admin, session] = randomAnonymousAccountAndSessionID();
870
+ test("Can sync a coValue through a server to another client", () =>
871
+ Effect.gen(function* () {
872
+ const [admin, session] = randomAnonymousAccountAndSessionID();
849
873
 
850
- const client1 = new LocalNode(admin, session, Crypto);
874
+ const client1 = new LocalNode(admin, session, Crypto);
851
875
 
852
- const group = client1.createGroup();
876
+ const group = client1.createGroup();
853
877
 
854
- const map = group.createMap();
855
- map.set("hello", "world", "trusting");
878
+ const map = group.createMap();
879
+ map.set("hello", "world", "trusting");
856
880
 
857
- const [serverUser, serverSession] = randomAnonymousAccountAndSessionID();
881
+ const [serverUser, serverSession] =
882
+ randomAnonymousAccountAndSessionID();
858
883
 
859
- const server = new LocalNode(serverUser, serverSession, Crypto);
884
+ const server = new LocalNode(serverUser, serverSession, Crypto);
860
885
 
861
- const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
862
- peer1role: "server",
863
- peer2role: "client",
864
- trace: true,
865
- });
886
+ const [serverAsPeerForClient1, client1AsPeer] = yield* connectedPeers(
887
+ "serverFor1",
888
+ "client1",
889
+ {
890
+ peer1role: "server",
891
+ peer2role: "client",
892
+ trace: true,
893
+ },
894
+ );
866
895
 
867
- client1.syncManager.addPeer(serverAsPeer);
868
- server.syncManager.addPeer(client1AsPeer);
896
+ client1.syncManager.addPeer(serverAsPeerForClient1);
897
+ server.syncManager.addPeer(client1AsPeer);
869
898
 
870
- const client2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
899
+ const client2 = new LocalNode(
900
+ admin,
901
+ newRandomSessionID(admin.id),
902
+ Crypto,
903
+ );
871
904
 
872
- const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
873
- "server",
874
- "client2",
875
- { peer1role: "server", peer2role: "client", trace: true },
876
- );
905
+ const [serverAsPeerForClient2, client2AsPeer] = yield* connectedPeers(
906
+ "serverFor2",
907
+ "client2",
908
+ {
909
+ peer1role: "server",
910
+ peer2role: "client",
911
+ trace: true,
912
+ },
913
+ );
877
914
 
878
- client2.syncManager.addPeer(serverAsOtherPeer);
879
- server.syncManager.addPeer(client2AsPeer);
915
+ client2.syncManager.addPeer(serverAsPeerForClient2);
916
+ server.syncManager.addPeer(client2AsPeer);
880
917
 
881
- const mapOnClient2 = await client2.loadCoValueCore(map.core.id);
882
- if (mapOnClient2 === "unavailable") {
883
- throw new Error("Map is unavailable");
884
- }
918
+ const mapOnClient2 = yield* Effect.promise(() =>
919
+ client2.loadCoValueCore(map.core.id),
920
+ );
921
+ if (mapOnClient2 === "unavailable") {
922
+ throw new Error("Map is unavailable");
923
+ }
885
924
 
886
- expect(expectMap(mapOnClient2.getCurrentContent()).get("hello")).toEqual(
887
- "world",
888
- );
889
- });
925
+ expect(
926
+ expectMap(mapOnClient2.getCurrentContent()).get("hello"),
927
+ ).toEqual("world");
928
+ }).pipe(Effect.scoped, Effect.runPromise));
890
929
 
891
- test("Can sync a coValue with private transactions through a server to another client", async () => {
892
- const [admin, session] = randomAnonymousAccountAndSessionID();
930
+ test("Can sync a coValue with private transactions through a server to another client", () =>
931
+ Effect.gen(function* () {
932
+ const [admin, session] = randomAnonymousAccountAndSessionID();
893
933
 
894
- const client1 = new LocalNode(admin, session, Crypto);
934
+ const client1 = new LocalNode(admin, session, Crypto);
895
935
 
896
- const group = client1.createGroup();
936
+ const group = client1.createGroup();
897
937
 
898
- const map = group.createMap();
899
- map.set("hello", "world", "private");
938
+ const map = group.createMap();
939
+ map.set("hello", "world", "private");
900
940
 
901
- const [serverUser, serverSession] = randomAnonymousAccountAndSessionID();
941
+ const [serverUser, serverSession] =
942
+ randomAnonymousAccountAndSessionID();
902
943
 
903
- const server = new LocalNode(serverUser, serverSession, Crypto);
944
+ const server = new LocalNode(serverUser, serverSession, Crypto);
904
945
 
905
- const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
906
- trace: true,
907
- peer1role: "server",
908
- peer2role: "client",
909
- });
946
+ const [serverAsPeer, client1AsPeer] = yield* connectedPeers(
947
+ "server",
948
+ "client1",
949
+ {
950
+ trace: true,
951
+ peer1role: "server",
952
+ peer2role: "client",
953
+ },
954
+ );
910
955
 
911
- client1.syncManager.addPeer(serverAsPeer);
912
- server.syncManager.addPeer(client1AsPeer);
956
+ client1.syncManager.addPeer(serverAsPeer);
957
+ server.syncManager.addPeer(client1AsPeer);
913
958
 
914
- const client2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
959
+ const client2 = new LocalNode(
960
+ admin,
961
+ newRandomSessionID(admin.id),
962
+ Crypto,
963
+ );
915
964
 
916
- const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
917
- "server",
918
- "client2",
919
- { trace: true, peer1role: "server", peer2role: "client" },
920
- );
965
+ const [serverAsOtherPeer, client2AsPeer] = yield* connectedPeers(
966
+ "server",
967
+ "client2",
968
+ {
969
+ trace: true,
970
+ peer1role: "server",
971
+ peer2role: "client",
972
+ },
973
+ );
921
974
 
922
- client2.syncManager.addPeer(serverAsOtherPeer);
923
- server.syncManager.addPeer(client2AsPeer);
975
+ client2.syncManager.addPeer(serverAsOtherPeer);
976
+ server.syncManager.addPeer(client2AsPeer);
924
977
 
925
- const mapOnClient2 = await client2.loadCoValueCore(map.core.id);
926
- if (mapOnClient2 === "unavailable") {
927
- throw new Error("Map is unavailable");
928
- }
978
+ const mapOnClient2 = yield* Effect.promise(() =>
979
+ client2.loadCoValueCore(map.core.id),
980
+ );
981
+ if (mapOnClient2 === "unavailable") {
982
+ throw new Error("Map is unavailable");
983
+ }
929
984
 
930
- expect(expectMap(mapOnClient2.getCurrentContent()).get("hello")).toEqual(
931
- "world",
932
- );
933
- });
985
+ expect(
986
+ expectMap(mapOnClient2.getCurrentContent()).get("hello"),
987
+ ).toEqual("world");
988
+ }).pipe(Effect.scoped, Effect.runPromise));
934
989
 
935
990
  test.skip("When a peer's incoming/readable stream closes, we remove the peer", async () => {
991
+ /*
936
992
  const [admin, session] = randomAnonymousAccountAndSessionID();
937
993
  const node = new LocalNode(admin, session, Crypto);
938
994
 
939
995
  const group = node.createGroup();
940
996
 
941
- const [inRx, inTx] = newStreamPair<SyncMessage>();
942
- const [outRx, outTx] = newStreamPair<SyncMessage>();
997
+ const [inRx, inTx] = await Effect.runPromise(newStreamPair());
998
+ const [outRx, outTx] = await Effect.runPromise(newStreamPair());
943
999
 
944
1000
  node.syncManager.addPeer({
945
1001
  id: "test",
@@ -948,12 +1004,11 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
948
1004
  role: "server",
949
1005
  });
950
1006
 
951
- const reader = outRx.getReader();
952
- // expect((await reader.read()).value).toMatchObject({
1007
+ // expect(yield* Queue.take(outRxQ)).toMatchObject({
953
1008
  // action: "load",
954
1009
  // id: admin.id,
955
1010
  // });
956
- expect((await reader.read()).value).toMatchObject({
1011
+ expect(yield * Queue.take(outRxQ)).toMatchObject({
957
1012
  action: "load",
958
1013
  id: group.core.id,
959
1014
  });
@@ -967,8 +1022,8 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
967
1022
  ...map.core.knownState(),
968
1023
  } satisfies SyncMessage);
969
1024
 
970
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
971
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
1025
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
1026
+ expect(yield * Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
972
1027
 
973
1028
  const mapContentMsg = await reader.read();
974
1029
 
@@ -984,16 +1039,18 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
984
1039
  await new Promise((resolve) => setTimeout(resolve, 100));
985
1040
 
986
1041
  expect(node.syncManager.peers["test"]).toBeUndefined();
1042
+ */
987
1043
  });
988
1044
 
989
1045
  test.skip("When a peer's outgoing/writable stream closes, we remove the peer", async () => {
1046
+ /*
990
1047
  const [admin, session] = randomAnonymousAccountAndSessionID();
991
1048
  const node = new LocalNode(admin, session, Crypto);
992
1049
 
993
1050
  const group = node.createGroup();
994
1051
 
995
- const [inRx] = newStreamPair<SyncMessage>();
996
- const [outRx, outTx] = newStreamPair<SyncMessage>();
1052
+ const [inRx] = await Effect.runPromise(newStreamPair());
1053
+ const [outRx, outTx] = await Effect.runPromise(newStreamPair());
997
1054
 
998
1055
  node.syncManager.addPeer({
999
1056
  id: "test",
@@ -1002,12 +1059,11 @@ test.skip("When a peer's outgoing/writable stream closes, we remove the peer", a
1002
1059
  role: "server",
1003
1060
  });
1004
1061
 
1005
- const reader = outRx.getReader();
1006
- // expect((await reader.read()).value).toMatchObject({
1062
+ // expect(yield* Queue.take(outRxQ)).toMatchObject({
1007
1063
  // action: "load",
1008
1064
  // id: admin.id,
1009
1065
  // });
1010
- expect((await reader.read()).value).toMatchObject({
1066
+ expect(yield * Queue.take(outRxQ)).toMatchObject({
1011
1067
  action: "load",
1012
1068
  id: group.core.id,
1013
1069
  });
@@ -1021,8 +1077,8 @@ test.skip("When a peer's outgoing/writable stream closes, we remove the peer", a
1021
1077
  ...map.core.knownState(),
1022
1078
  } satisfies SyncMessage);
1023
1079
 
1024
- // expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
1025
- expect((await reader.read()).value).toMatchObject(groupContentEx(group));
1080
+ // expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
1081
+ expect(yield * Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
1026
1082
 
1027
1083
  const mapContentMsg = await reader.read();
1028
1084
 
@@ -1041,43 +1097,53 @@ test.skip("When a peer's outgoing/writable stream closes, we remove the peer", a
1041
1097
  await new Promise((resolve) => setTimeout(resolve, 100));
1042
1098
 
1043
1099
  expect(node.syncManager.peers["test"]).toBeUndefined();
1100
+ */
1044
1101
  });
1045
1102
 
1046
- test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
1047
- const [admin, session] = randomAnonymousAccountAndSessionID();
1103
+ test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", () =>
1104
+ Effect.gen(function* () {
1105
+ const [admin, session] = randomAnonymousAccountAndSessionID();
1048
1106
 
1049
- const node1 = new LocalNode(admin, session, Crypto);
1107
+ const node1 = new LocalNode(admin, session, Crypto);
1050
1108
 
1051
- const group = node1.createGroup();
1109
+ const group = node1.createGroup();
1052
1110
 
1053
- const map = group.createMap();
1054
- map.set("hello", "world", "trusting");
1111
+ const map = group.createMap();
1112
+ map.set("hello", "world", "trusting");
1055
1113
 
1056
- const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
1114
+ const node2 = new LocalNode(
1115
+ admin,
1116
+ newRandomSessionID(admin.id),
1117
+ Crypto,
1118
+ );
1057
1119
 
1058
- const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
1059
- peer1role: "server",
1060
- peer2role: "client",
1061
- trace: true,
1062
- });
1120
+ const [node1asPeer, node2asPeer] = yield* connectedPeers(
1121
+ "peer1",
1122
+ "peer2",
1123
+ {
1124
+ peer1role: "server",
1125
+ peer2role: "client",
1126
+ trace: true,
1127
+ },
1128
+ );
1063
1129
 
1064
- node1.syncManager.addPeer(node2asPeer);
1130
+ node1.syncManager.addPeer(node2asPeer);
1065
1131
 
1066
- const mapOnNode2Promise = node2.loadCoValueCore(map.core.id);
1132
+ const mapOnNode2Promise = node2.loadCoValueCore(map.core.id);
1067
1133
 
1068
- expect(node2.coValues[map.core.id]?.state).toEqual("loading");
1134
+ expect(node2.coValues[map.core.id]?.state).toEqual("loading");
1069
1135
 
1070
- node2.syncManager.addPeer(node1asPeer);
1136
+ node2.syncManager.addPeer(node1asPeer);
1071
1137
 
1072
- const mapOnNode2 = await mapOnNode2Promise;
1073
- if (mapOnNode2 === "unavailable") {
1074
- throw new Error("Map is unavailable");
1075
- }
1138
+ const mapOnNode2 = yield* Effect.promise(() => mapOnNode2Promise);
1139
+ if (mapOnNode2 === "unavailable") {
1140
+ throw new Error("Map is unavailable");
1141
+ }
1076
1142
 
1077
- expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual(
1078
- "world",
1079
- );
1080
- });
1143
+ expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual(
1144
+ "world",
1145
+ );
1146
+ }).pipe(Effect.scoped, Effect.runPromise));
1081
1147
 
1082
1148
  function groupContentEx(group: RawGroup) {
1083
1149
  return {