cojson 0.12.0 → 0.13.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +22 -0
- package/dist/PriorityBasedMessageQueue.d.ts +16 -0
- package/dist/PriorityBasedMessageQueue.d.ts.map +1 -1
- package/dist/PriorityBasedMessageQueue.js +50 -1
- package/dist/PriorityBasedMessageQueue.js.map +1 -1
- package/dist/coValueCore.d.ts +1 -5
- package/dist/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore.js +29 -43
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValueState.d.ts.map +1 -1
- package/dist/coValueState.js +18 -4
- package/dist/coValueState.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +21 -28
- package/dist/sync.js.map +1 -1
- package/dist/tests/LinkedList.test.d.ts +2 -0
- package/dist/tests/LinkedList.test.d.ts.map +1 -0
- package/dist/tests/LinkedList.test.js +84 -0
- package/dist/tests/LinkedList.test.js.map +1 -0
- package/dist/tests/sync.test.js +51 -12
- package/dist/tests/sync.test.js.map +1 -1
- package/package.json +1 -1
- package/src/PriorityBasedMessageQueue.ts +62 -2
- package/src/coValueCore.ts +50 -60
- package/src/coValueState.ts +23 -4
- package/src/sync.ts +21 -38
- package/src/tests/LinkedList.test.ts +96 -0
- package/src/tests/sync.test.ts +64 -21
|
@@ -33,10 +33,70 @@ type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
|
|
|
33
33
|
}
|
|
34
34
|
? A
|
|
35
35
|
: Tuple<T, N, [...A, T]>;
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
type QueueTuple = Tuple<LinkedList<QueueEntry>, 8>;
|
|
38
|
+
|
|
39
|
+
type LinkedListNode<T> = {
|
|
40
|
+
value: T;
|
|
41
|
+
next: LinkedListNode<T> | undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Using a linked list to make the shift operation O(1) instead of O(n)
|
|
46
|
+
* as our queues can grow very large when the system is under pressure.
|
|
47
|
+
*/
|
|
48
|
+
export class LinkedList<T> {
|
|
49
|
+
head: LinkedListNode<T> | undefined = undefined;
|
|
50
|
+
tail: LinkedListNode<T> | undefined = undefined;
|
|
51
|
+
length = 0;
|
|
52
|
+
|
|
53
|
+
push(value: T) {
|
|
54
|
+
const node = { value, next: undefined };
|
|
55
|
+
|
|
56
|
+
if (this.head === undefined) {
|
|
57
|
+
this.head = node;
|
|
58
|
+
this.tail = node;
|
|
59
|
+
} else if (this.tail) {
|
|
60
|
+
this.tail.next = node;
|
|
61
|
+
this.tail = node;
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error("LinkedList is corrupted");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.length++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
shift() {
|
|
70
|
+
if (!this.head) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const node = this.head;
|
|
75
|
+
const value = node.value;
|
|
76
|
+
this.head = node.next;
|
|
77
|
+
node.next = undefined;
|
|
78
|
+
|
|
79
|
+
if (this.head === undefined) {
|
|
80
|
+
this.tail = undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.length--;
|
|
84
|
+
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
37
88
|
|
|
38
89
|
export class PriorityBasedMessageQueue {
|
|
39
|
-
private queues: QueueTuple = [
|
|
90
|
+
private queues: QueueTuple = [
|
|
91
|
+
new LinkedList<QueueEntry>(),
|
|
92
|
+
new LinkedList<QueueEntry>(),
|
|
93
|
+
new LinkedList<QueueEntry>(),
|
|
94
|
+
new LinkedList<QueueEntry>(),
|
|
95
|
+
new LinkedList<QueueEntry>(),
|
|
96
|
+
new LinkedList<QueueEntry>(),
|
|
97
|
+
new LinkedList<QueueEntry>(),
|
|
98
|
+
new LinkedList<QueueEntry>(),
|
|
99
|
+
];
|
|
40
100
|
queueSizeCounter = metrics
|
|
41
101
|
.getMeter("cojson")
|
|
42
102
|
.createUpDownCounter("jazz.messagequeue.size", {
|
package/src/coValueCore.ts
CHANGED
|
@@ -199,6 +199,7 @@ export class CoValueCore {
|
|
|
199
199
|
givenExpectedNewHash: Hash | undefined,
|
|
200
200
|
newSignature: Signature,
|
|
201
201
|
skipVerify: boolean = false,
|
|
202
|
+
givenNewStreamingHash?: StreamingHash,
|
|
202
203
|
): Result<true, TryAddTransactionsError> {
|
|
203
204
|
return this.node
|
|
204
205
|
.resolveAccountAgent(
|
|
@@ -208,42 +209,55 @@ export class CoValueCore {
|
|
|
208
209
|
.andThen((agent) => {
|
|
209
210
|
const signerID = this.crypto.getAgentSignerID(agent);
|
|
210
211
|
|
|
211
|
-
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
|
212
|
-
sessionID,
|
|
213
|
-
newTransactions,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
|
217
|
-
return err({
|
|
218
|
-
type: "InvalidHash",
|
|
219
|
-
id: this.id,
|
|
220
|
-
expectedNewHash,
|
|
221
|
-
givenExpectedNewHash,
|
|
222
|
-
} satisfies InvalidHashError);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
212
|
if (
|
|
226
|
-
skipVerify
|
|
227
|
-
|
|
213
|
+
skipVerify === true &&
|
|
214
|
+
givenNewStreamingHash &&
|
|
215
|
+
givenExpectedNewHash
|
|
228
216
|
) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
217
|
+
this.doAddTransactions(
|
|
218
|
+
sessionID,
|
|
219
|
+
newTransactions,
|
|
232
220
|
newSignature,
|
|
221
|
+
givenExpectedNewHash,
|
|
222
|
+
givenNewStreamingHash,
|
|
223
|
+
"immediate",
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
const { expectedNewHash, newStreamingHash } =
|
|
227
|
+
this.expectedNewHashAfter(sessionID, newTransactions);
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
givenExpectedNewHash &&
|
|
231
|
+
givenExpectedNewHash !== expectedNewHash
|
|
232
|
+
) {
|
|
233
|
+
return err({
|
|
234
|
+
type: "InvalidHash",
|
|
235
|
+
id: this.id,
|
|
236
|
+
expectedNewHash,
|
|
237
|
+
givenExpectedNewHash,
|
|
238
|
+
} satisfies InvalidHashError);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
|
|
242
|
+
return err({
|
|
243
|
+
type: "InvalidSignature",
|
|
244
|
+
id: this.id,
|
|
245
|
+
newSignature,
|
|
246
|
+
sessionID,
|
|
247
|
+
signerID,
|
|
248
|
+
} satisfies InvalidSignatureError);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.doAddTransactions(
|
|
233
252
|
sessionID,
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
newTransactions,
|
|
254
|
+
newSignature,
|
|
255
|
+
expectedNewHash,
|
|
256
|
+
newStreamingHash,
|
|
257
|
+
"immediate",
|
|
258
|
+
);
|
|
236
259
|
}
|
|
237
260
|
|
|
238
|
-
this.doAddTransactions(
|
|
239
|
-
sessionID,
|
|
240
|
-
newTransactions,
|
|
241
|
-
newSignature,
|
|
242
|
-
expectedNewHash,
|
|
243
|
-
newStreamingHash,
|
|
244
|
-
"immediate",
|
|
245
|
-
);
|
|
246
|
-
|
|
247
261
|
return ok(true as const);
|
|
248
262
|
});
|
|
249
263
|
}
|
|
@@ -370,40 +384,14 @@ export class CoValueCore {
|
|
|
370
384
|
const streamingHash =
|
|
371
385
|
this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
|
|
372
386
|
new StreamingHash(this.crypto);
|
|
373
|
-
for (const transaction of newTransactions) {
|
|
374
|
-
streamingHash.update(transaction);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const newStreamingHash = streamingHash.clone();
|
|
378
387
|
|
|
379
|
-
return {
|
|
380
|
-
expectedNewHash: streamingHash.digest(),
|
|
381
|
-
newStreamingHash,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async expectedNewHashAfterAsync(
|
|
386
|
-
sessionID: SessionID,
|
|
387
|
-
newTransactions: Transaction[],
|
|
388
|
-
): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
|
|
389
|
-
const streamingHash =
|
|
390
|
-
this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
|
|
391
|
-
new StreamingHash(this.crypto);
|
|
392
|
-
let before = performance.now();
|
|
393
388
|
for (const transaction of newTransactions) {
|
|
394
389
|
streamingHash.update(transaction);
|
|
395
|
-
const after = performance.now();
|
|
396
|
-
if (after - before > 1) {
|
|
397
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
398
|
-
before = performance.now();
|
|
399
|
-
}
|
|
400
390
|
}
|
|
401
391
|
|
|
402
|
-
const newStreamingHash = streamingHash.clone();
|
|
403
|
-
|
|
404
392
|
return {
|
|
405
393
|
expectedNewHash: streamingHash.digest(),
|
|
406
|
-
newStreamingHash,
|
|
394
|
+
newStreamingHash: streamingHash,
|
|
407
395
|
};
|
|
408
396
|
}
|
|
409
397
|
|
|
@@ -452,9 +440,10 @@ export class CoValueCore {
|
|
|
452
440
|
) as SessionID)
|
|
453
441
|
: this.node.currentSessionID;
|
|
454
442
|
|
|
455
|
-
const { expectedNewHash } = this.expectedNewHashAfter(
|
|
456
|
-
|
|
457
|
-
|
|
443
|
+
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
|
444
|
+
sessionID,
|
|
445
|
+
[transaction],
|
|
446
|
+
);
|
|
458
447
|
|
|
459
448
|
const signature = this.crypto.sign(
|
|
460
449
|
this.node.account.currentSignerSecret(),
|
|
@@ -467,6 +456,7 @@ export class CoValueCore {
|
|
|
467
456
|
expectedNewHash,
|
|
468
457
|
signature,
|
|
469
458
|
true,
|
|
459
|
+
newStreamingHash,
|
|
470
460
|
)._unsafeUnwrap({ withStackTrace: true });
|
|
471
461
|
|
|
472
462
|
if (success) {
|
package/src/coValueState.ts
CHANGED
|
@@ -176,11 +176,12 @@ export class CoValueState {
|
|
|
176
176
|
async loadFromPeers(peers: PeerState[]) {
|
|
177
177
|
const state = this.state;
|
|
178
178
|
|
|
179
|
-
if (state.type
|
|
179
|
+
if (state.type === "loading" || state.type === "available") {
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
if (peers.length === 0) {
|
|
184
|
+
this.moveToState(new CoValueUnavailableState());
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -192,7 +193,11 @@ export class CoValueState {
|
|
|
192
193
|
|
|
193
194
|
// If we are in the loading state we move to a new loading state
|
|
194
195
|
// to reset all the loading promises
|
|
195
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
this.state.type === "loading" ||
|
|
198
|
+
this.state.type === "unknown" ||
|
|
199
|
+
this.state.type === "unavailable"
|
|
200
|
+
) {
|
|
196
201
|
this.moveToState(
|
|
197
202
|
new CoValueLoadingState(peersWithoutErrors.map((p) => p.id)),
|
|
198
203
|
);
|
|
@@ -308,6 +313,19 @@ async function loadCoValueFromPeers(
|
|
|
308
313
|
}
|
|
309
314
|
|
|
310
315
|
if (coValueEntry.state.type === "loading") {
|
|
316
|
+
const { promise, resolve } = createResolvablePromise<void>();
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Use a very long timeout for storage peers, because under pressure
|
|
320
|
+
* they may take a long time to consume the messages queue
|
|
321
|
+
*
|
|
322
|
+
* TODO: Track errors on storage and do not rely on timeout
|
|
323
|
+
*/
|
|
324
|
+
const timeoutDuration =
|
|
325
|
+
peer.role === "storage"
|
|
326
|
+
? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
|
|
327
|
+
: CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
|
328
|
+
|
|
311
329
|
const timeout = setTimeout(() => {
|
|
312
330
|
if (coValueEntry.state.type === "loading") {
|
|
313
331
|
logger.warn("Failed to load coValue from peer", {
|
|
@@ -319,9 +337,10 @@ async function loadCoValueFromPeers(
|
|
|
319
337
|
type: "not-found-in-peer",
|
|
320
338
|
peerId: peer.id,
|
|
321
339
|
});
|
|
340
|
+
resolve();
|
|
322
341
|
}
|
|
323
|
-
},
|
|
324
|
-
await coValueEntry.state.waitForPeer(peer.id);
|
|
342
|
+
}, timeoutDuration);
|
|
343
|
+
await Promise.race([promise, coValueEntry.state.waitForPeer(peer.id)]);
|
|
325
344
|
clearTimeout(timeout);
|
|
326
345
|
}
|
|
327
346
|
}
|
package/src/sync.ts
CHANGED
|
@@ -23,10 +23,6 @@ export function emptyKnownState(id: RawCoID): CoValueKnownState {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function getErrorMessage(e: unknown) {
|
|
27
|
-
return e instanceof Error ? e.message : "Unknown error";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
26
|
export type SyncMessage =
|
|
31
27
|
| LoadMessage
|
|
32
28
|
| KnownStateMessage
|
|
@@ -415,7 +411,20 @@ export class SyncManager {
|
|
|
415
411
|
entry.loadFromPeers([peer]).catch((e) => {
|
|
416
412
|
logger.error("Error loading coValue in handleLoad", { err: e });
|
|
417
413
|
});
|
|
414
|
+
} else {
|
|
415
|
+
// We don't have any eligible peers to load the coValue from
|
|
416
|
+
// so we send a known state back to the sender to let it know
|
|
417
|
+
// that the coValue is unavailable
|
|
418
|
+
this.trySendToPeer(peer, {
|
|
419
|
+
action: "known",
|
|
420
|
+
id: msg.id,
|
|
421
|
+
header: false,
|
|
422
|
+
sessions: {},
|
|
423
|
+
}).catch((e) => {
|
|
424
|
+
logger.error("Error sending known state back", { err: e });
|
|
425
|
+
});
|
|
418
426
|
}
|
|
427
|
+
|
|
419
428
|
return;
|
|
420
429
|
} else {
|
|
421
430
|
this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
|
|
@@ -460,6 +469,13 @@ export class SyncManager {
|
|
|
460
469
|
err: e,
|
|
461
470
|
});
|
|
462
471
|
});
|
|
472
|
+
} else if (entry.state.type === "unavailable") {
|
|
473
|
+
this.trySendToPeer(peer, {
|
|
474
|
+
action: "known",
|
|
475
|
+
id: msg.id,
|
|
476
|
+
header: false,
|
|
477
|
+
sessions: {},
|
|
478
|
+
});
|
|
463
479
|
}
|
|
464
480
|
|
|
465
481
|
if (entry.state.type === "available") {
|
|
@@ -604,39 +620,12 @@ export class SyncManager {
|
|
|
604
620
|
continue;
|
|
605
621
|
}
|
|
606
622
|
|
|
607
|
-
const before = performance.now();
|
|
608
|
-
// eslint-disable-next-line neverthrow/must-use-result
|
|
609
623
|
const result = coValue.tryAddTransactions(
|
|
610
624
|
sessionID,
|
|
611
625
|
newTransactions,
|
|
612
626
|
undefined,
|
|
613
627
|
newContentForSession.lastSignature,
|
|
614
628
|
);
|
|
615
|
-
const after = performance.now();
|
|
616
|
-
if (after - before > 80) {
|
|
617
|
-
const totalTxLength = newTransactions
|
|
618
|
-
.map((t) =>
|
|
619
|
-
t.privacy === "private"
|
|
620
|
-
? t.encryptedChanges.length
|
|
621
|
-
: t.changes.length,
|
|
622
|
-
)
|
|
623
|
-
.reduce((a, b) => a + b, 0);
|
|
624
|
-
logger.debug(
|
|
625
|
-
`Adding incoming transactions took ${(after - before).toFixed(
|
|
626
|
-
2,
|
|
627
|
-
)}ms for ${totalTxLength} bytes = bandwidth: ${(
|
|
628
|
-
(1000 * totalTxLength) / (after - before) / (1024 * 1024)
|
|
629
|
-
).toFixed(2)} MB/s`,
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// const theirTotalnTxs = Object.values(
|
|
634
|
-
// peer.optimisticKnownStates[msg.id]?.sessions || {},
|
|
635
|
-
// ).reduce((sum, nTxs) => sum + nTxs, 0);
|
|
636
|
-
// const ourTotalnTxs = [...coValue.sessionLogs.values()].reduce(
|
|
637
|
-
// (sum, session) => sum + session.transactions.length,
|
|
638
|
-
// 0,
|
|
639
|
-
// );
|
|
640
629
|
|
|
641
630
|
if (result.isErr()) {
|
|
642
631
|
logger.error("Failed to add transactions", {
|
|
@@ -735,16 +724,10 @@ export class SyncManager {
|
|
|
735
724
|
}
|
|
736
725
|
|
|
737
726
|
async actuallySyncCoValue(coValue: CoValueCore) {
|
|
738
|
-
// let blockingSince = performance.now();
|
|
739
727
|
for (const peer of this.peersInPriorityOrder()) {
|
|
740
728
|
if (peer.closed) continue;
|
|
741
729
|
if (peer.erroredCoValues.has(coValue.id)) continue;
|
|
742
|
-
|
|
743
|
-
// await new Promise<void>((resolve) => {
|
|
744
|
-
// setTimeout(resolve, 0);
|
|
745
|
-
// });
|
|
746
|
-
// blockingSince = performance.now();
|
|
747
|
-
// }
|
|
730
|
+
|
|
748
731
|
if (peer.optimisticKnownStates.has(coValue.id)) {
|
|
749
732
|
await this.tellUntoldKnownStateIncludingDependencies(coValue.id, peer);
|
|
750
733
|
await this.sendNewContentIncludingDependencies(coValue.id, peer);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { LinkedList } from "../PriorityBasedMessageQueue";
|
|
3
|
+
|
|
4
|
+
describe("LinkedList", () => {
|
|
5
|
+
let list: LinkedList<number>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
list = new LinkedList<number>();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("initialization", () => {
|
|
12
|
+
it("should create an empty list", () => {
|
|
13
|
+
expect(list.length).toBe(0);
|
|
14
|
+
expect(list.head).toBeUndefined();
|
|
15
|
+
expect(list.tail).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("push", () => {
|
|
20
|
+
it("should add an element to an empty list", () => {
|
|
21
|
+
list.push(1);
|
|
22
|
+
expect(list.length).toBe(1);
|
|
23
|
+
expect(list.head?.value).toBe(1);
|
|
24
|
+
expect(list.tail?.value).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should add multiple elements in sequence", () => {
|
|
28
|
+
list.push(1);
|
|
29
|
+
list.push(2);
|
|
30
|
+
list.push(3);
|
|
31
|
+
expect(list.length).toBe(3);
|
|
32
|
+
expect(list.head?.value).toBe(1);
|
|
33
|
+
expect(list.tail?.value).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("shift", () => {
|
|
38
|
+
it("should return undefined for empty list", () => {
|
|
39
|
+
expect(list.shift()).toBeUndefined();
|
|
40
|
+
expect(list.length).toBe(0);
|
|
41
|
+
expect(list.head).toBeUndefined();
|
|
42
|
+
expect(list.tail).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should remove and return the first element", () => {
|
|
46
|
+
list.push(1);
|
|
47
|
+
list.push(2);
|
|
48
|
+
|
|
49
|
+
const shifted = list.shift();
|
|
50
|
+
expect(shifted).toBe(1);
|
|
51
|
+
expect(list.length).toBe(1);
|
|
52
|
+
expect(list.head?.value).toBe(2);
|
|
53
|
+
expect(list.tail?.value).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should maintain correct order when shifting multiple times", () => {
|
|
57
|
+
list.push(1);
|
|
58
|
+
list.push(2);
|
|
59
|
+
list.push(3);
|
|
60
|
+
|
|
61
|
+
expect(list.shift()).toBe(1);
|
|
62
|
+
expect(list.shift()).toBe(2);
|
|
63
|
+
expect(list.shift()).toBe(3);
|
|
64
|
+
expect(list.length).toBe(0);
|
|
65
|
+
expect(list.head).toBeUndefined();
|
|
66
|
+
expect(list.tail).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should handle shift after last element is removed", () => {
|
|
70
|
+
list.push(1);
|
|
71
|
+
list.shift();
|
|
72
|
+
expect(list.shift()).toBeUndefined();
|
|
73
|
+
expect(list.length).toBe(0);
|
|
74
|
+
expect(list.head).toBeUndefined();
|
|
75
|
+
expect(list.tail).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("edge cases", () => {
|
|
80
|
+
it("should handle push after all elements have been shifted", () => {
|
|
81
|
+
list.push(1);
|
|
82
|
+
list.shift();
|
|
83
|
+
list.push(2);
|
|
84
|
+
expect(list.length).toBe(1);
|
|
85
|
+
expect(list.shift()).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should handle alternating push and shift operations", () => {
|
|
89
|
+
list.push(1);
|
|
90
|
+
expect(list.shift()).toBe(1);
|
|
91
|
+
list.push(2);
|
|
92
|
+
expect(list.shift()).toBe(2);
|
|
93
|
+
expect(list.length).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/tests/sync.test.ts
CHANGED
|
@@ -956,27 +956,6 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
|
|
|
956
956
|
*/
|
|
957
957
|
});
|
|
958
958
|
|
|
959
|
-
test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
|
|
960
|
-
const { node: node1 } = await createConnectedTestNode();
|
|
961
|
-
|
|
962
|
-
const group = node1.createGroup();
|
|
963
|
-
|
|
964
|
-
const map = group.createMap();
|
|
965
|
-
map.set("hello", "world", "trusting");
|
|
966
|
-
|
|
967
|
-
const node2 = createTestNode();
|
|
968
|
-
|
|
969
|
-
const mapOnNode2Promise = loadCoValueOrFail(node2, map.id);
|
|
970
|
-
|
|
971
|
-
expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("unknown");
|
|
972
|
-
|
|
973
|
-
connectNodeToSyncServer(node2);
|
|
974
|
-
|
|
975
|
-
const mapOnNode2 = await mapOnNode2Promise;
|
|
976
|
-
|
|
977
|
-
expect(mapOnNode2.get("hello")).toEqual("world");
|
|
978
|
-
});
|
|
979
|
-
|
|
980
959
|
test("should keep the peer state when the peer closes", async () => {
|
|
981
960
|
const client = createTestNode();
|
|
982
961
|
|
|
@@ -1726,6 +1705,45 @@ describe("loadCoValueCore with retry", () => {
|
|
|
1726
1705
|
await expect(promise1).resolves.not.toBe("unavailable");
|
|
1727
1706
|
await expect(promise2).resolves.not.toBe("unavailable");
|
|
1728
1707
|
});
|
|
1708
|
+
|
|
1709
|
+
test("should load unavailable coValues after they are synced", async () => {
|
|
1710
|
+
const bob = createTestNode();
|
|
1711
|
+
const alice = createTestNode();
|
|
1712
|
+
|
|
1713
|
+
// Create a group and map on anotherClient
|
|
1714
|
+
const group = alice.createGroup();
|
|
1715
|
+
const map = group.createMap();
|
|
1716
|
+
map.set("key1", "value1", "trusting");
|
|
1717
|
+
|
|
1718
|
+
// Start loading before syncing
|
|
1719
|
+
const result = await bob.loadCoValueCore(map.id);
|
|
1720
|
+
|
|
1721
|
+
expect(result).toBe("unavailable");
|
|
1722
|
+
|
|
1723
|
+
connectTwoPeers(alice, bob, "server", "server");
|
|
1724
|
+
|
|
1725
|
+
const result2 = await bob.loadCoValueCore(map.id);
|
|
1726
|
+
|
|
1727
|
+
expect(result2).not.toBe("unavailable");
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
test("should successfully mark a coValue as unavailable if the server does not have it", async () => {
|
|
1731
|
+
const bob = createTestNode();
|
|
1732
|
+
const alice = createTestNode();
|
|
1733
|
+
const charlie = createTestNode();
|
|
1734
|
+
|
|
1735
|
+
connectTwoPeers(bob, charlie, "client", "server");
|
|
1736
|
+
|
|
1737
|
+
// Create a group and map on anotherClient
|
|
1738
|
+
const group = alice.createGroup();
|
|
1739
|
+
const map = group.createMap();
|
|
1740
|
+
map.set("key1", "value1", "trusting");
|
|
1741
|
+
|
|
1742
|
+
// Start loading before syncing
|
|
1743
|
+
const result = await bob.loadCoValueCore(map.id);
|
|
1744
|
+
|
|
1745
|
+
expect(result).toBe("unavailable");
|
|
1746
|
+
});
|
|
1729
1747
|
});
|
|
1730
1748
|
|
|
1731
1749
|
describe("waitForSyncWithPeer", () => {
|
|
@@ -1894,6 +1912,8 @@ describe("sync protocol", () => {
|
|
|
1894
1912
|
const map = group.createMap();
|
|
1895
1913
|
map.set("hello", "world", "trusting");
|
|
1896
1914
|
|
|
1915
|
+
await map.core.waitForSync();
|
|
1916
|
+
|
|
1897
1917
|
const mapOnJazzCloud = await loadCoValueOrFail(jazzCloud, map.id);
|
|
1898
1918
|
expect(mapOnJazzCloud.get("hello")).toEqual("world");
|
|
1899
1919
|
|
|
@@ -2038,6 +2058,29 @@ describe("sync protocol", () => {
|
|
|
2038
2058
|
},
|
|
2039
2059
|
},
|
|
2040
2060
|
},
|
|
2061
|
+
{
|
|
2062
|
+
from: "server",
|
|
2063
|
+
msg: {
|
|
2064
|
+
action: "known",
|
|
2065
|
+
header: true,
|
|
2066
|
+
id: map.id,
|
|
2067
|
+
sessions: {
|
|
2068
|
+
[client.currentSessionID]: 1,
|
|
2069
|
+
},
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
from: "server",
|
|
2074
|
+
msg: {
|
|
2075
|
+
action: "known",
|
|
2076
|
+
asDependencyOf: undefined,
|
|
2077
|
+
header: true,
|
|
2078
|
+
id: map.id,
|
|
2079
|
+
sessions: {
|
|
2080
|
+
[client.currentSessionID]: 1,
|
|
2081
|
+
},
|
|
2082
|
+
},
|
|
2083
|
+
},
|
|
2041
2084
|
]);
|
|
2042
2085
|
});
|
|
2043
2086
|
});
|