cojson 0.19.21 → 0.19.22
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 +13 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
- package/dist/CojsonMessageChannel/index.d.ts +3 -0
- package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/index.js +2 -0
- package/dist/CojsonMessageChannel/index.js.map +1 -0
- package/dist/CojsonMessageChannel/types.d.ts +149 -0
- package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/types.js +36 -0
- package/dist/CojsonMessageChannel/types.js.map +1 -0
- package/dist/GarbageCollector.d.ts +4 -2
- package/dist/GarbageCollector.d.ts.map +1 -1
- package/dist/GarbageCollector.js +5 -3
- package/dist/GarbageCollector.js.map +1 -1
- package/dist/SyncStateManager.d.ts +3 -3
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +4 -4
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +19 -1
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +29 -5
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/exports.d.ts +1 -0
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +1 -0
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +1 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +3 -2
- package/dist/localNode.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +8 -3
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +12 -3
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +8 -3
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +12 -3
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +5 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +6 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +25 -4
- package/dist/sync.js.map +1 -1
- package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
- package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
- package/dist/tests/CojsonMessageChannel.test.js +236 -0
- package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
- package/dist/tests/GarbageCollector.test.js +87 -13
- package/dist/tests/GarbageCollector.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +33 -1
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +32 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/SyncManager.processQueues.test.js +1 -1
- package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
- package/dist/tests/SyncStateManager.test.js +1 -1
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coPlainText.test.js +1 -1
- package/dist/tests/coPlainText.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +1 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
- package/dist/tests/sync.garbageCollection.test.js +56 -32
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +3 -5
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +1 -1
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +3 -3
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +9 -9
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +7 -7
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.tracking.test.js +35 -4
- package/dist/tests/sync.tracking.test.js.map +1 -1
- package/dist/tests/testStorage.js +2 -2
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +24 -2
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +68 -7
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
- package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
- package/src/CojsonMessageChannel/index.ts +9 -0
- package/src/CojsonMessageChannel/types.ts +200 -0
- package/src/GarbageCollector.ts +5 -5
- package/src/SyncStateManager.ts +6 -6
- package/src/coValueCore/coValueCore.ts +30 -7
- package/src/exports.ts +1 -0
- package/src/localNode.ts +3 -5
- package/src/storage/storageAsync.ts +15 -4
- package/src/storage/storageSync.ts +15 -4
- package/src/storage/types.ts +6 -0
- package/src/sync.ts +33 -4
- package/src/tests/CojsonMessageChannel.test.ts +306 -0
- package/src/tests/GarbageCollector.test.ts +114 -13
- package/src/tests/StorageApiAsync.test.ts +50 -1
- package/src/tests/StorageApiSync.test.ts +49 -0
- package/src/tests/SyncManager.processQueues.test.ts +1 -1
- package/src/tests/SyncStateManager.test.ts +1 -1
- package/src/tests/coPlainText.test.ts +1 -1
- package/src/tests/coValueCore.loadFromStorage.test.ts +2 -0
- package/src/tests/knownState.lazyLoading.test.ts +2 -0
- package/src/tests/sync.garbageCollection.test.ts +69 -36
- package/src/tests/sync.load.test.ts +3 -5
- package/src/tests/sync.mesh.test.ts +1 -1
- package/src/tests/sync.peerReconciliation.test.ts +3 -3
- package/src/tests/sync.storage.test.ts +9 -9
- package/src/tests/sync.storageAsync.test.ts +7 -7
- package/src/tests/sync.tracking.test.ts +54 -4
- package/src/tests/testStorage.ts +2 -2
- package/src/tests/testUtils.ts +85 -6
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for JazzMessageChannel
|
|
3
|
+
*
|
|
4
|
+
* These types support cross-context communication via MessageChannel API
|
|
5
|
+
* and compatible implementations (e.g., Electron's MessageChannelMain).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Duck-typed interface for any object that can receive messages via postMessage.
|
|
10
|
+
*
|
|
11
|
+
* Covers:
|
|
12
|
+
* - Worker
|
|
13
|
+
* - Window (including iframes via contentWindow)
|
|
14
|
+
* - MessagePort
|
|
15
|
+
* - ServiceWorker
|
|
16
|
+
* - Client (Service Worker clients)
|
|
17
|
+
* - Electron's WebContents (renderer windows)
|
|
18
|
+
*/
|
|
19
|
+
export interface PostMessageTarget {
|
|
20
|
+
postMessage(message: unknown, transfer?: MessagePortLike[]): void;
|
|
21
|
+
postMessage(
|
|
22
|
+
message: unknown,
|
|
23
|
+
targetOrigin: string,
|
|
24
|
+
transfer?: MessagePortLike[],
|
|
25
|
+
): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* MessagePort-like interface that covers browser MessagePort and Electron MessagePortMain.
|
|
30
|
+
*
|
|
31
|
+
* Note: Electron's MessagePortMain does not have a start() method,
|
|
32
|
+
* so it's optional here.
|
|
33
|
+
*/
|
|
34
|
+
export interface MessagePortLike {
|
|
35
|
+
postMessage(message: unknown): void;
|
|
36
|
+
addEventListener(
|
|
37
|
+
type: "message" | "messageerror",
|
|
38
|
+
listener: (event: unknown) => void,
|
|
39
|
+
): void;
|
|
40
|
+
removeEventListener(
|
|
41
|
+
type: "message" | "messageerror",
|
|
42
|
+
listener: (event: unknown) => void,
|
|
43
|
+
): void;
|
|
44
|
+
addEventListener(event: "close", listener: () => void): void;
|
|
45
|
+
removeEventListener(type: "close", listener: () => void): void;
|
|
46
|
+
/** Optional - not present on Electron's MessagePortMain */
|
|
47
|
+
start?(): void;
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* MessageChannel-like interface that covers browser MessageChannel
|
|
53
|
+
* and Electron MessageChannelMain.
|
|
54
|
+
*/
|
|
55
|
+
export interface MessageChannelLike {
|
|
56
|
+
port1: MessagePortLike;
|
|
57
|
+
port2: MessagePortLike;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Options for JazzMessageChannel.expose()
|
|
62
|
+
*/
|
|
63
|
+
export interface ExposeOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Unique identifier for the peer connection, sent to the guest during handshake.
|
|
66
|
+
* Both sides will use this same ID for the Peer object.
|
|
67
|
+
* If not provided, a unique ID will be generated using `channel_${Math.random()}`.
|
|
68
|
+
*/
|
|
69
|
+
id?: string;
|
|
70
|
+
|
|
71
|
+
/** Role of the peer in the sync topology */
|
|
72
|
+
role?: "client" | "server";
|
|
73
|
+
|
|
74
|
+
/** Target origin for Window targets (default: "*") */
|
|
75
|
+
targetOrigin?: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A pre-created MessageChannel to use instead of creating a new one.
|
|
79
|
+
* Use this for environments where the global MessageChannel is not available,
|
|
80
|
+
* e.g., Electron main process with MessageChannelMain.
|
|
81
|
+
* If not provided, a new MessageChannel will be created.
|
|
82
|
+
*/
|
|
83
|
+
messageChannel?: MessageChannelLike;
|
|
84
|
+
|
|
85
|
+
/** Callback when the connection closes */
|
|
86
|
+
onClose?: () => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Options for CojsonMessageChannel.waitForConnection()
|
|
91
|
+
*/
|
|
92
|
+
export interface WaitForConnectionOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Expected peer ID to accept.
|
|
95
|
+
* If provided, only handshakes with matching id will be accepted; others are ignored.
|
|
96
|
+
* If not provided, any connection will be accepted.
|
|
97
|
+
*/
|
|
98
|
+
id?: string;
|
|
99
|
+
|
|
100
|
+
/** Role of the peer in the sync topology */
|
|
101
|
+
role?: "client" | "server";
|
|
102
|
+
|
|
103
|
+
/** Allowed origins for Window contexts (default: ["*"]) */
|
|
104
|
+
allowedOrigins?: string[];
|
|
105
|
+
|
|
106
|
+
/** Callback when the connection closes */
|
|
107
|
+
onClose?: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Options for CojsonMessageChannel.acceptFromPort()
|
|
112
|
+
* Note: No timeout option - acceptFromPort waits indefinitely.
|
|
113
|
+
*/
|
|
114
|
+
export interface AcceptFromPortOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Expected peer ID to accept.
|
|
117
|
+
* If provided, only handshakes with matching id will be accepted; others are ignored.
|
|
118
|
+
* If not provided, any connection will be accepted.
|
|
119
|
+
*/
|
|
120
|
+
id?: string;
|
|
121
|
+
|
|
122
|
+
/** Role of the peer in the sync topology */
|
|
123
|
+
role?: "client" | "server";
|
|
124
|
+
|
|
125
|
+
/** Callback when the connection closes */
|
|
126
|
+
onClose?: () => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Port transfer message sent via target.postMessage.
|
|
131
|
+
* The actual port is transferred via Transferable.
|
|
132
|
+
*/
|
|
133
|
+
export interface PortTransferMessage {
|
|
134
|
+
type: "jazz:port";
|
|
135
|
+
/** The peer ID for this connection */
|
|
136
|
+
id: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ready signal sent from host to guest via MessagePort.
|
|
141
|
+
* Contains the peer ID that both sides will use.
|
|
142
|
+
*/
|
|
143
|
+
export interface ReadyMessage {
|
|
144
|
+
type: "jazz:ready";
|
|
145
|
+
/** The peer ID to use on both sides */
|
|
146
|
+
id: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Acknowledgment sent from guest to host via MessagePort.
|
|
151
|
+
* No id needed - guest uses the id from the host's ReadyMessage.
|
|
152
|
+
*/
|
|
153
|
+
export interface ReadyAckMessage {
|
|
154
|
+
type: "jazz:ready";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Union of all control messages used in the handshake protocol.
|
|
159
|
+
*/
|
|
160
|
+
export type ControlMessage =
|
|
161
|
+
| PortTransferMessage
|
|
162
|
+
| ReadyMessage
|
|
163
|
+
| ReadyAckMessage;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Type guard to check if a message is a Jazz control message.
|
|
167
|
+
* Control messages are identified by having a "type" property starting with "jazz:".
|
|
168
|
+
*/
|
|
169
|
+
export function isControlMessage(msg: unknown): msg is ControlMessage {
|
|
170
|
+
return (
|
|
171
|
+
typeof msg === "object" &&
|
|
172
|
+
msg !== null &&
|
|
173
|
+
"type" in msg &&
|
|
174
|
+
typeof msg.type === "string" &&
|
|
175
|
+
msg.type.startsWith("jazz:")
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Type guard to check if a message is a PortTransferMessage.
|
|
181
|
+
*/
|
|
182
|
+
export function isPortTransferMessage(
|
|
183
|
+
msg: unknown,
|
|
184
|
+
): msg is PortTransferMessage {
|
|
185
|
+
return isControlMessage(msg) && msg.type === "jazz:port";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Type guard to check if a message is a ReadyMessage (with id).
|
|
190
|
+
*/
|
|
191
|
+
export function isReadyMessage(msg: unknown): msg is ReadyMessage {
|
|
192
|
+
return isControlMessage(msg) && msg.type === "jazz:ready" && "id" in msg;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Type guard to check if a message is a ReadyAckMessage (without id).
|
|
197
|
+
*/
|
|
198
|
+
export function isReadyAckMessage(msg: unknown): msg is ReadyAckMessage {
|
|
199
|
+
return isControlMessage(msg) && msg.type === "jazz:ready" && !("id" in msg);
|
|
200
|
+
}
|
package/src/GarbageCollector.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { CoValueCore } from "./coValueCore/coValueCore.js";
|
|
|
2
2
|
import { GARBAGE_COLLECTOR_CONFIG } from "./config.js";
|
|
3
3
|
import { RawCoID } from "./ids.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* TTL-based garbage collector for removing unused CoValues from memory.
|
|
7
|
+
*/
|
|
5
8
|
export class GarbageCollector {
|
|
6
9
|
private readonly interval: ReturnType<typeof setInterval>;
|
|
7
10
|
|
|
8
|
-
constructor(
|
|
9
|
-
private readonly coValues: Map<RawCoID, CoValueCore>,
|
|
10
|
-
private readonly garbageCollectGroups: boolean,
|
|
11
|
-
) {
|
|
11
|
+
constructor(private readonly coValues: Map<RawCoID, CoValueCore>) {
|
|
12
12
|
this.interval = setInterval(() => {
|
|
13
13
|
this.collect();
|
|
14
14
|
}, GARBAGE_COLLECTOR_CONFIG.INTERVAL);
|
|
@@ -36,7 +36,7 @@ export class GarbageCollector {
|
|
|
36
36
|
const timeSinceLastAccessed = currentTime - verified.lastAccessed;
|
|
37
37
|
|
|
38
38
|
if (timeSinceLastAccessed > GARBAGE_COLLECTOR_CONFIG.MAX_AGE) {
|
|
39
|
-
coValue.unmount(
|
|
39
|
+
coValue.unmount();
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/SyncStateManager.ts
CHANGED
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
areCurrentSessionsInSyncWith,
|
|
5
5
|
} from "./knownState.js";
|
|
6
6
|
import { PeerState } from "./PeerState.js";
|
|
7
|
-
import { PeerID, SyncManager } from "./sync.js";
|
|
7
|
+
import { Peer, PeerID, SyncManager } from "./sync.js";
|
|
8
8
|
|
|
9
9
|
export type SyncState = {
|
|
10
10
|
uploaded: boolean;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export type GlobalSyncStateListenerCallback = (
|
|
14
|
-
|
|
14
|
+
peer: Peer,
|
|
15
15
|
knownState: CoValueKnownState,
|
|
16
16
|
sync: SyncState,
|
|
17
17
|
) => void;
|
|
@@ -93,10 +93,10 @@ export class SyncStateManager {
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
triggerUpdate(
|
|
96
|
+
triggerUpdate(peer: Peer, id: RawCoID, knownState: CoValueKnownState) {
|
|
97
97
|
const globalListeners = this.listeners;
|
|
98
98
|
const coValueListeners = this.listenersByCoValues.get(id);
|
|
99
|
-
const peerMap = this.listenersByPeersAndCoValues.get(
|
|
99
|
+
const peerMap = this.listenersByPeersAndCoValues.get(peer.id);
|
|
100
100
|
const coValueAndPeerListeners = peerMap?.get(id);
|
|
101
101
|
|
|
102
102
|
if (
|
|
@@ -113,12 +113,12 @@ export class SyncStateManager {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
for (const listener of this.listeners) {
|
|
116
|
-
listener(
|
|
116
|
+
listener(peer, knownState, syncState);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
if (coValueListeners) {
|
|
120
120
|
for (const listener of coValueListeners) {
|
|
121
|
-
listener(
|
|
121
|
+
listener(peer, knownState, syncState);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -373,16 +373,26 @@ export class CoValueCore {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
376
|
+
/**
|
|
377
|
+
* Removes the CoValue from memory.
|
|
378
|
+
*
|
|
379
|
+
* @returns true if the coValue was successfully unmounted, false otherwise
|
|
380
|
+
*/
|
|
381
|
+
unmount(): boolean {
|
|
382
|
+
if (this.listeners.size > 0) {
|
|
383
|
+
// The coValue is still in use
|
|
381
384
|
return false;
|
|
382
385
|
}
|
|
383
386
|
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
for (const dependant of this.dependant) {
|
|
388
|
+
if (this.node.hasCoValue(dependant)) {
|
|
389
|
+
// Another in-memory coValue depends on this coValue
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!this.node.syncManager.isSyncedToServerPeers(this.id)) {
|
|
395
|
+
return false;
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
this.counter.add(-1, { state: this.loadingState });
|
|
@@ -1193,6 +1203,15 @@ export class CoValueCore {
|
|
|
1193
1203
|
return matchingTransactions;
|
|
1194
1204
|
}
|
|
1195
1205
|
|
|
1206
|
+
/**
|
|
1207
|
+
* The CoValues that this CoValue depends on.
|
|
1208
|
+
* We currently track dependencies for:
|
|
1209
|
+
* - Ownership (a CoValue depends on its account/group owner)
|
|
1210
|
+
* - Group membership (a group depends on its direct account/group members)
|
|
1211
|
+
* - Sessions (a CoValue depends on Accounts that made changes to it)
|
|
1212
|
+
* - Branches (a branched CoValue depends on its branch source)
|
|
1213
|
+
* See {@link dependant} for the CoValues that depend on this CoValue.
|
|
1214
|
+
*/
|
|
1196
1215
|
dependencies: Set<RawCoID> = new Set();
|
|
1197
1216
|
incompleteDependencies: Set<RawCoID> = new Set();
|
|
1198
1217
|
private addDependency(dependency: RawCoID) {
|
|
@@ -1238,6 +1257,10 @@ export class CoValueCore {
|
|
|
1238
1257
|
}
|
|
1239
1258
|
}
|
|
1240
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* The CoValues that depend on this CoValue.
|
|
1262
|
+
* This is the inverse relationship of {@link dependencies}.
|
|
1263
|
+
*/
|
|
1241
1264
|
dependant: Set<RawCoID> = new Set();
|
|
1242
1265
|
private addDependant(dependant: RawCoID) {
|
|
1243
1266
|
this.dependant.add(dependant);
|
package/src/exports.ts
CHANGED
package/src/localNode.ts
CHANGED
|
@@ -82,15 +82,12 @@ export class LocalNode {
|
|
|
82
82
|
this.crypto = crypto;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
enableGarbageCollector(
|
|
85
|
+
enableGarbageCollector() {
|
|
86
86
|
if (this.garbageCollector) {
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
this.garbageCollector = new GarbageCollector(
|
|
91
|
-
this.coValues,
|
|
92
|
-
opts?.garbageCollectGroups ?? false,
|
|
93
|
-
);
|
|
90
|
+
this.garbageCollector = new GarbageCollector(this.coValues);
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
setStorage(storage: StorageAPI) {
|
|
@@ -133,6 +130,7 @@ export class LocalNode {
|
|
|
133
130
|
|
|
134
131
|
internalDeleteCoValue(id: RawCoID) {
|
|
135
132
|
this.coValues.delete(id);
|
|
133
|
+
this.storage?.onCoValueUnmounted(id);
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
getCurrentAccountOrAgentID(): RawAccountID | AgentID {
|
|
@@ -34,7 +34,11 @@ import type {
|
|
|
34
34
|
export class StorageApiAsync implements StorageAPI {
|
|
35
35
|
private readonly dbClient: DBClientInterfaceAsync;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Keeps track of CoValues that are in memory, to avoid reloading them from storage
|
|
39
|
+
* when it isn't necessary
|
|
40
|
+
*/
|
|
41
|
+
private inMemoryCoValues = new Set<RawCoID>();
|
|
38
42
|
|
|
39
43
|
// Track pending loads to deduplicate concurrent requests
|
|
40
44
|
private pendingKnownStateLoads = new Map<
|
|
@@ -153,7 +157,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
153
157
|
);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
this.
|
|
160
|
+
this.inMemoryCoValues.add(coValueRow.id);
|
|
157
161
|
|
|
158
162
|
let contentMessage = createContentMessage(coValueRow.id, coValueRow.header);
|
|
159
163
|
|
|
@@ -224,7 +228,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
224
228
|
done?.(true);
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
async pushContentWithDependencies(
|
|
231
|
+
private async pushContentWithDependencies(
|
|
228
232
|
coValueRow: StoredCoValueRow,
|
|
229
233
|
contentMessage: NewContentMessage,
|
|
230
234
|
pushCallback: (data: NewContentMessage) => void,
|
|
@@ -237,7 +241,7 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
237
241
|
const promises = [];
|
|
238
242
|
|
|
239
243
|
for (const dependedOnCoValue of dependedOnCoValuesList) {
|
|
240
|
-
if (this.
|
|
244
|
+
if (this.inMemoryCoValues.has(dependedOnCoValue)) {
|
|
241
245
|
continue;
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -364,6 +368,8 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
364
368
|
});
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
this.inMemoryCoValues.add(id);
|
|
372
|
+
|
|
367
373
|
this.knownStates.handleUpdate(id, knownState);
|
|
368
374
|
|
|
369
375
|
if (invalidAssumptions) {
|
|
@@ -460,7 +466,12 @@ export class StorageApiAsync implements StorageAPI {
|
|
|
460
466
|
this.dbClient.stopTrackingSyncState(id);
|
|
461
467
|
}
|
|
462
468
|
|
|
469
|
+
onCoValueUnmounted(id: RawCoID): void {
|
|
470
|
+
this.inMemoryCoValues.delete(id);
|
|
471
|
+
}
|
|
472
|
+
|
|
463
473
|
close() {
|
|
474
|
+
this.inMemoryCoValues.clear();
|
|
464
475
|
return this.storeQueue.close();
|
|
465
476
|
}
|
|
466
477
|
}
|
|
@@ -37,7 +37,11 @@ import { getPriorityFromHeader } from "../priority.js";
|
|
|
37
37
|
|
|
38
38
|
export class StorageApiSync implements StorageAPI {
|
|
39
39
|
private readonly dbClient: DBClientInterfaceSync;
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Keeps track of CoValues that are in memory, to avoid reloading them from storage
|
|
42
|
+
* when it isn't necessary
|
|
43
|
+
*/
|
|
44
|
+
private inMemoryCoValues = new Set<RawCoID>();
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Queue for streaming content that will be pulled by SyncManager.
|
|
@@ -138,7 +142,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
138
142
|
);
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
this.
|
|
145
|
+
this.inMemoryCoValues.add(coValueRow.id);
|
|
142
146
|
|
|
143
147
|
const priority = getPriorityFromHeader(coValueRow.header);
|
|
144
148
|
const contentMessage = createContentMessage(
|
|
@@ -244,7 +248,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
244
248
|
});
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
async pushContentWithDependencies(
|
|
251
|
+
private async pushContentWithDependencies(
|
|
248
252
|
coValueRow: StoredCoValueRow,
|
|
249
253
|
contentMessage: NewContentMessage,
|
|
250
254
|
pushCallback: (data: NewContentMessage) => void,
|
|
@@ -255,7 +259,7 @@ export class StorageApiSync implements StorageAPI {
|
|
|
255
259
|
);
|
|
256
260
|
|
|
257
261
|
for (const dependedOnCoValue of dependedOnCoValuesList) {
|
|
258
|
-
if (this.
|
|
262
|
+
if (this.inMemoryCoValues.has(dependedOnCoValue)) {
|
|
259
263
|
continue;
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -353,6 +357,8 @@ export class StorageApiSync implements StorageAPI {
|
|
|
353
357
|
});
|
|
354
358
|
}
|
|
355
359
|
|
|
360
|
+
this.inMemoryCoValues.add(id);
|
|
361
|
+
|
|
356
362
|
this.knownStates.handleUpdate(id, knownState);
|
|
357
363
|
|
|
358
364
|
if (invalidAssumptions) {
|
|
@@ -450,7 +456,12 @@ export class StorageApiSync implements StorageAPI {
|
|
|
450
456
|
this.dbClient.stopTrackingSyncState(id);
|
|
451
457
|
}
|
|
452
458
|
|
|
459
|
+
onCoValueUnmounted(id: RawCoID): void {
|
|
460
|
+
this.inMemoryCoValues.delete(id);
|
|
461
|
+
}
|
|
462
|
+
|
|
453
463
|
close() {
|
|
464
|
+
this.inMemoryCoValues.clear();
|
|
454
465
|
return undefined;
|
|
455
466
|
}
|
|
456
467
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface StorageAPI {
|
|
|
67
67
|
callback: (knownState: CoValueKnownState | undefined) => void,
|
|
68
68
|
): void;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Called when a CoValue is unmounted from memory.
|
|
72
|
+
* Used to clean up the metadata associated with that CoValue.
|
|
73
|
+
*/
|
|
74
|
+
onCoValueUnmounted(id: RawCoID): void;
|
|
75
|
+
|
|
70
76
|
close(): Promise<unknown> | undefined;
|
|
71
77
|
}
|
|
72
78
|
|
package/src/sync.ts
CHANGED
|
@@ -102,6 +102,10 @@ export interface Peer {
|
|
|
102
102
|
persistent?: boolean;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function isPersistentServerPeer(peer: Peer | PeerState): boolean {
|
|
106
|
+
return peer.role === "server" && (peer.persistent ?? false);
|
|
107
|
+
}
|
|
108
|
+
|
|
105
109
|
export type ServerPeerSelector = (
|
|
106
110
|
id: RawCoID,
|
|
107
111
|
serverPeers: PeerState[],
|
|
@@ -354,7 +358,7 @@ export class SyncManager {
|
|
|
354
358
|
}
|
|
355
359
|
|
|
356
360
|
startPeerReconciliation(peer: PeerState) {
|
|
357
|
-
if (peer
|
|
361
|
+
if (isPersistentServerPeer(peer)) {
|
|
358
362
|
// Resume syncing unsynced CoValues asynchronously
|
|
359
363
|
this.resumeUnsyncedCoValues().catch((error) => {
|
|
360
364
|
logger.warn("Failed to resume unsynced CoValues:", error);
|
|
@@ -525,7 +529,7 @@ export class SyncManager {
|
|
|
525
529
|
|
|
526
530
|
const unsubscribeFromKnownStatesUpdates =
|
|
527
531
|
peerState.subscribeToKnownStatesUpdates((id, knownState) => {
|
|
528
|
-
this.syncState.triggerUpdate(peer
|
|
532
|
+
this.syncState.triggerUpdate(peer, id, knownState.value());
|
|
529
533
|
});
|
|
530
534
|
|
|
531
535
|
if (!skipReconciliation && peerState.role === "server") {
|
|
@@ -711,8 +715,8 @@ export class SyncManager {
|
|
|
711
715
|
|
|
712
716
|
peer.combineWith(msg.id, knownStateFrom(msg));
|
|
713
717
|
|
|
714
|
-
// The header is a boolean value that tells us if the other peer
|
|
715
|
-
// If it's false
|
|
718
|
+
// The header is a boolean value that tells us if the other peer has information about the header.
|
|
719
|
+
// If it's false at this point it means that the coValue is unavailable on the other peer.
|
|
716
720
|
const availableOnPeer = peer.getOptimisticKnownState(msg.id)?.header;
|
|
717
721
|
|
|
718
722
|
if (!availableOnPeer) {
|
|
@@ -1076,6 +1080,18 @@ export class SyncManager {
|
|
|
1076
1080
|
const isSyncRequired = this.local.syncWhen !== "never";
|
|
1077
1081
|
if (isSyncRequired && peers.length === 0) {
|
|
1078
1082
|
this.unsyncedTracker.add(coValueId);
|
|
1083
|
+
|
|
1084
|
+
// Mark CoValue as synced once a persistent server peer is added and
|
|
1085
|
+
// the CoValue is synced
|
|
1086
|
+
const unsubscribe = this.syncState.subscribeToCoValueUpdates(
|
|
1087
|
+
coValueId,
|
|
1088
|
+
(peer, _knownState, syncState) => {
|
|
1089
|
+
if (isPersistentServerPeer(peer) && syncState.uploaded) {
|
|
1090
|
+
this.unsyncedTracker.remove(coValueId);
|
|
1091
|
+
unsubscribe();
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
);
|
|
1079
1095
|
return;
|
|
1080
1096
|
}
|
|
1081
1097
|
|
|
@@ -1128,6 +1144,19 @@ export class SyncManager {
|
|
|
1128
1144
|
});
|
|
1129
1145
|
}
|
|
1130
1146
|
|
|
1147
|
+
/**
|
|
1148
|
+
* Returns true if the local CoValue changes have been synced to all persistent server peers.
|
|
1149
|
+
*
|
|
1150
|
+
* Used during garbage collection to determine if the coValue is pending sync.
|
|
1151
|
+
*/
|
|
1152
|
+
isSyncedToServerPeers(id: RawCoID): boolean {
|
|
1153
|
+
// If there are currently no server peers, go ahead with GC.
|
|
1154
|
+
// The CoValue will be reloaded into memory and synced when a peer is added.
|
|
1155
|
+
return this.getPersistentServerPeers(id).every((peer) =>
|
|
1156
|
+
this.syncState.isSynced(peer, id),
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1131
1160
|
waitForSyncWithPeer(peerId: PeerID, id: RawCoID, timeout: number) {
|
|
1132
1161
|
const peerState = this.peers[peerId];
|
|
1133
1162
|
|