cojson 0.8.21 → 0.8.27

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.
package/src/sync.ts CHANGED
@@ -2,7 +2,6 @@ import { PeerState } from "./PeerState.js";
2
2
  import { SyncStateSubscriptionManager } from "./SyncStateSubscriptionManager.js";
3
3
  import { CoValueHeader, Transaction } from "./coValueCore.js";
4
4
  import { CoValueCore } from "./coValueCore.js";
5
- import { CoValueState } from "./coValueState.js";
6
5
  import { Signature } from "./crypto/crypto.js";
7
6
  import { RawCoID, SessionID } from "./ids.js";
8
7
  import { LocalNode } from "./localNode.js";
@@ -79,6 +78,7 @@ export interface Peer {
79
78
  role: "peer" | "server" | "client" | "storage";
80
79
  priority?: number;
81
80
  crashOnClose: boolean;
81
+ deletePeerStateOnClose?: boolean;
82
82
  }
83
83
 
84
84
  export function combinedKnownStates(
@@ -135,31 +135,10 @@ export class SyncManager {
135
135
  return Object.values(this.peers);
136
136
  }
137
137
 
138
- async loadFromPeers(id: RawCoID, forPeer?: PeerID) {
139
- const eligiblePeers = this.peersInPriorityOrder().filter(
140
- (peer) => peer.id !== forPeer && peer.isServerOrStoragePeer(),
138
+ getServerAndStoragePeers(excludePeerId?: PeerID): PeerState[] {
139
+ return this.peersInPriorityOrder().filter(
140
+ (peer) => peer.isServerOrStoragePeer() && peer.id !== excludePeerId,
141
141
  );
142
-
143
- const coValueEntry = this.local.coValues[id];
144
-
145
- for (const peer of eligiblePeers) {
146
- if (peer.erroredCoValues.has(id)) {
147
- console.error(
148
- `Skipping load on errored coValue ${id} from peer ${peer.id}`,
149
- );
150
- continue;
151
- }
152
- await peer.pushOutgoingMessage({
153
- action: "load",
154
- id: id,
155
- header: false,
156
- sessions: {},
157
- });
158
-
159
- if (coValueEntry?.state.type === "unknown") {
160
- await coValueEntry.state.waitForPeer(peer.id);
161
- }
162
- }
163
142
  }
164
143
 
165
144
  async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
@@ -192,19 +171,10 @@ export class SyncManager {
192
171
  }
193
172
 
194
173
  async subscribeToIncludingDependencies(id: RawCoID, peer: PeerState) {
195
- const entry = this.local.coValues[id];
196
-
197
- if (!entry) {
198
- throw new Error("Expected coValue entry on subscribe");
199
- }
174
+ const entry = this.local.coValuesStore.get(id);
200
175
 
201
- if (entry.state.type === "unknown") {
202
- this.trySendToPeer(peer, {
203
- action: "load",
204
- id,
205
- header: false,
206
- sessions: {},
207
- }).catch((e: unknown) => {
176
+ if (entry.state.type !== "available") {
177
+ entry.loadFromPeers([peer]).catch((e: unknown) => {
208
178
  console.error("Error sending load", e);
209
179
  });
210
180
  return;
@@ -334,7 +304,7 @@ export class SyncManager {
334
304
 
335
305
  if (peerState.isServerOrStoragePeer()) {
336
306
  const initialSync = async () => {
337
- for (const id of Object.keys(this.local.coValues) as RawCoID[]) {
307
+ for (const id of this.local.coValuesStore.getKeys()) {
338
308
  // console.log("subscribing to after peer added", id, peer.id)
339
309
  await this.subscribeToIncludingDependencies(id, peerState);
340
310
 
@@ -392,6 +362,10 @@ export class SyncManager {
392
362
  const state = this.peers[peer.id];
393
363
  state?.gracefulShutdown();
394
364
  unsubscribeFromKnownStatesUpdates();
365
+
366
+ if (peer.deletePeerStateOnClose) {
367
+ delete this.peers[peer.id];
368
+ }
395
369
  });
396
370
  }
397
371
 
@@ -405,55 +379,33 @@ export class SyncManager {
405
379
  id: msg.id,
406
380
  value: knownStateIn(msg),
407
381
  });
408
- let entry = this.local.coValues[msg.id];
382
+ const entry = this.local.coValuesStore.get(msg.id);
409
383
 
410
- if (!entry) {
411
- // console.log(`Loading ${msg.id} from all peers except ${peer.id}`);
384
+ if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
385
+ const eligiblePeers = this.getServerAndStoragePeers(peer.id);
412
386
 
413
- // special case: we should be able to solve this much more neatly
414
- // with an explicit state machine in the future
415
- const eligiblePeers = this.peersInPriorityOrder().filter(
416
- (other) => other.id !== peer.id && other.isServerOrStoragePeer(),
417
- );
418
387
  if (eligiblePeers.length === 0) {
388
+ // If the load request contains a header or any session data
389
+ // and we don't have any eligible peers to load the coValue from
390
+ // we try to load it from the sender because it is the only place
391
+ // where we can get informations about the coValue
419
392
  if (msg.header || Object.keys(msg.sessions).length > 0) {
420
- this.local.coValues[msg.id] = CoValueState.Unknown(
421
- new Set([peer.id]),
422
- );
423
- this.trySendToPeer(peer, {
424
- action: "known",
425
- id: msg.id,
426
- header: false,
427
- sessions: {},
428
- }).catch((e) => {
429
- console.error("Error sending known state", e);
393
+ entry.loadFromPeers([peer]).catch((e) => {
394
+ console.error("Error loading coValue in handleLoad", e);
430
395
  });
431
396
  }
432
397
  return;
433
398
  } else {
434
- this.local
435
- .loadCoValueCore(msg.id, {
436
- dontLoadFrom: peer.id,
437
- dontWaitFor: peer.id,
438
- })
439
- .catch((e) => {
440
- console.error("Error loading coValue in handleLoad", e);
441
- });
399
+ this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
400
+ console.error("Error loading coValue in handleLoad", e);
401
+ });
442
402
  }
443
-
444
- entry = this.local.coValues[msg.id]!;
445
403
  }
446
404
 
447
- if (entry.state.type === "unknown") {
448
- // console.debug(
449
- // "Waiting for loaded",
450
- // msg.id,
451
- // "after message from",
452
- // peer.id,
453
- // );
454
- const loaded = await entry.state.ready;
405
+ if (entry.state.type === "loading") {
406
+ const value = await entry.getCoValue();
455
407
 
456
- if (loaded === "unavailable") {
408
+ if (value === "unavailable") {
457
409
  peer.dispatchToKnownStates({
458
410
  type: "SET",
459
411
  id: msg.id,
@@ -474,12 +426,14 @@ export class SyncManager {
474
426
  }
475
427
  }
476
428
 
477
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
478
- await this.sendNewContentIncludingDependencies(msg.id, peer);
429
+ if (entry.state.type === "available") {
430
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
431
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
432
+ }
479
433
  }
480
434
 
481
435
  async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
482
- let entry = this.local.coValues[msg.id];
436
+ const entry = this.local.coValuesStore.get(msg.id);
483
437
 
484
438
  peer.dispatchToKnownStates({
485
439
  type: "COMBINE_WITH",
@@ -487,18 +441,22 @@ export class SyncManager {
487
441
  value: knownStateIn(msg),
488
442
  });
489
443
 
490
- if (!entry) {
444
+ if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
491
445
  if (msg.asDependencyOf) {
492
- if (this.local.coValues[msg.asDependencyOf]) {
493
- this.local
494
- .loadCoValueCore(msg.id, { dontLoadFrom: peer.id })
495
- .catch((e) => {
496
- console.error(
497
- `Error loading coValue ${msg.id} to create loading state, as dependency of ${msg.asDependencyOf}`,
498
- e,
499
- );
500
- });
501
- entry = this.local.coValues[msg.id]!; // must exist after loadCoValueCore
446
+ const dependencyEntry = this.local.coValuesStore.get(
447
+ msg.asDependencyOf,
448
+ );
449
+
450
+ if (
451
+ dependencyEntry.state.type === "available" ||
452
+ dependencyEntry.state.type === "loading"
453
+ ) {
454
+ this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
455
+ console.error(
456
+ `Error loading coValue ${msg.id} to create loading state, as dependency of ${msg.asDependencyOf}`,
457
+ e,
458
+ );
459
+ });
502
460
  } else {
503
461
  throw new Error(
504
462
  "Expected coValue dependency entry to be created, missing subscribe?",
@@ -511,12 +469,14 @@ export class SyncManager {
511
469
  }
512
470
  }
513
471
 
514
- if (entry.state.type === "unknown") {
472
+ // The header is a boolean value that tells us if the other peer do have information about the header.
473
+ // If it's false in this point it means that the coValue is unavailable on the other peer.
474
+ if (entry.state.type !== "available") {
515
475
  const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
516
476
 
517
477
  if (!availableOnPeer) {
518
478
  entry.dispatch({
519
- type: "not-found",
479
+ type: "not-found-in-peer",
520
480
  peerId: peer.id,
521
481
  });
522
482
  }
@@ -524,23 +484,18 @@ export class SyncManager {
524
484
  return;
525
485
  }
526
486
 
527
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
528
- await this.sendNewContentIncludingDependencies(msg.id, peer);
487
+ if (entry.state.type === "available") {
488
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
489
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
490
+ }
529
491
  }
530
492
 
531
493
  async handleNewContent(msg: NewContentMessage, peer: PeerState) {
532
- const entry = this.local.coValues[msg.id];
533
-
534
- if (!entry) {
535
- console.error(
536
- `Expected coValue entry for ${msg.id} to be created on new content, missing subscribe?`,
537
- );
538
- return;
539
- }
494
+ const entry = this.local.coValuesStore.get(msg.id);
540
495
 
541
496
  let coValue: CoValueCore;
542
497
 
543
- if (entry.state.type === "unknown") {
498
+ if (entry.state.type !== "available") {
544
499
  if (!msg.header) {
545
500
  console.error("Expected header to be sent in first message");
546
501
  return;
@@ -555,9 +510,8 @@ export class SyncManager {
555
510
  coValue = new CoValueCore(msg.header, this.local);
556
511
 
557
512
  entry.dispatch({
558
- type: "found",
513
+ type: "available",
559
514
  coValue,
560
- peerId: peer.id,
561
515
  });
562
516
  } else {
563
517
  coValue = entry.state.coValue;
@@ -0,0 +1,427 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { PeerState } from "../PeerState";
3
+ import { CoValueCore } from "../coValueCore";
4
+ import { CO_VALUE_LOADING_MAX_RETRIES, CoValueState } from "../coValueState";
5
+ import { RawCoID } from "../ids";
6
+ import { Peer } from "../sync";
7
+
8
+ describe("CoValueState", () => {
9
+ const mockCoValueId = "co_test123" as RawCoID;
10
+
11
+ test("should create unknown state", () => {
12
+ const state = CoValueState.Unknown(mockCoValueId);
13
+
14
+ expect(state.id).toBe(mockCoValueId);
15
+ expect(state.state.type).toBe("unknown");
16
+ });
17
+
18
+ test("should create loading state", () => {
19
+ const peerIds = ["peer1", "peer2"];
20
+ const state = CoValueState.Loading(mockCoValueId, peerIds);
21
+
22
+ expect(state.id).toBe(mockCoValueId);
23
+ expect(state.state.type).toBe("loading");
24
+ });
25
+
26
+ test("should create available state", async () => {
27
+ const mockCoValue = createMockCoValueCore(mockCoValueId);
28
+ const state = CoValueState.Available(mockCoValue);
29
+
30
+ expect(state.id).toBe(mockCoValueId);
31
+ expect(state.state.type).toBe("available");
32
+ expect((state.state as any).coValue).toBe(mockCoValue);
33
+ await expect(state.getCoValue()).resolves.toEqual(mockCoValue);
34
+ });
35
+
36
+ test("should handle found action", async () => {
37
+ const mockCoValue = createMockCoValueCore(mockCoValueId);
38
+ const state = CoValueState.Loading(mockCoValueId, ["peer1", "peer2"]);
39
+
40
+ const stateValuePromise = state.getCoValue();
41
+
42
+ state.dispatch({
43
+ type: "available",
44
+ coValue: mockCoValue,
45
+ });
46
+
47
+ const result = await state.getCoValue();
48
+ expect(result).toBe(mockCoValue);
49
+ await expect(stateValuePromise).resolves.toBe(mockCoValue);
50
+ });
51
+
52
+ test("should ignore actions when not in loading state", () => {
53
+ const state = CoValueState.Unknown(mockCoValueId);
54
+
55
+ state.dispatch({
56
+ type: "not-found-in-peer",
57
+ peerId: "peer1",
58
+ });
59
+
60
+ expect(state.state.type).toBe("unknown");
61
+ });
62
+
63
+ test("should retry loading from peers when unsuccessful", async () => {
64
+ vi.useFakeTimers();
65
+
66
+ const peer1 = createMockPeerState(
67
+ {
68
+ id: "peer1",
69
+ role: "server",
70
+ },
71
+ async () => {
72
+ state.dispatch({
73
+ type: "not-found-in-peer",
74
+ peerId: "peer1",
75
+ });
76
+ },
77
+ );
78
+ const peer2 = createMockPeerState(
79
+ {
80
+ id: "peer2",
81
+ role: "server",
82
+ },
83
+ async () => {
84
+ state.dispatch({
85
+ type: "not-found-in-peer",
86
+ peerId: "peer2",
87
+ });
88
+ },
89
+ );
90
+ const mockPeers = [peer1, peer2] as unknown as PeerState[];
91
+
92
+ const state = CoValueState.Unknown(mockCoValueId);
93
+ const loadPromise = state.loadFromPeers(mockPeers);
94
+
95
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
96
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
97
+ await vi.runAllTimersAsync();
98
+ }
99
+
100
+ await loadPromise;
101
+
102
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(
103
+ CO_VALUE_LOADING_MAX_RETRIES,
104
+ );
105
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
106
+ CO_VALUE_LOADING_MAX_RETRIES,
107
+ );
108
+ expect(state.state.type).toBe("unavailable");
109
+ await expect(state.getCoValue()).resolves.toBe("unavailable");
110
+
111
+ vi.useRealTimers();
112
+ });
113
+
114
+ test("should skip errored coValues when loading from peers", async () => {
115
+ vi.useFakeTimers();
116
+
117
+ const peer1 = createMockPeerState(
118
+ {
119
+ id: "peer1",
120
+ role: "server",
121
+ },
122
+ async () => {
123
+ peer1.erroredCoValues.set(mockCoValueId, new Error("test") as any);
124
+ state.dispatch({
125
+ type: "not-found-in-peer",
126
+ peerId: "peer1",
127
+ });
128
+ },
129
+ );
130
+ const peer2 = createMockPeerState(
131
+ {
132
+ id: "peer2",
133
+ role: "server",
134
+ },
135
+ async () => {
136
+ state.dispatch({
137
+ type: "not-found-in-peer",
138
+ peerId: "peer2",
139
+ });
140
+ },
141
+ );
142
+
143
+ const mockPeers = [peer1, peer2] as unknown as PeerState[];
144
+
145
+ const state = CoValueState.Unknown(mockCoValueId);
146
+ const loadPromise = state.loadFromPeers(mockPeers);
147
+
148
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
149
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
150
+ await vi.runAllTimersAsync();
151
+ }
152
+
153
+ await loadPromise;
154
+
155
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(1);
156
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
157
+ CO_VALUE_LOADING_MAX_RETRIES,
158
+ );
159
+ expect(state.state.type).toBe("unavailable");
160
+ await expect(state.getCoValue()).resolves.toBe("unavailable");
161
+
162
+ vi.useRealTimers();
163
+ });
164
+
165
+ test("should retry only on server peers", async () => {
166
+ vi.useFakeTimers();
167
+
168
+ const peer1 = createMockPeerState(
169
+ {
170
+ id: "peer1",
171
+ role: "storage",
172
+ },
173
+ async () => {
174
+ state.dispatch({
175
+ type: "not-found-in-peer",
176
+ peerId: "peer1",
177
+ });
178
+ },
179
+ );
180
+ const peer2 = createMockPeerState(
181
+ {
182
+ id: "peer2",
183
+ role: "server",
184
+ },
185
+ async () => {
186
+ state.dispatch({
187
+ type: "not-found-in-peer",
188
+ peerId: "peer2",
189
+ });
190
+ },
191
+ );
192
+ const mockPeers = [peer1, peer2] as unknown as PeerState[];
193
+
194
+ const state = CoValueState.Unknown(mockCoValueId);
195
+ const loadPromise = state.loadFromPeers(mockPeers);
196
+
197
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
198
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
199
+ await vi.runAllTimersAsync();
200
+ }
201
+
202
+ await loadPromise;
203
+
204
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(1);
205
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
206
+ CO_VALUE_LOADING_MAX_RETRIES,
207
+ );
208
+ expect(state.state.type).toBe("unavailable");
209
+ await expect(state.getCoValue()).resolves.toEqual("unavailable");
210
+
211
+ vi.useRealTimers();
212
+ });
213
+
214
+ test("should handle the coValues that become available in between of the retries", async () => {
215
+ vi.useFakeTimers();
216
+
217
+ let retries = 0;
218
+
219
+ const peer1 = createMockPeerState(
220
+ {
221
+ id: "peer1",
222
+ role: "server",
223
+ },
224
+ async () => {
225
+ retries++;
226
+ state.dispatch({
227
+ type: "not-found-in-peer",
228
+ peerId: "peer1",
229
+ });
230
+
231
+ if (retries === 2) {
232
+ setTimeout(() => {
233
+ state.dispatch({
234
+ type: "available",
235
+ coValue: createMockCoValueCore(mockCoValueId),
236
+ });
237
+ }, 100);
238
+ }
239
+ },
240
+ );
241
+
242
+ const mockPeers = [peer1] as unknown as PeerState[];
243
+
244
+ const state = CoValueState.Unknown(mockCoValueId);
245
+ const loadPromise = state.loadFromPeers(mockPeers);
246
+
247
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
248
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES + 1; i++) {
249
+ await vi.runAllTimersAsync();
250
+ }
251
+
252
+ await loadPromise;
253
+
254
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(2);
255
+ expect(state.state.type).toBe("available");
256
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
257
+ vi.useRealTimers();
258
+ });
259
+
260
+ test("should have a coValue as value property when becomes available after that have been marked as unavailable", async () => {
261
+ vi.useFakeTimers();
262
+
263
+ const peer1 = createMockPeerState(
264
+ {
265
+ id: "peer1",
266
+ role: "server",
267
+ },
268
+ async () => {
269
+ state.dispatch({
270
+ type: "not-found-in-peer",
271
+ peerId: "peer1",
272
+ });
273
+ },
274
+ );
275
+
276
+ const mockPeers = [peer1] as unknown as PeerState[];
277
+
278
+ const state = CoValueState.Unknown(mockCoValueId);
279
+ const loadPromise = state.loadFromPeers(mockPeers);
280
+
281
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
282
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
283
+ await vi.runAllTimersAsync();
284
+ }
285
+
286
+ state.dispatch({
287
+ type: "available",
288
+ coValue: createMockCoValueCore(mockCoValueId),
289
+ });
290
+
291
+ await loadPromise;
292
+
293
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(5);
294
+ expect(state.state.type).toBe("available");
295
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
296
+
297
+ vi.useRealTimers();
298
+ });
299
+
300
+ test("should stop retrying when value becomes available", async () => {
301
+ vi.useFakeTimers();
302
+
303
+ let run = 1;
304
+
305
+ const peer1 = createMockPeerState(
306
+ {
307
+ id: "peer1",
308
+ role: "server",
309
+ },
310
+ async () => {
311
+ if (run > 2) {
312
+ state.dispatch({
313
+ type: "available",
314
+ coValue: createMockCoValueCore(mockCoValueId),
315
+ });
316
+ }
317
+ state.dispatch({
318
+ type: "not-found-in-peer",
319
+ peerId: "peer1",
320
+ });
321
+ run++;
322
+ },
323
+ );
324
+
325
+ const mockPeers = [peer1] as unknown as PeerState[];
326
+
327
+ const state = CoValueState.Unknown(mockCoValueId);
328
+ const loadPromise = state.loadFromPeers(mockPeers);
329
+
330
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
331
+ await vi.runAllTimersAsync();
332
+ }
333
+ await loadPromise;
334
+
335
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(3);
336
+ expect(state.state.type).toBe("available");
337
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
338
+
339
+ vi.useRealTimers();
340
+ });
341
+
342
+ test("should start sending the known state to peers when available", async () => {
343
+ vi.useFakeTimers();
344
+
345
+ const mockCoValue = createMockCoValueCore(mockCoValueId);
346
+
347
+ const peer1 = createMockPeerState(
348
+ {
349
+ id: "peer1",
350
+ role: "storage",
351
+ },
352
+ async () => {
353
+ state.dispatch({
354
+ type: "available",
355
+ coValue: mockCoValue,
356
+ });
357
+ },
358
+ );
359
+ const peer2 = createMockPeerState(
360
+ {
361
+ id: "peer1",
362
+ role: "server",
363
+ },
364
+ async () => {
365
+ state.dispatch({
366
+ type: "not-found-in-peer",
367
+ peerId: "peer2",
368
+ });
369
+ },
370
+ );
371
+
372
+ const state = CoValueState.Unknown(mockCoValueId);
373
+ const loadPromise = state.loadFromPeers([peer1, peer2]);
374
+
375
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
376
+ await vi.runAllTimersAsync();
377
+ }
378
+ await loadPromise;
379
+
380
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(1);
381
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(1);
382
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledWith({
383
+ action: "load",
384
+ ...mockCoValue.knownState(),
385
+ });
386
+ expect(state.state.type).toBe("available");
387
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
388
+
389
+ vi.useRealTimers();
390
+ });
391
+ });
392
+
393
+ function createMockPeerState(
394
+ peer: Partial<Peer>,
395
+ pushFn = () => Promise.resolve(),
396
+ ) {
397
+ const peerState = new PeerState(
398
+ {
399
+ id: "peer",
400
+ role: "server",
401
+ outgoing: {
402
+ push: pushFn,
403
+ },
404
+ ...peer,
405
+ } as Peer,
406
+ undefined,
407
+ );
408
+
409
+ vi.spyOn(peerState, "pushOutgoingMessage").mockImplementation(pushFn);
410
+
411
+ return peerState;
412
+ }
413
+
414
+ function createMockCoValueCore(mockCoValueId: string) {
415
+ // Setting the knownState as part of the prototype to simplify
416
+ // the equality checks
417
+ const mockCoValue = Object.create({
418
+ knownState: vi.fn().mockReturnValue({
419
+ id: mockCoValueId,
420
+ header: true,
421
+ sessions: {},
422
+ }),
423
+ });
424
+
425
+ mockCoValue.id = mockCoValueId;
426
+ return mockCoValue as unknown as CoValueCore;
427
+ }
@@ -42,7 +42,7 @@ test("Can create a CoStream in a group", () => {
42
42
  expect(stream instanceof RawCoStream).toEqual(true);
43
43
  });
44
44
 
45
- test("Can create a BinaryCoStream in a group", () => {
45
+ test("Can create a FileStream in a group", () => {
46
46
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
47
47
 
48
48
  const group = node.createGroup();