cojson 0.8.21 → 0.8.23
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/CHANGELOG.md +7 -0
- package/dist/native/CoValuesStore.js +31 -0
- package/dist/native/CoValuesStore.js.map +1 -0
- package/dist/native/PeerState.js +3 -0
- package/dist/native/PeerState.js.map +1 -1
- package/dist/native/SyncStateSubscriptionManager.js +2 -2
- package/dist/native/SyncStateSubscriptionManager.js.map +1 -1
- package/dist/native/coValueState.js +175 -27
- package/dist/native/coValueState.js.map +1 -1
- package/dist/native/localNode.js +20 -41
- package/dist/native/localNode.js.map +1 -1
- package/dist/native/sync.js +43 -82
- package/dist/native/sync.js.map +1 -1
- package/dist/web/CoValuesStore.js +31 -0
- package/dist/web/CoValuesStore.js.map +1 -0
- package/dist/web/PeerState.js +3 -0
- package/dist/web/PeerState.js.map +1 -1
- package/dist/web/SyncStateSubscriptionManager.js +2 -2
- package/dist/web/SyncStateSubscriptionManager.js.map +1 -1
- package/dist/web/coValueState.js +175 -27
- package/dist/web/coValueState.js.map +1 -1
- package/dist/web/localNode.js +20 -41
- package/dist/web/localNode.js.map +1 -1
- package/dist/web/sync.js +43 -82
- package/dist/web/sync.js.map +1 -1
- package/package.json +1 -1
- package/src/CoValuesStore.ts +38 -0
- package/src/PeerState.ts +4 -0
- package/src/SyncStateSubscriptionManager.ts +2 -2
- package/src/coValueState.ts +253 -42
- package/src/localNode.ts +28 -56
- package/src/sync.ts +58 -104
- package/src/tests/coValueState.test.ts +362 -0
- package/src/tests/group.test.ts +1 -1
- package/src/tests/sync.test.ts +123 -9
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
|
-
|
|
139
|
-
|
|
140
|
-
(peer) => peer.
|
|
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.
|
|
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
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
382
|
+
const entry = this.local.coValuesStore.get(msg.id);
|
|
409
383
|
|
|
410
|
-
if (
|
|
411
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
.
|
|
436
|
-
|
|
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 === "
|
|
448
|
-
|
|
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 (
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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 (
|
|
444
|
+
if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
|
|
491
445
|
if (msg.asDependencyOf) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
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
|
-
|
|
528
|
-
|
|
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.
|
|
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
|
|
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: "
|
|
513
|
+
type: "available",
|
|
559
514
|
coValue,
|
|
560
|
-
peerId: peer.id,
|
|
561
515
|
});
|
|
562
516
|
} else {
|
|
563
517
|
coValue = entry.state.coValue;
|
|
@@ -0,0 +1,362 @@
|
|
|
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 = { id: mockCoValueId } as CoValueCore;
|
|
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 = { id: mockCoValueId } as CoValueCore;
|
|
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: { id: mockCoValueId } as CoValueCore,
|
|
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: { id: mockCoValueId } as CoValueCore,
|
|
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: { id: mockCoValueId } as CoValueCore,
|
|
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
|
+
|
|
343
|
+
function createMockPeerState(
|
|
344
|
+
peer: Partial<Peer>,
|
|
345
|
+
pushFn = () => Promise.resolve(),
|
|
346
|
+
) {
|
|
347
|
+
const peerState = new PeerState(
|
|
348
|
+
{
|
|
349
|
+
id: "peer",
|
|
350
|
+
role: "server",
|
|
351
|
+
outgoing: {
|
|
352
|
+
push: pushFn,
|
|
353
|
+
},
|
|
354
|
+
...peer,
|
|
355
|
+
} as Peer,
|
|
356
|
+
undefined,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
vi.spyOn(peerState, "pushOutgoingMessage").mockImplementation(pushFn);
|
|
360
|
+
|
|
361
|
+
return peerState;
|
|
362
|
+
}
|
package/src/tests/group.test.ts
CHANGED
|
@@ -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
|
|
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();
|