cojson 0.20.0 → 0.20.2
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 +21 -0
- package/dist/GarbageCollector.d.ts +3 -3
- package/dist/GarbageCollector.d.ts.map +1 -1
- package/dist/GarbageCollector.js +4 -4
- package/dist/GarbageCollector.js.map +1 -1
- package/dist/PeerState.d.ts +6 -1
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/PeerState.js +18 -3
- package/dist/PeerState.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +26 -5
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +115 -50
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/coList.d.ts +1 -0
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +3 -0
- package/dist/coValues/coList.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +3 -3
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +2 -2
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +12 -0
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +51 -3
- package/dist/localNode.js.map +1 -1
- package/dist/queue/LinkedList.d.ts +9 -3
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js +30 -1
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/OutgoingLoadQueue.d.ts +95 -0
- package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -0
- package/dist/queue/OutgoingLoadQueue.js +240 -0
- package/dist/queue/OutgoingLoadQueue.js.map +1 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +34 -41
- package/dist/sync.js.map +1 -1
- package/dist/tests/LinkedList.test.js +90 -0
- package/dist/tests/LinkedList.test.js.map +1 -1
- package/dist/tests/OutgoingLoadQueue.test.d.ts +2 -0
- package/dist/tests/OutgoingLoadQueue.test.d.ts.map +1 -0
- package/dist/tests/OutgoingLoadQueue.test.js +814 -0
- package/dist/tests/OutgoingLoadQueue.test.js.map +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js +87 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +44 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
- package/dist/tests/sync.concurrentLoad.test.d.ts +2 -0
- package/dist/tests/sync.concurrentLoad.test.d.ts.map +1 -0
- package/dist/tests/sync.concurrentLoad.test.js +481 -0
- package/dist/tests/sync.concurrentLoad.test.js.map +1 -0
- package/dist/tests/sync.garbageCollection.test.js +87 -3
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.multipleServers.test.js +0 -62
- package/dist/tests/sync.multipleServers.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +156 -0
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +1 -1
- package/dist/tests/testStorage.d.ts.map +1 -1
- package/dist/tests/testStorage.js +3 -1
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +1 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +2 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/GarbageCollector.ts +4 -3
- package/src/PeerState.ts +26 -3
- package/src/coValueCore/coValueCore.ts +129 -53
- package/src/coValues/coList.ts +4 -0
- package/src/config.ts +4 -4
- package/src/exports.ts +2 -2
- package/src/localNode.ts +65 -4
- package/src/queue/LinkedList.ts +34 -4
- package/src/queue/OutgoingLoadQueue.ts +307 -0
- package/src/sync.ts +37 -43
- package/src/tests/LinkedList.test.ts +111 -0
- package/src/tests/OutgoingLoadQueue.test.ts +1129 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +108 -0
- package/src/tests/knownState.lazyLoading.test.ts +52 -0
- package/src/tests/sync.concurrentLoad.test.ts +650 -0
- package/src/tests/sync.garbageCollection.test.ts +115 -3
- package/src/tests/sync.multipleServers.test.ts +0 -65
- package/src/tests/sync.peerReconciliation.test.ts +199 -0
- package/src/tests/sync.storage.test.ts +1 -1
- package/src/tests/testStorage.ts +3 -1
- package/src/tests/testUtils.ts +3 -1
package/src/GarbageCollector.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CoValueCore } from "./coValueCore/coValueCore.js";
|
|
2
2
|
import { GARBAGE_COLLECTOR_CONFIG } from "./config.js";
|
|
3
3
|
import { RawCoID } from "./ids.js";
|
|
4
|
+
import type { LocalNode } from "./localNode.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* TTL-based garbage collector for removing unused CoValues from memory.
|
|
@@ -8,7 +9,7 @@ import { RawCoID } from "./ids.js";
|
|
|
8
9
|
export class GarbageCollector {
|
|
9
10
|
private readonly interval: ReturnType<typeof setInterval>;
|
|
10
11
|
|
|
11
|
-
constructor(private readonly
|
|
12
|
+
constructor(private readonly node: LocalNode) {
|
|
12
13
|
this.interval = setInterval(() => {
|
|
13
14
|
this.collect();
|
|
14
15
|
}, GARBAGE_COLLECTOR_CONFIG.INTERVAL);
|
|
@@ -26,7 +27,7 @@ export class GarbageCollector {
|
|
|
26
27
|
|
|
27
28
|
collect() {
|
|
28
29
|
const currentTime = this.getCurrentTime();
|
|
29
|
-
for (const coValue of this.
|
|
30
|
+
for (const coValue of this.node.allCoValues()) {
|
|
30
31
|
const { verified } = coValue;
|
|
31
32
|
|
|
32
33
|
if (!verified?.lastAccessed) {
|
|
@@ -36,7 +37,7 @@ export class GarbageCollector {
|
|
|
36
37
|
const timeSinceLastAccessed = currentTime - verified.lastAccessed;
|
|
37
38
|
|
|
38
39
|
if (timeSinceLastAccessed > GARBAGE_COLLECTOR_CONFIG.MAX_AGE) {
|
|
39
|
-
coValue.
|
|
40
|
+
this.node.internalUnmountCoValue(coValue.id);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
package/src/PeerState.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PeerKnownState } from "./coValueCore/PeerKnownState.js";
|
|
2
|
+
import { CoValueCore } from "./exports.js";
|
|
2
3
|
import { RawCoID } from "./ids.js";
|
|
3
4
|
import { CoValueKnownState } from "./knownState.js";
|
|
4
5
|
import { logger } from "./logger.js";
|
|
6
|
+
import { type LoadMode, OutgoingLoadQueue } from "./queue/OutgoingLoadQueue.js";
|
|
5
7
|
import { Peer, SyncMessage } from "./sync.js";
|
|
6
8
|
|
|
7
9
|
export class PeerState {
|
|
@@ -17,6 +19,7 @@ export class PeerState {
|
|
|
17
19
|
knownStates: Map<RawCoID, PeerKnownState> | undefined,
|
|
18
20
|
) {
|
|
19
21
|
this._knownStates = knownStates ?? new Map();
|
|
22
|
+
this.loadQueue = new OutgoingLoadQueue(peer.id);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
getKnownState(id: RawCoID) {
|
|
@@ -54,10 +57,29 @@ export class PeerState {
|
|
|
54
57
|
|
|
55
58
|
readonly toldKnownState: Set<RawCoID> = new Set();
|
|
56
59
|
readonly loadRequestSent: Set<RawCoID> = new Set();
|
|
60
|
+
private loadQueue: OutgoingLoadQueue;
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
this.toldKnownState.add(id);
|
|
60
|
-
this.loadRequestSent.add(id);
|
|
62
|
+
sendLoadRequest(coValue: CoValueCore, mode?: LoadMode): void {
|
|
63
|
+
this.toldKnownState.add(coValue.id);
|
|
64
|
+
this.loadRequestSent.add(coValue.id);
|
|
65
|
+
this.loadQueue.enqueue(
|
|
66
|
+
coValue,
|
|
67
|
+
() => {
|
|
68
|
+
this.pushOutgoingMessage({
|
|
69
|
+
action: "load",
|
|
70
|
+
...coValue.knownStateWithStreaming(),
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
mode,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
trackLoadRequestUpdate(coValue: CoValueCore) {
|
|
78
|
+
this.loadQueue.trackUpdate(coValue);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
trackLoadRequestComplete(coValue: CoValueCore) {
|
|
82
|
+
this.loadQueue.trackComplete(coValue);
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
trackToldKnownState(id: RawCoID) {
|
|
@@ -192,6 +214,7 @@ export class PeerState {
|
|
|
192
214
|
});
|
|
193
215
|
|
|
194
216
|
this.closed = true;
|
|
217
|
+
this.loadQueue.clear();
|
|
195
218
|
this.peer.outgoing.push("Disconnected");
|
|
196
219
|
this.peer.outgoing.close();
|
|
197
220
|
this.peer.incoming.close();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
|
|
2
2
|
import type { PeerState } from "../PeerState.js";
|
|
3
3
|
import type { RawCoValue } from "../coValue.js";
|
|
4
|
+
import type { LoadMode } from "../queue/OutgoingLoadQueue.js";
|
|
4
5
|
import {
|
|
5
6
|
RawAccount,
|
|
6
7
|
type ControlledAccountOrAgent,
|
|
@@ -281,6 +282,15 @@ export class CoValueCore {
|
|
|
281
282
|
}
|
|
282
283
|
>();
|
|
283
284
|
|
|
285
|
+
// Tracks why we have lastKnownState (separate from loadingStatuses)
|
|
286
|
+
// - "garbageCollected": was in memory, got GC'd
|
|
287
|
+
// - "onlyKnownState": checked storage, found knownState, but didn't load full content
|
|
288
|
+
#lastKnownStateSource?: "garbageCollected" | "onlyKnownState";
|
|
289
|
+
|
|
290
|
+
// Cache the knownState when transitioning to garbageCollected/onlyKnownState
|
|
291
|
+
// Used during peer reconciliation to send accurate LOAD requests
|
|
292
|
+
#lastKnownState?: CoValueKnownState;
|
|
293
|
+
|
|
284
294
|
// cached state and listeners
|
|
285
295
|
private _cachedContent?: RawCoValue;
|
|
286
296
|
readonly listeners: Set<(core: CoValueCore, unsub: () => void) => void> =
|
|
@@ -307,14 +317,26 @@ export class CoValueCore {
|
|
|
307
317
|
get loadingState() {
|
|
308
318
|
if (this.verified) {
|
|
309
319
|
return "available";
|
|
310
|
-
} else if (this.loadingStatuses.size === 0) {
|
|
311
|
-
return "unknown";
|
|
312
320
|
}
|
|
313
321
|
|
|
322
|
+
// Check for pending peers FIRST - loading takes priority over other states
|
|
314
323
|
for (const peer of this.loadingStatuses.values()) {
|
|
315
324
|
if (peer.type === "pending") {
|
|
316
325
|
return "loading";
|
|
317
|
-
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check for lastKnownStateSource (garbageCollected or onlyKnownState)
|
|
330
|
+
if (this.#lastKnownStateSource) {
|
|
331
|
+
return this.#lastKnownStateSource;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (this.loadingStatuses.size === 0) {
|
|
335
|
+
return "unknown";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const peer of this.loadingStatuses.values()) {
|
|
339
|
+
if (peer.type === "unknown") {
|
|
318
340
|
return "unknown";
|
|
319
341
|
}
|
|
320
342
|
}
|
|
@@ -428,32 +450,22 @@ export class CoValueCore {
|
|
|
428
450
|
}
|
|
429
451
|
|
|
430
452
|
/**
|
|
431
|
-
* Removes the CoValue from memory.
|
|
453
|
+
* Removes the CoValue content from memory but keeps a shell with cached knownState.
|
|
454
|
+
* This enables accurate LOAD requests during peer reconciliation.
|
|
432
455
|
*
|
|
433
456
|
* @returns true if the coValue was successfully unmounted, false otherwise
|
|
434
457
|
*/
|
|
435
458
|
unmount(): boolean {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
for (const dependant of this.dependant) {
|
|
442
|
-
if (this.node.hasCoValue(dependant)) {
|
|
443
|
-
// Another in-memory coValue depends on this coValue
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (!this.node.syncManager.isSyncedToServerPeers(this.id)) {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
459
|
+
return this.node.internalUnmountCoValue(this.id);
|
|
460
|
+
}
|
|
451
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Decrements the counter for the current loading state.
|
|
464
|
+
* Used during unmount to properly track state transitions.
|
|
465
|
+
* @internal
|
|
466
|
+
*/
|
|
467
|
+
decrementLoadingStateCounter() {
|
|
452
468
|
this.counter.add(-1, { state: this.loadingState });
|
|
453
|
-
|
|
454
|
-
this.node.internalDeleteCoValue(this.id);
|
|
455
|
-
|
|
456
|
-
return true;
|
|
457
469
|
}
|
|
458
470
|
|
|
459
471
|
markNotFoundInPeer(peerId: PeerID) {
|
|
@@ -469,6 +481,35 @@ export class CoValueCore {
|
|
|
469
481
|
this.scheduleNotifyUpdate();
|
|
470
482
|
}
|
|
471
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Clean up cached state when CoValue becomes available.
|
|
486
|
+
* Called after the CoValue transitions from garbageCollected/onlyKnownState to available.
|
|
487
|
+
*/
|
|
488
|
+
private cleanupLastKnownState() {
|
|
489
|
+
// Clear both fields - in-memory verified state is now authoritative
|
|
490
|
+
this.#lastKnownStateSource = undefined;
|
|
491
|
+
this.#lastKnownState = undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Initialize this CoValueCore as a garbageCollected shell.
|
|
496
|
+
* Called when creating a replacement CoValueCore after unmounting.
|
|
497
|
+
*/
|
|
498
|
+
setGarbageCollectedState(knownState: CoValueKnownState) {
|
|
499
|
+
// Only set garbageCollected state if storage is active
|
|
500
|
+
// Without storage, we can't reload the CoValue anyway
|
|
501
|
+
if (!this.node.storage) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Transition counter from 'unknown' (set by constructor) to 'garbageCollected'
|
|
506
|
+
// previousState will be 'unknown', newState will be 'garbageCollected'
|
|
507
|
+
const previousState = this.loadingState;
|
|
508
|
+
this.#lastKnownStateSource = "garbageCollected";
|
|
509
|
+
this.#lastKnownState = knownState;
|
|
510
|
+
this.updateCounter(previousState);
|
|
511
|
+
}
|
|
512
|
+
|
|
472
513
|
missingDependencies = new Set<RawCoID>();
|
|
473
514
|
|
|
474
515
|
isCircularDependency(dependency: CoValueCore) {
|
|
@@ -575,6 +616,10 @@ export class CoValueCore {
|
|
|
575
616
|
header,
|
|
576
617
|
new SessionMap(this.id, this.node.crypto, streamingKnownState),
|
|
577
618
|
);
|
|
619
|
+
// Clean up if transitioning from garbageCollected/onlyKnownState
|
|
620
|
+
if (this.isAvailable()) {
|
|
621
|
+
this.cleanupLastKnownState();
|
|
622
|
+
}
|
|
578
623
|
|
|
579
624
|
return true;
|
|
580
625
|
}
|
|
@@ -605,10 +650,11 @@ export class CoValueCore {
|
|
|
605
650
|
* Used to correctly manage the content & subscriptions during the content streaming process
|
|
606
651
|
*/
|
|
607
652
|
knownStateWithStreaming(): CoValueKnownState {
|
|
608
|
-
|
|
609
|
-
this.verified
|
|
610
|
-
|
|
611
|
-
|
|
653
|
+
if (this.verified) {
|
|
654
|
+
return this.verified.immutableKnownStateWithStreaming();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return this.knownState();
|
|
612
658
|
}
|
|
613
659
|
|
|
614
660
|
/**
|
|
@@ -617,9 +663,22 @@ export class CoValueCore {
|
|
|
617
663
|
* The return value identity is going to be stable as long as the CoValue is not modified.
|
|
618
664
|
*
|
|
619
665
|
* On change the knownState is invalidated and a new object is returned.
|
|
666
|
+
*
|
|
667
|
+
* For garbageCollected/onlyKnownState CoValues, returns the cached knownState.
|
|
620
668
|
*/
|
|
621
669
|
knownState(): CoValueKnownState {
|
|
622
|
-
|
|
670
|
+
// 1. If we have verified content in memory, use that (authoritative)
|
|
671
|
+
if (this.verified) {
|
|
672
|
+
return this.verified.immutableKnownState();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 2. If we have last known state (GC'd or onlyKnownState), use that
|
|
676
|
+
if (this.#lastKnownState) {
|
|
677
|
+
return this.#lastKnownState;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 3. Fallback to empty state (truly unknown CoValue)
|
|
681
|
+
return emptyKnownState(this.id);
|
|
623
682
|
}
|
|
624
683
|
|
|
625
684
|
/**
|
|
@@ -1847,11 +1906,11 @@ export class CoValueCore {
|
|
|
1847
1906
|
return this.node.syncManager.waitForSync(this.id, options?.timeout);
|
|
1848
1907
|
}
|
|
1849
1908
|
|
|
1850
|
-
load(peers: PeerState[]) {
|
|
1909
|
+
load(peers: PeerState[], mode?: LoadMode) {
|
|
1851
1910
|
this.loadFromStorage((found) => {
|
|
1852
1911
|
// When found the load is triggered by handleNewContent
|
|
1853
1912
|
if (!found) {
|
|
1854
|
-
this.loadFromPeers(peers);
|
|
1913
|
+
this.loadFromPeers(peers, mode);
|
|
1855
1914
|
}
|
|
1856
1915
|
});
|
|
1857
1916
|
}
|
|
@@ -1890,7 +1949,16 @@ export class CoValueCore {
|
|
|
1890
1949
|
return;
|
|
1891
1950
|
}
|
|
1892
1951
|
|
|
1893
|
-
if
|
|
1952
|
+
// Check if we need to load from storage:
|
|
1953
|
+
// - If storage state is not unknown (already tried), AND
|
|
1954
|
+
// - Overall state is not garbageCollected/onlyKnownState (which need full content)
|
|
1955
|
+
// Then return early
|
|
1956
|
+
const overallState = this.loadingState;
|
|
1957
|
+
if (
|
|
1958
|
+
currentState !== "unknown" &&
|
|
1959
|
+
overallState !== "garbageCollected" &&
|
|
1960
|
+
overallState !== "onlyKnownState"
|
|
1961
|
+
) {
|
|
1894
1962
|
done?.(currentState === "available");
|
|
1895
1963
|
return;
|
|
1896
1964
|
}
|
|
@@ -1915,7 +1983,8 @@ export class CoValueCore {
|
|
|
1915
1983
|
* Lazily load only the knownState from storage without loading full transaction data.
|
|
1916
1984
|
* This is useful for checking if a peer needs new content before committing to a full load.
|
|
1917
1985
|
*
|
|
1918
|
-
*
|
|
1986
|
+
* If found in storage, marks the CoValue as onlyKnownState and caches the knownState.
|
|
1987
|
+
* This enables accurate LOAD requests during peer reconciliation.
|
|
1919
1988
|
*
|
|
1920
1989
|
* @param done - Callback with the storage knownState, or undefined if not found in storage
|
|
1921
1990
|
*/
|
|
@@ -1927,17 +1996,29 @@ export class CoValueCore {
|
|
|
1927
1996
|
return;
|
|
1928
1997
|
}
|
|
1929
1998
|
|
|
1930
|
-
// If already
|
|
1931
|
-
|
|
1932
|
-
|
|
1999
|
+
// If we already have knowledge about this CoValue (in memory or cached), return it
|
|
2000
|
+
// knownState() returns verified state, lastKnownState, or empty state
|
|
2001
|
+
const knownState = this.knownState();
|
|
2002
|
+
if (knownState.header) {
|
|
2003
|
+
done(knownState);
|
|
1933
2004
|
return;
|
|
1934
2005
|
}
|
|
1935
2006
|
|
|
1936
2007
|
// Delegate to storage - caching is handled at storage level
|
|
1937
|
-
this.node.storage.loadKnownState(this.id,
|
|
2008
|
+
this.node.storage.loadKnownState(this.id, (knownState) => {
|
|
2009
|
+
// The coValue could become available in the meantime.
|
|
2010
|
+
if (knownState && !this.isAvailable()) {
|
|
2011
|
+
// Cache the knownState and mark as onlyKnownState
|
|
2012
|
+
const previousState = this.loadingState;
|
|
2013
|
+
this.#lastKnownStateSource = "onlyKnownState";
|
|
2014
|
+
this.#lastKnownState = knownState;
|
|
2015
|
+
this.updateCounter(previousState);
|
|
2016
|
+
}
|
|
2017
|
+
done(knownState);
|
|
2018
|
+
});
|
|
1938
2019
|
}
|
|
1939
2020
|
|
|
1940
|
-
loadFromPeers(peers: PeerState[]) {
|
|
2021
|
+
loadFromPeers(peers: PeerState[], mode?: LoadMode) {
|
|
1941
2022
|
if (peers.length === 0) {
|
|
1942
2023
|
return;
|
|
1943
2024
|
}
|
|
@@ -1947,29 +2028,17 @@ export class CoValueCore {
|
|
|
1947
2028
|
|
|
1948
2029
|
if (currentState === "unknown" || currentState === "unavailable") {
|
|
1949
2030
|
this.markPending(peer.id);
|
|
1950
|
-
this.internalLoadFromPeer(peer);
|
|
2031
|
+
this.internalLoadFromPeer(peer, mode);
|
|
1951
2032
|
}
|
|
1952
2033
|
}
|
|
1953
2034
|
}
|
|
1954
2035
|
|
|
1955
|
-
private internalLoadFromPeer(peer: PeerState) {
|
|
2036
|
+
private internalLoadFromPeer(peer: PeerState, mode?: LoadMode) {
|
|
1956
2037
|
if (peer.closed && !peer.persistent) {
|
|
1957
2038
|
this.markNotFoundInPeer(peer.id);
|
|
1958
2039
|
return;
|
|
1959
2040
|
}
|
|
1960
2041
|
|
|
1961
|
-
/**
|
|
1962
|
-
* On reconnection persistent peers will automatically fire the load request
|
|
1963
|
-
* as part of the reconnection process.
|
|
1964
|
-
*/
|
|
1965
|
-
if (!peer.closed) {
|
|
1966
|
-
peer.pushOutgoingMessage({
|
|
1967
|
-
action: "load",
|
|
1968
|
-
...this.knownState(),
|
|
1969
|
-
});
|
|
1970
|
-
peer.trackLoadRequestSent(this.id);
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
2042
|
const markNotFound = () => {
|
|
1974
2043
|
if (this.getLoadingStateForPeer(peer.id) === "pending") {
|
|
1975
2044
|
logger.warn("Timeout waiting for peer to load coValue", {
|
|
@@ -1980,11 +2049,19 @@ export class CoValueCore {
|
|
|
1980
2049
|
}
|
|
1981
2050
|
};
|
|
1982
2051
|
|
|
1983
|
-
|
|
2052
|
+
// Close listener for non-persistent peers
|
|
1984
2053
|
const removeCloseListener = peer.persistent
|
|
1985
2054
|
? undefined
|
|
1986
2055
|
: peer.addCloseListener(markNotFound);
|
|
1987
2056
|
|
|
2057
|
+
/**
|
|
2058
|
+
* On reconnection persistent peers will automatically fire the load request
|
|
2059
|
+
* as part of the reconnection process.
|
|
2060
|
+
*/
|
|
2061
|
+
if (!peer.closed) {
|
|
2062
|
+
peer.sendLoadRequest(this, mode);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1988
2065
|
this.subscribe((state, unsubscribe) => {
|
|
1989
2066
|
const peerState = state.getLoadingStateForPeer(peer.id);
|
|
1990
2067
|
if (
|
|
@@ -1995,7 +2072,6 @@ export class CoValueCore {
|
|
|
1995
2072
|
) {
|
|
1996
2073
|
unsubscribe();
|
|
1997
2074
|
removeCloseListener?.();
|
|
1998
|
-
clearTimeout(timeout);
|
|
1999
2075
|
}
|
|
2000
2076
|
}, true);
|
|
2001
2077
|
}
|
package/src/coValues/coList.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -15,8 +15,9 @@ export function setMaxRecommendedTxSize(size: number) {
|
|
|
15
15
|
|
|
16
16
|
export const CO_VALUE_LOADING_CONFIG = {
|
|
17
17
|
MAX_RETRIES: 1,
|
|
18
|
-
TIMEOUT:
|
|
18
|
+
TIMEOUT: 60_000,
|
|
19
19
|
RETRY_DELAY: 3000,
|
|
20
|
+
MAX_IN_FLIGHT_LOADS_PER_PEER: 1000,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export function setCoValueLoadingMaxRetries(maxRetries: number) {
|
|
@@ -54,13 +55,12 @@ export function setGarbageCollectorInterval(interval: number) {
|
|
|
54
55
|
|
|
55
56
|
export const WEBSOCKET_CONFIG = {
|
|
56
57
|
MAX_OUTGOING_MESSAGES_CHUNK_BYTES: 25_000,
|
|
57
|
-
OUTGOING_MESSAGES_CHUNK_DELAY: 5,
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
export function setMaxOutgoingMessagesChunkBytes(bytes: number) {
|
|
61
61
|
WEBSOCKET_CONFIG.MAX_OUTGOING_MESSAGES_CHUNK_BYTES = bytes;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function
|
|
65
|
-
|
|
64
|
+
export function setMaxInFlightLoadsPerPeer(limit: number) {
|
|
65
|
+
CO_VALUE_LOADING_CONFIG.MAX_IN_FLIGHT_LOADS_PER_PEER = limit;
|
|
66
66
|
}
|
package/src/exports.ts
CHANGED
|
@@ -88,8 +88,8 @@ import {
|
|
|
88
88
|
setCoValueLoadingRetryDelay,
|
|
89
89
|
setCoValueLoadingTimeout,
|
|
90
90
|
setIncomingMessagesTimeBudget,
|
|
91
|
+
setMaxInFlightLoadsPerPeer,
|
|
91
92
|
setMaxOutgoingMessagesChunkBytes,
|
|
92
|
-
setOutgoingMessagesChunkDelay,
|
|
93
93
|
setMaxRecommendedTxSize,
|
|
94
94
|
} from "./config.js";
|
|
95
95
|
import { LogLevel, logger } from "./logger.js";
|
|
@@ -142,7 +142,7 @@ export const cojsonInternals = {
|
|
|
142
142
|
canBeBranched,
|
|
143
143
|
WEBSOCKET_CONFIG,
|
|
144
144
|
setMaxOutgoingMessagesChunkBytes,
|
|
145
|
-
|
|
145
|
+
setMaxInFlightLoadsPerPeer,
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
export {
|
package/src/localNode.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromS
|
|
|
39
39
|
import { expectGroup } from "./typeUtils/expectGroup.js";
|
|
40
40
|
import { canBeBranched } from "./coValueCore/branching.js";
|
|
41
41
|
import { connectedPeers } from "./streamUtils.js";
|
|
42
|
-
import { emptyKnownState } from "./knownState.js";
|
|
42
|
+
import { CoValueKnownState, emptyKnownState } from "./knownState.js";
|
|
43
43
|
|
|
44
44
|
/** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
|
|
45
45
|
|
|
@@ -87,7 +87,7 @@ export class LocalNode {
|
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
this.garbageCollector = new GarbageCollector(this
|
|
90
|
+
this.garbageCollector = new GarbageCollector(this);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
setStorage(storage: StorageAPI) {
|
|
@@ -123,7 +123,13 @@ export class LocalNode {
|
|
|
123
123
|
return false;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
const state = coValue.loadingState;
|
|
127
|
+
// garbageCollected and onlyKnownState shells don't have actual content loaded
|
|
128
|
+
return (
|
|
129
|
+
state !== "unknown" &&
|
|
130
|
+
state !== "garbageCollected" &&
|
|
131
|
+
state !== "onlyKnownState"
|
|
132
|
+
);
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
getCoValue(id: RawCoID) {
|
|
@@ -143,11 +149,64 @@ export class LocalNode {
|
|
|
143
149
|
return this.coValues.values();
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Simple delete of a CoValue from memory.
|
|
154
|
+
* Used for testing and forced cleanup scenarios.
|
|
155
|
+
* @internal
|
|
156
|
+
*/
|
|
146
157
|
internalDeleteCoValue(id: RawCoID) {
|
|
147
158
|
this.coValues.delete(id);
|
|
148
159
|
this.storage?.onCoValueUnmounted(id);
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Unmount a CoValue from memory, keeping a shell with cached knownState.
|
|
164
|
+
* This enables accurate LOAD requests during peer reconciliation.
|
|
165
|
+
*
|
|
166
|
+
* @returns true if the coValue was successfully unmounted, false otherwise
|
|
167
|
+
*/
|
|
168
|
+
internalUnmountCoValue(id: RawCoID): boolean {
|
|
169
|
+
const coValue = this.coValues.get(id);
|
|
170
|
+
|
|
171
|
+
if (!coValue) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (coValue.listeners.size > 0) {
|
|
176
|
+
// The coValue is still in use
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const dependant of coValue.dependant) {
|
|
181
|
+
if (this.hasCoValue(dependant)) {
|
|
182
|
+
// Another in-memory coValue depends on this coValue
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!this.syncManager.isSyncedToServerPeers(id)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Cache the knownState before replacing
|
|
192
|
+
const lastKnownState = coValue.knownState();
|
|
193
|
+
|
|
194
|
+
// Handle counter: decrement old coValue's state
|
|
195
|
+
coValue.decrementLoadingStateCounter();
|
|
196
|
+
|
|
197
|
+
// Create new shell CoValueCore in garbageCollected state
|
|
198
|
+
const shell = new CoValueCore(id, this);
|
|
199
|
+
shell.setGarbageCollectedState(lastKnownState);
|
|
200
|
+
|
|
201
|
+
// Single map update (replacing old with shell)
|
|
202
|
+
this.coValues.set(id, shell);
|
|
203
|
+
|
|
204
|
+
// Notify storage
|
|
205
|
+
this.storage?.onCoValueUnmounted(id);
|
|
206
|
+
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
151
210
|
getCurrentAccountOrAgentID(): RawAccountID | AgentID {
|
|
152
211
|
return accountOrAgentIDfromSessionID(this.currentSessionID);
|
|
153
212
|
}
|
|
@@ -449,7 +508,9 @@ export class LocalNode {
|
|
|
449
508
|
|
|
450
509
|
if (
|
|
451
510
|
coValue.loadingState === "unknown" ||
|
|
452
|
-
coValue.loadingState === "unavailable"
|
|
511
|
+
coValue.loadingState === "unavailable" ||
|
|
512
|
+
coValue.loadingState === "garbageCollected" ||
|
|
513
|
+
coValue.loadingState === "onlyKnownState"
|
|
453
514
|
) {
|
|
454
515
|
const peers = this.syncManager.getServerPeers(id, skipLoadingFromPeer);
|
|
455
516
|
|
package/src/queue/LinkedList.ts
CHANGED
|
@@ -10,8 +10,9 @@ type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
|
|
|
10
10
|
? A
|
|
11
11
|
: Tuple<T, N, [...A, T]>;
|
|
12
12
|
export type QueueTuple = Tuple<LinkedList<SyncMessage>, 3>;
|
|
13
|
-
type LinkedListNode<T> = {
|
|
13
|
+
export type LinkedListNode<T> = {
|
|
14
14
|
value: T;
|
|
15
|
+
prev: LinkedListNode<T> | undefined;
|
|
15
16
|
next: LinkedListNode<T> | undefined;
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
@@ -26,13 +27,14 @@ export class LinkedList<T> {
|
|
|
26
27
|
tail: LinkedListNode<T> | undefined = undefined;
|
|
27
28
|
length = 0;
|
|
28
29
|
|
|
29
|
-
push(value: T) {
|
|
30
|
-
const node = { value, next: undefined };
|
|
30
|
+
push(value: T): LinkedListNode<T> {
|
|
31
|
+
const node: LinkedListNode<T> = { value, prev: undefined, next: undefined };
|
|
31
32
|
|
|
32
33
|
if (this.head === undefined) {
|
|
33
34
|
this.head = node;
|
|
34
35
|
this.tail = node;
|
|
35
36
|
} else if (this.tail) {
|
|
37
|
+
node.prev = this.tail;
|
|
36
38
|
this.tail.next = node;
|
|
37
39
|
this.tail = node;
|
|
38
40
|
} else {
|
|
@@ -41,6 +43,7 @@ export class LinkedList<T> {
|
|
|
41
43
|
|
|
42
44
|
this.length++;
|
|
43
45
|
this.meter?.push();
|
|
46
|
+
return node;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
shift() {
|
|
@@ -55,6 +58,8 @@ export class LinkedList<T> {
|
|
|
55
58
|
|
|
56
59
|
if (this.head === undefined) {
|
|
57
60
|
this.tail = undefined;
|
|
61
|
+
} else {
|
|
62
|
+
this.head.prev = undefined;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
this.length--;
|
|
@@ -63,6 +68,31 @@ export class LinkedList<T> {
|
|
|
63
68
|
return value;
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Remove a specific node from the list in O(1) time.
|
|
73
|
+
* The node must be a valid node that was returned by push().
|
|
74
|
+
*/
|
|
75
|
+
remove(node: LinkedListNode<T>): void {
|
|
76
|
+
if (node.prev) {
|
|
77
|
+
node.prev.next = node.next;
|
|
78
|
+
} else {
|
|
79
|
+
// Node is the head
|
|
80
|
+
this.head = node.next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (node.next) {
|
|
84
|
+
node.next.prev = node.prev;
|
|
85
|
+
} else {
|
|
86
|
+
// Node is the tail
|
|
87
|
+
this.tail = node.prev;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
node.prev = undefined;
|
|
91
|
+
node.next = undefined;
|
|
92
|
+
this.length--;
|
|
93
|
+
this.meter?.pull();
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
isEmpty() {
|
|
67
97
|
return this.head === undefined;
|
|
68
98
|
}
|
|
@@ -108,7 +138,7 @@ class QueueMeter {
|
|
|
108
138
|
}
|
|
109
139
|
}
|
|
110
140
|
export function meteredList<T>(
|
|
111
|
-
type: "incoming" | "outgoing" | "storage-streaming",
|
|
141
|
+
type: "incoming" | "outgoing" | "storage-streaming" | "load-requests-queue",
|
|
112
142
|
attrs?: Record<string, string | number>,
|
|
113
143
|
) {
|
|
114
144
|
return new LinkedList<T>(new QueueMeter("jazz.messagequeue." + type, attrs));
|