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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { CO_VALUE_LOADING_CONFIG } from "../config.js";
|
|
2
|
+
import { CoValueCore } from "../exports.js";
|
|
3
|
+
import type { RawCoID } from "../ids.js";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
import type { PeerID } from "../sync.js";
|
|
6
|
+
import { LinkedList, type LinkedListNode, meteredList } from "./LinkedList.js";
|
|
7
|
+
|
|
8
|
+
interface PendingLoad {
|
|
9
|
+
value: CoValueCore;
|
|
10
|
+
sendCallback: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mode for enqueuing load requests:
|
|
15
|
+
* - "high-priority" (default): high priority, processed in order
|
|
16
|
+
* - "low-priority": processed after all high priority requests
|
|
17
|
+
* - "immediate": bypasses the queue entirely, executes immediately
|
|
18
|
+
*/
|
|
19
|
+
export type LoadMode = "low-priority" | "immediate" | "high-priority";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A queue that manages outgoing load requests with throttling.
|
|
23
|
+
*
|
|
24
|
+
* Features:
|
|
25
|
+
* - Limits concurrent in-flight load requests per peer
|
|
26
|
+
* - FIFO order for pending requests
|
|
27
|
+
* - O(1) enqueue and dequeue operations using LinkedList
|
|
28
|
+
* - Manages timeouts for in-flight loads with a single timer
|
|
29
|
+
*/
|
|
30
|
+
export class OutgoingLoadQueue {
|
|
31
|
+
private inFlightLoads: Map<CoValueCore, number> = new Map();
|
|
32
|
+
private highPriorityPending: LinkedList<PendingLoad> = meteredList(
|
|
33
|
+
"load-requests-queue",
|
|
34
|
+
{ priority: "high" },
|
|
35
|
+
);
|
|
36
|
+
private lowPriorityPending: LinkedList<PendingLoad> = meteredList(
|
|
37
|
+
"load-requests-queue",
|
|
38
|
+
{ priority: "low" },
|
|
39
|
+
);
|
|
40
|
+
/**
|
|
41
|
+
* Tracks nodes in the low-priority queue by CoValue ID for O(1) upgrade lookup.
|
|
42
|
+
*/
|
|
43
|
+
private lowPriorityNodes: Map<RawCoID, LinkedListNode<PendingLoad>> =
|
|
44
|
+
new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Tracks nodes in the high-priority queue by CoValue ID for O(1) immediate mode lookup.
|
|
47
|
+
*/
|
|
48
|
+
private highPriorityNodes: Map<RawCoID, LinkedListNode<PendingLoad>> =
|
|
49
|
+
new Map();
|
|
50
|
+
private timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
51
|
+
|
|
52
|
+
constructor(private peerId: PeerID) {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if we can send another load request.
|
|
56
|
+
*/
|
|
57
|
+
private canSend(): boolean {
|
|
58
|
+
return (
|
|
59
|
+
this.inFlightLoads.size <
|
|
60
|
+
CO_VALUE_LOADING_CONFIG.MAX_IN_FLIGHT_LOADS_PER_PEER
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Track that a load request has been sent.
|
|
66
|
+
*/
|
|
67
|
+
private trackSent(coValue: CoValueCore): void {
|
|
68
|
+
const now = performance.now();
|
|
69
|
+
this.inFlightLoads.set(coValue, now);
|
|
70
|
+
this.scheduleTimeoutCheck(CO_VALUE_LOADING_CONFIG.TIMEOUT);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Schedule a timeout check if not already scheduled.
|
|
75
|
+
* Uses a single timer to check all in-flight loads.
|
|
76
|
+
*/
|
|
77
|
+
private scheduleTimeoutCheck(nextTimeout: number): void {
|
|
78
|
+
if (this.timeoutHandle !== null) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.timeoutHandle = setTimeout(() => {
|
|
83
|
+
this.timeoutHandle = null;
|
|
84
|
+
this.checkTimeouts();
|
|
85
|
+
}, nextTimeout);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check all in-flight loads for timeouts and handle them.
|
|
90
|
+
*/
|
|
91
|
+
private checkTimeouts(): void {
|
|
92
|
+
const now = performance.now();
|
|
93
|
+
|
|
94
|
+
let nextTimeout: number | undefined;
|
|
95
|
+
for (const [coValue, sentAt] of this.inFlightLoads.entries()) {
|
|
96
|
+
const timeout = sentAt + CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
|
97
|
+
|
|
98
|
+
if (now >= timeout) {
|
|
99
|
+
if (!coValue.isAvailable()) {
|
|
100
|
+
logger.warn("Load request timed out", {
|
|
101
|
+
id: coValue.id,
|
|
102
|
+
peerId: this.peerId,
|
|
103
|
+
});
|
|
104
|
+
coValue.markNotFoundInPeer(this.peerId);
|
|
105
|
+
} else if (coValue.isStreaming()) {
|
|
106
|
+
logger.warn(
|
|
107
|
+
"Content streaming is taking more than " +
|
|
108
|
+
CO_VALUE_LOADING_CONFIG.TIMEOUT / 1000 +
|
|
109
|
+
"s",
|
|
110
|
+
{
|
|
111
|
+
id: coValue.id,
|
|
112
|
+
peerId: this.peerId,
|
|
113
|
+
knownState: coValue.knownState().sessions,
|
|
114
|
+
streamingTarget: coValue.knownStateWithStreaming().sessions,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.inFlightLoads.delete(coValue);
|
|
120
|
+
this.processQueue();
|
|
121
|
+
} else {
|
|
122
|
+
nextTimeout = Math.min(nextTimeout ?? Infinity, timeout - now);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Reschedule if there are still in-flight loads
|
|
127
|
+
if (nextTimeout) {
|
|
128
|
+
this.scheduleTimeoutCheck(nextTimeout);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
trackUpdate(coValue: CoValueCore): void {
|
|
133
|
+
if (!this.inFlightLoads.has(coValue)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Refresh the timeout for the in-flight load
|
|
138
|
+
this.inFlightLoads.set(coValue, performance.now());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Track that a load request has completed.
|
|
143
|
+
* Triggers processing of pending requests.
|
|
144
|
+
*/
|
|
145
|
+
trackComplete(coValue: CoValueCore): void {
|
|
146
|
+
if (!this.inFlightLoads.has(coValue)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (coValue.isStreaming()) {
|
|
151
|
+
// wait for the next chunk
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.inFlightLoads.delete(coValue);
|
|
156
|
+
this.processQueue();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Enqueue a load request.
|
|
161
|
+
* Immediately processes the queue to send requests if capacity is available.
|
|
162
|
+
* Skips CoValues that are already in-flight or pending.
|
|
163
|
+
*
|
|
164
|
+
* @param coValue - The CoValue to load
|
|
165
|
+
* @param sendCallback - Callback to send the request when ready
|
|
166
|
+
* @param mode - Optional mode: "low-priority" for background loads, "immediate" to bypass queue
|
|
167
|
+
*/
|
|
168
|
+
enqueue(
|
|
169
|
+
value: CoValueCore,
|
|
170
|
+
sendCallback: () => void,
|
|
171
|
+
mode: LoadMode = "high-priority",
|
|
172
|
+
): void {
|
|
173
|
+
if (this.inFlightLoads.has(value)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lowPriorityNode = this.lowPriorityNodes.get(value.id);
|
|
178
|
+
const highPriorityNode = this.highPriorityNodes.get(value.id);
|
|
179
|
+
|
|
180
|
+
switch (mode) {
|
|
181
|
+
case "immediate":
|
|
182
|
+
// Upgrade any low-priority or high-priority requests to immediate priority
|
|
183
|
+
if (lowPriorityNode) {
|
|
184
|
+
this.lowPriorityPending.remove(lowPriorityNode);
|
|
185
|
+
this.lowPriorityNodes.delete(value.id);
|
|
186
|
+
}
|
|
187
|
+
if (highPriorityNode) {
|
|
188
|
+
this.highPriorityPending.remove(highPriorityNode);
|
|
189
|
+
this.highPriorityNodes.delete(value.id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.trackSent(value);
|
|
193
|
+
sendCallback();
|
|
194
|
+
break;
|
|
195
|
+
case "high-priority":
|
|
196
|
+
if (highPriorityNode) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Upgrade any low-priority requests to high-priority
|
|
201
|
+
if (lowPriorityNode) {
|
|
202
|
+
this.lowPriorityPending.remove(lowPriorityNode);
|
|
203
|
+
this.lowPriorityNodes.delete(value.id);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.highPriorityNodes.set(
|
|
207
|
+
value.id,
|
|
208
|
+
this.highPriorityPending.push({ value, sendCallback }),
|
|
209
|
+
);
|
|
210
|
+
this.processQueue();
|
|
211
|
+
break;
|
|
212
|
+
case "low-priority":
|
|
213
|
+
if (lowPriorityNode || highPriorityNode) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.lowPriorityNodes.set(
|
|
218
|
+
value.id,
|
|
219
|
+
this.lowPriorityPending.push({ value, sendCallback }),
|
|
220
|
+
);
|
|
221
|
+
this.processQueue();
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private processing = false;
|
|
227
|
+
/**
|
|
228
|
+
* Process all pending load requests while capacity is available.
|
|
229
|
+
* High-priority requests are processed first, then low-priority.
|
|
230
|
+
*/
|
|
231
|
+
private processQueue(): void {
|
|
232
|
+
if (this.processing || !this.canSend()) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.processing = true;
|
|
236
|
+
|
|
237
|
+
while (this.canSend()) {
|
|
238
|
+
// Try high-priority first
|
|
239
|
+
let next = this.highPriorityPending.shift();
|
|
240
|
+
|
|
241
|
+
if (next) {
|
|
242
|
+
// Remove from the tracking map since we're processing it
|
|
243
|
+
this.highPriorityNodes.delete(next.value.id);
|
|
244
|
+
} else {
|
|
245
|
+
// Fall back to low-priority if high-priority is empty
|
|
246
|
+
next = this.lowPriorityPending.shift();
|
|
247
|
+
if (next) {
|
|
248
|
+
// Remove from the tracking map since we're processing it
|
|
249
|
+
this.lowPriorityNodes.delete(next.value.id);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!next) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.trackSent(next.value);
|
|
258
|
+
next.sendCallback();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.processing = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Clear all state. Called on disconnect.
|
|
266
|
+
* Clears the timeout and all pending/in-flight loads.
|
|
267
|
+
*/
|
|
268
|
+
clear(): void {
|
|
269
|
+
if (this.timeoutHandle !== null) {
|
|
270
|
+
clearTimeout(this.timeoutHandle);
|
|
271
|
+
this.timeoutHandle = null;
|
|
272
|
+
}
|
|
273
|
+
this.inFlightLoads.clear();
|
|
274
|
+
this.highPriorityPending = new LinkedList();
|
|
275
|
+
this.lowPriorityPending = new LinkedList();
|
|
276
|
+
this.highPriorityNodes.clear();
|
|
277
|
+
this.lowPriorityNodes.clear();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get the number of in-flight loads (for testing/debugging).
|
|
282
|
+
*/
|
|
283
|
+
get inFlightCount(): number {
|
|
284
|
+
return this.inFlightLoads.size;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the number of pending loads (for testing/debugging).
|
|
289
|
+
*/
|
|
290
|
+
get pendingCount(): number {
|
|
291
|
+
return this.highPriorityPending.length + this.lowPriorityPending.length;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get the number of high-priority pending loads (for testing/debugging).
|
|
296
|
+
*/
|
|
297
|
+
get highPriorityPendingCount(): number {
|
|
298
|
+
return this.highPriorityPending.length;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get the number of low-priority pending loads (for testing/debugging).
|
|
303
|
+
*/
|
|
304
|
+
get lowPriorityPendingCount(): number {
|
|
305
|
+
return this.lowPriorityPending.length;
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/sync.ts
CHANGED
|
@@ -401,22 +401,18 @@ export class SyncManager {
|
|
|
401
401
|
};
|
|
402
402
|
|
|
403
403
|
for (const coValue of this.local.allCoValues()) {
|
|
404
|
-
if (
|
|
405
|
-
//
|
|
406
|
-
// we try to load it from the peer
|
|
407
|
-
if (!peer.loadRequestSent.has(coValue.id)) {
|
|
408
|
-
peer.trackLoadRequestSent(coValue.id);
|
|
409
|
-
this.trySendToPeer(peer, {
|
|
410
|
-
action: "load",
|
|
411
|
-
header: false,
|
|
412
|
-
id: coValue.id,
|
|
413
|
-
sessions: {},
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
} else {
|
|
417
|
-
// Build the list of coValues ordered by dependency
|
|
418
|
-
// so we can send the load message in the correct order
|
|
404
|
+
if (coValue.isAvailable()) {
|
|
405
|
+
// In memory - build ordered list for dependency-aware sending
|
|
419
406
|
buildOrderedCoValueList(coValue);
|
|
407
|
+
} else if (coValue.loadingState === "unknown") {
|
|
408
|
+
// Skip unknown CoValues - we never tried to load them, so don't
|
|
409
|
+
// restore a subscription we never had. This prevents loading
|
|
410
|
+
// content for CoValues we don't actually care about.
|
|
411
|
+
continue;
|
|
412
|
+
} else if (!peer.loadRequestSent.has(coValue.id)) {
|
|
413
|
+
// For garbageCollected/onlyKnownState: knownState() returns lastKnownState
|
|
414
|
+
// For unavailable/loading/errored: knownState() returns empty state
|
|
415
|
+
peer.sendLoadRequest(coValue, "low-priority");
|
|
420
416
|
}
|
|
421
417
|
|
|
422
418
|
// Fill the missing known states with empty known states
|
|
@@ -431,12 +427,11 @@ export class SyncManager {
|
|
|
431
427
|
* - Subscribe to the coValue updates
|
|
432
428
|
* - Start the sync process in case we or the other peer
|
|
433
429
|
* lacks some transactions
|
|
430
|
+
*
|
|
431
|
+
* Use low priority for reconciliation loads so that user-initiated
|
|
432
|
+
* loads take precedence.
|
|
434
433
|
*/
|
|
435
|
-
peer.
|
|
436
|
-
this.trySendToPeer(peer, {
|
|
437
|
-
action: "load",
|
|
438
|
-
...coValue.knownState(),
|
|
439
|
-
});
|
|
434
|
+
peer.sendLoadRequest(coValue, "low-priority");
|
|
440
435
|
}
|
|
441
436
|
}
|
|
442
437
|
|
|
@@ -515,7 +510,7 @@ export class SyncManager {
|
|
|
515
510
|
currentTimer - lastTimer >
|
|
516
511
|
SYNC_SCHEDULER_CONFIG.INCOMING_MESSAGES_TIME_BUDGET
|
|
517
512
|
) {
|
|
518
|
-
await
|
|
513
|
+
await waitForNextTick();
|
|
519
514
|
lastTimer = performance.now();
|
|
520
515
|
}
|
|
521
516
|
}
|
|
@@ -733,6 +728,8 @@ export class SyncManager {
|
|
|
733
728
|
if (coValue.isAvailable()) {
|
|
734
729
|
this.sendNewContent(msg.id, peer);
|
|
735
730
|
}
|
|
731
|
+
|
|
732
|
+
peer.trackLoadRequestComplete(coValue);
|
|
736
733
|
}
|
|
737
734
|
|
|
738
735
|
recordTransactionsSize(newTransactions: Transaction[], source: string) {
|
|
@@ -751,6 +748,7 @@ export class SyncManager {
|
|
|
751
748
|
) {
|
|
752
749
|
const coValue = this.local.getCoValue(msg.id);
|
|
753
750
|
const peer = from === "storage" || from === "import" ? undefined : from;
|
|
751
|
+
|
|
754
752
|
const sourceRole =
|
|
755
753
|
from === "storage"
|
|
756
754
|
? "storage"
|
|
@@ -767,6 +765,7 @@ export class SyncManager {
|
|
|
767
765
|
};
|
|
768
766
|
}
|
|
769
767
|
|
|
768
|
+
peer?.trackLoadRequestUpdate(coValue);
|
|
770
769
|
coValue.addDependenciesFromContentMessage(msg);
|
|
771
770
|
|
|
772
771
|
// If some of the dependencies are missing, we wait for them to be available
|
|
@@ -786,7 +785,12 @@ export class SyncManager {
|
|
|
786
785
|
peers.push(peer);
|
|
787
786
|
}
|
|
788
787
|
|
|
789
|
-
|
|
788
|
+
// Use immediate mode to bypass the concurrency limit for dependencies
|
|
789
|
+
// We do this to avoid that the dependency load is blocked
|
|
790
|
+
// by the pending dependendant load
|
|
791
|
+
// Also these should be done with the highest priority, because we need to
|
|
792
|
+
// unblock the coValue wait
|
|
793
|
+
dependencyCoValue.load(peers, "immediate");
|
|
790
794
|
}
|
|
791
795
|
}
|
|
792
796
|
|
|
@@ -1014,8 +1018,6 @@ export class SyncManager {
|
|
|
1014
1018
|
peer.trackToldKnownState(msg.id);
|
|
1015
1019
|
}
|
|
1016
1020
|
|
|
1017
|
-
const syncedPeers = [];
|
|
1018
|
-
|
|
1019
1021
|
/**
|
|
1020
1022
|
* Store the content and propagate it to the server peers and the subscribed client peers
|
|
1021
1023
|
*/
|
|
@@ -1029,6 +1031,8 @@ export class SyncManager {
|
|
|
1029
1031
|
}
|
|
1030
1032
|
}
|
|
1031
1033
|
|
|
1034
|
+
peer?.trackLoadRequestComplete(coValue);
|
|
1035
|
+
|
|
1032
1036
|
for (const peer of this.getPeers(coValue.id)) {
|
|
1033
1037
|
/**
|
|
1034
1038
|
* We sync the content against the source peer if it is a client or server peers
|
|
@@ -1042,25 +1046,8 @@ export class SyncManager {
|
|
|
1042
1046
|
// We directly forward the new content to peers that have an active subscription
|
|
1043
1047
|
if (peer.isCoValueSubscribedToPeer(coValue.id)) {
|
|
1044
1048
|
this.sendNewContent(coValue.id, peer);
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
peer.role === "server" &&
|
|
1048
|
-
!peer.loadRequestSent.has(coValue.id)
|
|
1049
|
-
) {
|
|
1050
|
-
const state = coValue.getLoadingStateForPeer(peer.id);
|
|
1051
|
-
|
|
1052
|
-
// Check if there is a inflight load operation and we
|
|
1053
|
-
// are waiting for other peers to send the load request
|
|
1054
|
-
if (state === "unknown") {
|
|
1055
|
-
// Sending a load message to the peer to get to know how much content is missing
|
|
1056
|
-
// before sending the new content
|
|
1057
|
-
this.trySendToPeer(peer, {
|
|
1058
|
-
action: "load",
|
|
1059
|
-
...coValue.knownStateWithStreaming(),
|
|
1060
|
-
});
|
|
1061
|
-
peer.trackLoadRequestSent(coValue.id);
|
|
1062
|
-
syncedPeers.push(peer);
|
|
1063
|
-
}
|
|
1049
|
+
} else if (peer.role === "server") {
|
|
1050
|
+
peer.sendLoadRequest(coValue);
|
|
1064
1051
|
}
|
|
1065
1052
|
}
|
|
1066
1053
|
}
|
|
@@ -1344,3 +1331,10 @@ export function hwrServerPeerSelector(n: number): ServerPeerSelector {
|
|
|
1344
1331
|
.map((wp) => wp.peer);
|
|
1345
1332
|
};
|
|
1346
1333
|
}
|
|
1334
|
+
|
|
1335
|
+
let waitForNextTick = () =>
|
|
1336
|
+
new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
1337
|
+
|
|
1338
|
+
if (typeof setImmediate === "function") {
|
|
1339
|
+
waitForNextTick = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
1340
|
+
}
|
|
@@ -76,6 +76,117 @@ describe("LinkedList", () => {
|
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
describe("remove", () => {
|
|
80
|
+
it("should remove the only element in the list", () => {
|
|
81
|
+
const node = list.push(1);
|
|
82
|
+
list.remove(node);
|
|
83
|
+
|
|
84
|
+
expect(list.length).toBe(0);
|
|
85
|
+
expect(list.head).toBeUndefined();
|
|
86
|
+
expect(list.tail).toBeUndefined();
|
|
87
|
+
expect(node.prev).toBeUndefined();
|
|
88
|
+
expect(node.next).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should remove the head element", () => {
|
|
92
|
+
const node1 = list.push(1);
|
|
93
|
+
const node2 = list.push(2);
|
|
94
|
+
const node3 = list.push(3);
|
|
95
|
+
|
|
96
|
+
list.remove(node1);
|
|
97
|
+
|
|
98
|
+
expect(list.length).toBe(2);
|
|
99
|
+
expect(list.head).toBe(node2);
|
|
100
|
+
expect(list.tail).toBe(node3);
|
|
101
|
+
expect(node2.prev).toBeUndefined();
|
|
102
|
+
expect(node1.prev).toBeUndefined();
|
|
103
|
+
expect(node1.next).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should remove the tail element", () => {
|
|
107
|
+
const node1 = list.push(1);
|
|
108
|
+
const node2 = list.push(2);
|
|
109
|
+
const node3 = list.push(3);
|
|
110
|
+
|
|
111
|
+
list.remove(node3);
|
|
112
|
+
|
|
113
|
+
expect(list.length).toBe(2);
|
|
114
|
+
expect(list.head).toBe(node1);
|
|
115
|
+
expect(list.tail).toBe(node2);
|
|
116
|
+
expect(node2.next).toBeUndefined();
|
|
117
|
+
expect(node3.prev).toBeUndefined();
|
|
118
|
+
expect(node3.next).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should remove a middle element", () => {
|
|
122
|
+
const node1 = list.push(1);
|
|
123
|
+
const node2 = list.push(2);
|
|
124
|
+
const node3 = list.push(3);
|
|
125
|
+
|
|
126
|
+
list.remove(node2);
|
|
127
|
+
|
|
128
|
+
expect(list.length).toBe(2);
|
|
129
|
+
expect(list.head).toBe(node1);
|
|
130
|
+
expect(list.tail).toBe(node3);
|
|
131
|
+
expect(node1.next).toBe(node3);
|
|
132
|
+
expect(node3.prev).toBe(node1);
|
|
133
|
+
expect(node2.prev).toBeUndefined();
|
|
134
|
+
expect(node2.next).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should maintain correct state after multiple removes", () => {
|
|
138
|
+
const node1 = list.push(1);
|
|
139
|
+
const node2 = list.push(2);
|
|
140
|
+
const node3 = list.push(3);
|
|
141
|
+
const node4 = list.push(4);
|
|
142
|
+
|
|
143
|
+
// Remove middle
|
|
144
|
+
list.remove(node2);
|
|
145
|
+
expect(list.length).toBe(3);
|
|
146
|
+
expect(node1.next).toBe(node3);
|
|
147
|
+
expect(node3.prev).toBe(node1);
|
|
148
|
+
|
|
149
|
+
// Remove head
|
|
150
|
+
list.remove(node1);
|
|
151
|
+
expect(list.length).toBe(2);
|
|
152
|
+
expect(list.head).toBe(node3);
|
|
153
|
+
|
|
154
|
+
// Remove tail
|
|
155
|
+
list.remove(node4);
|
|
156
|
+
expect(list.length).toBe(1);
|
|
157
|
+
expect(list.head).toBe(node3);
|
|
158
|
+
expect(list.tail).toBe(node3);
|
|
159
|
+
|
|
160
|
+
// Remove last
|
|
161
|
+
list.remove(node3);
|
|
162
|
+
expect(list.length).toBe(0);
|
|
163
|
+
expect(list.head).toBeUndefined();
|
|
164
|
+
expect(list.tail).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should allow shift after remove", () => {
|
|
168
|
+
const node1 = list.push(1);
|
|
169
|
+
list.push(2);
|
|
170
|
+
list.push(3);
|
|
171
|
+
|
|
172
|
+
list.remove(node1);
|
|
173
|
+
|
|
174
|
+
expect(list.shift()).toBe(2);
|
|
175
|
+
expect(list.shift()).toBe(3);
|
|
176
|
+
expect(list.length).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should allow push after remove", () => {
|
|
180
|
+
const node1 = list.push(1);
|
|
181
|
+
list.remove(node1);
|
|
182
|
+
|
|
183
|
+
list.push(2);
|
|
184
|
+
expect(list.length).toBe(1);
|
|
185
|
+
expect(list.head?.value).toBe(2);
|
|
186
|
+
expect(list.tail?.value).toBe(2);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
79
190
|
describe("edge cases", () => {
|
|
80
191
|
it("should handle push after all elements have been shifted", () => {
|
|
81
192
|
list.push(1);
|