document-drive 1.18.0 → 1.19.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"nanoevents": "^9.0.0",
|
|
34
34
|
"prisma": "^5.18.0",
|
|
35
35
|
"sanitize-filename": "^1.6.3",
|
|
36
|
-
"@powerhousedao/scalars": "1.
|
|
36
|
+
"@powerhousedao/scalars": "1.24.0",
|
|
37
37
|
"document-model": "2.20.0",
|
|
38
|
-
"document-model-libs": "1.
|
|
38
|
+
"document-model-libs": "1.133.0"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
41
|
"@prisma/client": "^5.18.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"redis": "^4.6.15",
|
|
45
45
|
"sequelize": "^6.37.3",
|
|
46
46
|
"sqlite3": "^5.1.7",
|
|
47
|
-
"@powerhousedao/scalars": "1.
|
|
47
|
+
"@powerhousedao/scalars": "1.24.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@prisma/client": "5.17.0",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"vitest-fetch-mock": "^0.3.0",
|
|
67
67
|
"webdriverio": "^9.0.9",
|
|
68
68
|
"document-model": "2.20.0",
|
|
69
|
-
"document-model-libs": "1.
|
|
69
|
+
"document-model-libs": "1.133.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"check-types": "tsc --build",
|
package/src/server/index.ts
CHANGED
|
@@ -662,7 +662,15 @@ export class BaseDocumentDriveServer implements IBaseDocumentDriveServer {
|
|
|
662
662
|
for (const zodListener of drive.state.local.listeners) {
|
|
663
663
|
const transmitter = this.transmitterFactory.instance(
|
|
664
664
|
zodListener.callInfo?.transmitterType ?? "",
|
|
665
|
-
|
|
665
|
+
{
|
|
666
|
+
driveId,
|
|
667
|
+
listenerId: zodListener.listenerId,
|
|
668
|
+
block: zodListener.block,
|
|
669
|
+
filter: zodListener.filter,
|
|
670
|
+
system: zodListener.system,
|
|
671
|
+
label: zodListener.label || undefined,
|
|
672
|
+
callInfo: zodListener.callInfo || undefined,
|
|
673
|
+
},
|
|
666
674
|
this,
|
|
667
675
|
);
|
|
668
676
|
|
|
@@ -2102,7 +2110,15 @@ export class BaseDocumentDriveServer implements IBaseDocumentDriveServer {
|
|
|
2102
2110
|
// create the transmitter
|
|
2103
2111
|
const transmitter = this.transmitterFactory.instance(
|
|
2104
2112
|
zodListener.callInfo?.transmitterType ?? "",
|
|
2105
|
-
|
|
2113
|
+
{
|
|
2114
|
+
driveId,
|
|
2115
|
+
listenerId: zodListener.listenerId,
|
|
2116
|
+
block: zodListener.block,
|
|
2117
|
+
filter: zodListener.filter,
|
|
2118
|
+
system: zodListener.system,
|
|
2119
|
+
label: zodListener.label || undefined,
|
|
2120
|
+
callInfo: zodListener.callInfo || undefined,
|
|
2121
|
+
},
|
|
2106
2122
|
this,
|
|
2107
2123
|
);
|
|
2108
2124
|
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
} from "../types";
|
|
24
24
|
import { StrandUpdateSource } from "./transmitter/types";
|
|
25
25
|
|
|
26
|
+
const ENABLE_SYNC_DEBUG = false;
|
|
27
|
+
|
|
26
28
|
function debounce<T extends unknown[], R>(
|
|
27
29
|
func: (...args: T) => Promise<R>,
|
|
28
30
|
delay = 250,
|
|
@@ -50,26 +52,43 @@ function debounce<T extends unknown[], R>(
|
|
|
50
52
|
|
|
51
53
|
export class ListenerManager implements IListenerManager {
|
|
52
54
|
static LISTENER_UPDATE_DELAY = 250;
|
|
53
|
-
|
|
55
|
+
private debugID = `[LM #${Math.floor(Math.random() * 999)}]`;
|
|
54
56
|
protected driveServer: IBaseDocumentDriveServer;
|
|
57
|
+
protected options: ListenerManagerOptions;
|
|
58
|
+
|
|
55
59
|
// driveId -> listenerId -> listenerState
|
|
56
60
|
protected listenerStateByDriveId = new Map<
|
|
57
61
|
string,
|
|
58
62
|
Map<string, ListenerState>
|
|
59
63
|
>();
|
|
60
|
-
protected options: ListenerManagerOptions;
|
|
61
64
|
|
|
62
65
|
constructor(
|
|
63
66
|
drive: IBaseDocumentDriveServer,
|
|
64
67
|
listenerState = new Map<string, Map<string, ListenerState>>(),
|
|
65
68
|
options: ListenerManagerOptions = DefaultListenerManagerOptions,
|
|
66
69
|
) {
|
|
70
|
+
this.debugLog(`constructor(...)`);
|
|
67
71
|
this.driveServer = drive;
|
|
68
72
|
this.listenerStateByDriveId = listenerState;
|
|
69
73
|
this.options = { ...DefaultListenerManagerOptions, ...options };
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
private debugLog(...data: any[]) {
|
|
77
|
+
if (!ENABLE_SYNC_DEBUG) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (data.length > 0 && typeof data[0] === "string") {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
83
|
+
console.log(`${this.debugID} ${data[0]}`, ...data.slice(1));
|
|
84
|
+
} else {
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
86
|
+
console.log(this.debugID, ...data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
async initialize(handler: DriveUpdateErrorHandler) {
|
|
91
|
+
this.debugLog("initialize(...)");
|
|
73
92
|
// if network connect comes back online
|
|
74
93
|
// then triggers the listeners update
|
|
75
94
|
if (typeof window !== "undefined") {
|
|
@@ -86,6 +105,10 @@ export class ListenerManager implements IListenerManager {
|
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
async setListener(driveId: string, listener: Listener) {
|
|
108
|
+
this.debugLog(
|
|
109
|
+
`setListener(drive: ${driveId}, listener: ${listener.listenerId})`,
|
|
110
|
+
);
|
|
111
|
+
|
|
89
112
|
// slight code smell -- drive id may not need to be on listener or not passed in
|
|
90
113
|
if (driveId !== listener.driveId) {
|
|
91
114
|
throw new Error("Drive ID mismatch");
|
|
@@ -113,6 +136,8 @@ export class ListenerManager implements IListenerManager {
|
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
async removeListener(driveId: string, listenerId: string) {
|
|
139
|
+
this.debugLog("setListener()");
|
|
140
|
+
|
|
116
141
|
const driveMap = this.listenerStateByDriveId.get(driveId);
|
|
117
142
|
if (!driveMap) {
|
|
118
143
|
return false;
|
|
@@ -222,27 +247,46 @@ export class ListenerManager implements IListenerManager {
|
|
|
222
247
|
private async _triggerUpdate(
|
|
223
248
|
source: StrandUpdateSource,
|
|
224
249
|
onError?: (error: Error, driveId: string, listener: ListenerState) => void,
|
|
250
|
+
maxContinues = 500,
|
|
225
251
|
) {
|
|
252
|
+
this.debugLog(
|
|
253
|
+
`_triggerUpdate(source: ${source.type}, maxContinues: ${maxContinues})`,
|
|
254
|
+
this.listenerStateByDriveId,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (maxContinues < 0) {
|
|
258
|
+
throw new Error("Maximum retries exhausted.");
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
const listenerUpdates: ListenerUpdate[] = [];
|
|
262
|
+
|
|
227
263
|
for (const [driveId, drive] of this.listenerStateByDriveId) {
|
|
228
|
-
for (const [
|
|
264
|
+
for (const [listenerId, listenerState] of drive) {
|
|
229
265
|
const transmitter = listenerState.listener.transmitter;
|
|
266
|
+
|
|
230
267
|
if (!transmitter?.transmit) {
|
|
268
|
+
this.debugLog(`Transmitter not set on listener: ${listenerId}`);
|
|
231
269
|
continue;
|
|
232
270
|
}
|
|
233
271
|
|
|
234
|
-
const syncUnits = await this.getListenerSyncUnits(
|
|
235
|
-
driveId,
|
|
236
|
-
listenerState.listener.listenerId,
|
|
237
|
-
);
|
|
238
|
-
|
|
272
|
+
const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
|
|
239
273
|
const strandUpdates: StrandUpdate[] = [];
|
|
274
|
+
|
|
275
|
+
this.debugLog("syncUnits", syncUnits);
|
|
276
|
+
|
|
240
277
|
// TODO change to push one after the other, reusing operation data
|
|
241
278
|
const tasks = syncUnits.map((syncUnit) => async () => {
|
|
242
279
|
const unitState = listenerState.syncUnits.get(syncUnit.syncId);
|
|
243
280
|
|
|
244
281
|
if (unitState && unitState.listenerRev >= syncUnit.revision) {
|
|
282
|
+
this.debugLog(
|
|
283
|
+
`Abandoning push for sync unit ${syncUnit.syncId}: already up-to-date (${unitState.listenerRev} >= ${syncUnit.revision})`,
|
|
284
|
+
);
|
|
245
285
|
return;
|
|
286
|
+
} else {
|
|
287
|
+
this.debugLog(
|
|
288
|
+
`Listener out-of-date for sync unit ${syncUnit.syncId}: ${unitState?.listenerRev} < ${syncUnit.revision}`,
|
|
289
|
+
);
|
|
246
290
|
}
|
|
247
291
|
|
|
248
292
|
const opData: OperationUpdate[] = [];
|
|
@@ -261,6 +305,9 @@ export class ListenerManager implements IListenerManager {
|
|
|
261
305
|
}
|
|
262
306
|
|
|
263
307
|
if (!opData.length) {
|
|
308
|
+
this.debugLog(
|
|
309
|
+
`Abandoning push for ${syncUnit.syncId}: no operations found`,
|
|
310
|
+
);
|
|
264
311
|
return;
|
|
265
312
|
}
|
|
266
313
|
|
|
@@ -272,34 +319,53 @@ export class ListenerManager implements IListenerManager {
|
|
|
272
319
|
scope: syncUnit.scope as OperationScope,
|
|
273
320
|
});
|
|
274
321
|
});
|
|
322
|
+
|
|
275
323
|
if (this.options.sequentialUpdates) {
|
|
324
|
+
this.debugLog(
|
|
325
|
+
`Collecting ${tasks.length} syncUnit strandUpdates in sequence`,
|
|
326
|
+
);
|
|
276
327
|
for (const task of tasks) {
|
|
277
328
|
await task();
|
|
278
329
|
}
|
|
279
330
|
} else {
|
|
331
|
+
this.debugLog(
|
|
332
|
+
`Collecting ${tasks.length} syncUnit strandUpdates in parallel`,
|
|
333
|
+
);
|
|
280
334
|
await Promise.all(tasks.map((task) => task()));
|
|
281
335
|
}
|
|
282
336
|
|
|
283
337
|
if (strandUpdates.length == 0) {
|
|
338
|
+
this.debugLog(`No strandUpdates needed for listener ${listenerId}`);
|
|
284
339
|
continue;
|
|
285
340
|
}
|
|
286
341
|
|
|
287
342
|
listenerState.pendingTimeout = new Date(
|
|
288
343
|
new Date().getTime() / 1000 + 300,
|
|
289
344
|
).toISOString();
|
|
345
|
+
|
|
290
346
|
listenerState.listenerStatus = "PENDING";
|
|
291
347
|
|
|
292
348
|
// TODO update listeners in parallel, blocking for listeners with block=true
|
|
293
349
|
try {
|
|
350
|
+
this.debugLog(
|
|
351
|
+
`_triggerUpdate(source: ${source.type}) > transmitter.transmit`,
|
|
352
|
+
);
|
|
353
|
+
|
|
294
354
|
const listenerRevisions = await transmitter.transmit(
|
|
295
355
|
strandUpdates,
|
|
296
356
|
source,
|
|
297
357
|
);
|
|
298
358
|
|
|
359
|
+
this.debugLog(
|
|
360
|
+
`_triggerUpdate(source: ${source.type}) > transmission succeeded`,
|
|
361
|
+
listenerRevisions,
|
|
362
|
+
);
|
|
363
|
+
|
|
299
364
|
listenerState.pendingTimeout = "0";
|
|
300
365
|
listenerState.listenerStatus = "PENDING";
|
|
301
366
|
|
|
302
367
|
const lastUpdated = new Date().toISOString();
|
|
368
|
+
let continuationNeeded = false;
|
|
303
369
|
|
|
304
370
|
for (const revision of listenerRevisions) {
|
|
305
371
|
const syncUnit = syncUnits.find(
|
|
@@ -308,11 +374,42 @@ export class ListenerManager implements IListenerManager {
|
|
|
308
374
|
revision.scope === unit.scope &&
|
|
309
375
|
revision.branch === unit.branch,
|
|
310
376
|
);
|
|
377
|
+
|
|
311
378
|
if (syncUnit) {
|
|
312
379
|
listenerState.syncUnits.set(syncUnit.syncId, {
|
|
313
380
|
lastUpdated,
|
|
314
381
|
listenerRev: revision.revision,
|
|
315
382
|
});
|
|
383
|
+
|
|
384
|
+
// Check for revision status vv
|
|
385
|
+
const su = strandUpdates.find(
|
|
386
|
+
(su) =>
|
|
387
|
+
su.driveId === revision.driveId &&
|
|
388
|
+
su.documentId === revision.documentId &&
|
|
389
|
+
su.scope === revision.scope &&
|
|
390
|
+
su.branch === revision.branch,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
if (su && su.operations.length > 0) {
|
|
394
|
+
const suIndex = su.operations.at(
|
|
395
|
+
su.operations.length - 1,
|
|
396
|
+
)?.index;
|
|
397
|
+
if (suIndex !== revision.revision) {
|
|
398
|
+
this.debugLog(
|
|
399
|
+
`Revision still out-of-date for ${su.documentId}:${su.scope}:${su.branch} ${suIndex} <> ${revision.revision}`,
|
|
400
|
+
);
|
|
401
|
+
continuationNeeded = true;
|
|
402
|
+
} else {
|
|
403
|
+
this.debugLog(
|
|
404
|
+
`Revision match for ${su.documentId}:${su.scope}:${su.branch} ${suIndex}`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
this.debugLog(
|
|
409
|
+
`Cannot find strand update for (${revision.documentId}:${revision.scope}:${revision.branch} in drive ${revision.driveId})`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
// Check for revision status ^^
|
|
316
413
|
} else {
|
|
317
414
|
logger.warn(
|
|
318
415
|
`Received revision for untracked unit for listener ${listenerState.listener.listenerId}`,
|
|
@@ -324,23 +421,31 @@ export class ListenerManager implements IListenerManager {
|
|
|
324
421
|
for (const revision of listenerRevisions) {
|
|
325
422
|
const error = revision.status === "ERROR";
|
|
326
423
|
if (revision.error?.includes("Missing operations")) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
throw new OperationError(
|
|
336
|
-
revision.status as ErrorStatus,
|
|
337
|
-
undefined,
|
|
338
|
-
revision.error,
|
|
339
|
-
revision.error,
|
|
340
|
-
);
|
|
341
|
-
}
|
|
424
|
+
continuationNeeded = true;
|
|
425
|
+
} else if (error) {
|
|
426
|
+
throw new OperationError(
|
|
427
|
+
revision.status as ErrorStatus,
|
|
428
|
+
undefined,
|
|
429
|
+
revision.error,
|
|
430
|
+
revision.error,
|
|
431
|
+
);
|
|
342
432
|
}
|
|
343
433
|
}
|
|
434
|
+
|
|
435
|
+
if (!continuationNeeded) {
|
|
436
|
+
listenerUpdates.push({
|
|
437
|
+
listenerId: listenerState.listener.listenerId,
|
|
438
|
+
listenerRevisions,
|
|
439
|
+
});
|
|
440
|
+
} else {
|
|
441
|
+
const updates = await this._triggerUpdate(
|
|
442
|
+
source,
|
|
443
|
+
onError,
|
|
444
|
+
maxContinues - 1,
|
|
445
|
+
);
|
|
446
|
+
listenerUpdates.push(...updates);
|
|
447
|
+
}
|
|
448
|
+
|
|
344
449
|
listenerState.listenerStatus = "SUCCESS";
|
|
345
450
|
} catch (e) {
|
|
346
451
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
@@ -350,6 +455,12 @@ export class ListenerManager implements IListenerManager {
|
|
|
350
455
|
}
|
|
351
456
|
}
|
|
352
457
|
}
|
|
458
|
+
|
|
459
|
+
this.debugLog(
|
|
460
|
+
`Returning listener updates (maxContinues: ${maxContinues})`,
|
|
461
|
+
listenerUpdates,
|
|
462
|
+
);
|
|
463
|
+
|
|
353
464
|
return listenerUpdates;
|
|
354
465
|
}
|
|
355
466
|
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
StrandUpdateSource,
|
|
23
23
|
} from "./types";
|
|
24
24
|
|
|
25
|
+
const ENABLE_SYNC_DEBUG = false;
|
|
26
|
+
|
|
25
27
|
export type OperationUpdateGraphQL = Omit<OperationUpdate, "input"> & {
|
|
26
28
|
input: string;
|
|
27
29
|
};
|
|
@@ -44,16 +46,52 @@ export interface IPullResponderTransmitter extends ITransmitter {
|
|
|
44
46
|
getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]>;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
const STATIC_DEBUG_ID = `[PRT #static]`;
|
|
50
|
+
|
|
51
|
+
function staticDebugLog(...data: any[]) {
|
|
52
|
+
if (!ENABLE_SYNC_DEBUG) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (data.length > 0 && typeof data[0] === "string") {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
58
|
+
console.log(`${STATIC_DEBUG_ID} ${data[0]}`, ...data.slice(1));
|
|
59
|
+
} else {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
61
|
+
console.log(STATIC_DEBUG_ID, ...data);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
66
|
+
private debugID = `[PRT #${Math.floor(Math.random() * 999)}]`;
|
|
48
67
|
private listener: Listener;
|
|
49
68
|
private manager: IListenerManager;
|
|
50
69
|
|
|
51
70
|
constructor(listener: Listener, manager: IListenerManager) {
|
|
52
71
|
this.listener = listener;
|
|
53
72
|
this.manager = manager;
|
|
73
|
+
this.debugLog(`constructor(listener: ${listener.listenerId})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private debugLog(...data: any[]) {
|
|
77
|
+
if (!ENABLE_SYNC_DEBUG) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (data.length > 0 && typeof data[0] === "string") {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
83
|
+
console.log(`${this.debugID} ${data[0]}`, ...data.slice(1));
|
|
84
|
+
} else {
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
86
|
+
console.log(this.debugID, ...data);
|
|
87
|
+
}
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]> {
|
|
91
|
+
this.debugLog(
|
|
92
|
+
`getStrands(drive: ${this.listener.driveId}, listener: ${this.listener.listenerId})`,
|
|
93
|
+
);
|
|
94
|
+
|
|
57
95
|
return this.manager.getStrands(
|
|
58
96
|
this.listener.driveId,
|
|
59
97
|
this.listener.listenerId,
|
|
@@ -71,6 +109,11 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
71
109
|
listenerId: string,
|
|
72
110
|
revisions: ListenerRevision[],
|
|
73
111
|
): Promise<boolean> {
|
|
112
|
+
this.debugLog(
|
|
113
|
+
`processAcknowledge(drive: ${driveId}, listener: ${listenerId})`,
|
|
114
|
+
revisions,
|
|
115
|
+
);
|
|
116
|
+
|
|
74
117
|
const syncUnits = await this.manager.getListenerSyncUnitIds(
|
|
75
118
|
driveId,
|
|
76
119
|
listenerId,
|
|
@@ -106,6 +149,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
106
149
|
url: string,
|
|
107
150
|
filter: ListenerFilter,
|
|
108
151
|
): Promise<Listener["listenerId"]> {
|
|
152
|
+
staticDebugLog(`registerPullResponder(url: ${url})`, filter);
|
|
109
153
|
// graphql request to switchboard
|
|
110
154
|
const result = await requestGraphql<{
|
|
111
155
|
registerPullResponderListener: {
|
|
@@ -141,6 +185,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
141
185
|
listenerId: string,
|
|
142
186
|
options?: GetStrandsOptions, // TODO add support for since
|
|
143
187
|
): Promise<StrandUpdate[]> {
|
|
188
|
+
staticDebugLog(`pullStrands(url: ${url}, listener: ${listenerId})`);
|
|
144
189
|
const result = await requestGraphql<PullStrandsGraphQL>(
|
|
145
190
|
url,
|
|
146
191
|
gql`
|
|
@@ -207,6 +252,11 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
207
252
|
listenerId: string,
|
|
208
253
|
revisions: ListenerRevision[],
|
|
209
254
|
): Promise<boolean> {
|
|
255
|
+
staticDebugLog(
|
|
256
|
+
`acknowledgeStrands(url: ${url}, listener: ${listenerId})`,
|
|
257
|
+
revisions,
|
|
258
|
+
);
|
|
259
|
+
|
|
210
260
|
const result = await requestGraphql<{ acknowledge: boolean }>(
|
|
211
261
|
url,
|
|
212
262
|
gql`
|
|
@@ -241,6 +291,8 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
241
291
|
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
242
292
|
onAcknowledge?: (success: boolean) => void,
|
|
243
293
|
) {
|
|
294
|
+
staticDebugLog(`executePull(driveId: ${driveId}), trigger:`, trigger);
|
|
295
|
+
|
|
244
296
|
try {
|
|
245
297
|
const { url, listenerId } = trigger.data;
|
|
246
298
|
const strands = await PullResponderTransmitter.pullStrands(
|
|
@@ -323,6 +375,8 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
323
375
|
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
324
376
|
onAcknowledge?: (success: boolean) => void,
|
|
325
377
|
): CancelPullLoop {
|
|
378
|
+
staticDebugLog(`setupPull(drive: ${driveId}), trigger:`, trigger);
|
|
379
|
+
|
|
326
380
|
const { interval } = trigger.data;
|
|
327
381
|
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
328
382
|
if (interval) {
|
|
@@ -341,6 +395,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
341
395
|
|
|
342
396
|
const executeLoop = async () => {
|
|
343
397
|
while (!isCancelled) {
|
|
398
|
+
staticDebugLog("Execute loop...");
|
|
344
399
|
await this.executePull(
|
|
345
400
|
driveId,
|
|
346
401
|
trigger,
|
|
@@ -350,6 +405,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
350
405
|
onAcknowledge,
|
|
351
406
|
);
|
|
352
407
|
await new Promise((resolve) => {
|
|
408
|
+
staticDebugLog(`Scheduling next pull in ${loopInterval} ms`);
|
|
353
409
|
timeout = setTimeout(resolve, loopInterval) as unknown as number;
|
|
354
410
|
});
|
|
355
411
|
}
|
|
@@ -370,6 +426,10 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
370
426
|
url: string,
|
|
371
427
|
options: Pick<RemoteDriveOptions, "pullInterval" | "pullFilter">,
|
|
372
428
|
): Promise<PullResponderTrigger> {
|
|
429
|
+
staticDebugLog(
|
|
430
|
+
`createPullResponderTrigger(drive: ${driveId}, url: ${url})`,
|
|
431
|
+
);
|
|
432
|
+
|
|
373
433
|
const { pullFilter, pullInterval } = options;
|
|
374
434
|
const listenerId = await PullResponderTransmitter.registerPullResponder(
|
|
375
435
|
driveId,
|
|
@@ -4,13 +4,31 @@ import { logger } from "../../../utils/logger";
|
|
|
4
4
|
import { ListenerRevision, StrandUpdate } from "../../types";
|
|
5
5
|
import { ITransmitter, StrandUpdateSource } from "./types";
|
|
6
6
|
|
|
7
|
+
const ENABLE_SYNC_DEBUG = false;
|
|
8
|
+
const SYNC_OPS_BATCH_LIMIT = 10;
|
|
9
|
+
|
|
7
10
|
export class SwitchboardPushTransmitter implements ITransmitter {
|
|
8
11
|
private targetURL: string;
|
|
12
|
+
private debugID = `[SPT #${Math.floor(Math.random() * 999)}]`;
|
|
9
13
|
|
|
10
14
|
constructor(targetURL: string) {
|
|
11
15
|
this.targetURL = targetURL;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
private debugLog(...data: any[]) {
|
|
19
|
+
if (!ENABLE_SYNC_DEBUG) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (data.length > 0 && typeof data[0] === "string") {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
25
|
+
console.log(`${this.debugID} ${data[0]}`, ...data.slice(1));
|
|
26
|
+
} else {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
28
|
+
console.log(this.debugID, ...data);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
async transmit(
|
|
15
33
|
strands: StrandUpdate[],
|
|
16
34
|
source: StrandUpdateSource,
|
|
@@ -19,6 +37,7 @@ export class SwitchboardPushTransmitter implements ITransmitter {
|
|
|
19
37
|
source.type === "trigger" &&
|
|
20
38
|
source.trigger.data?.url === this.targetURL
|
|
21
39
|
) {
|
|
40
|
+
this.debugLog(`Cutting trigger loop from ${this.targetURL}.`);
|
|
22
41
|
return strands.map((strand) => ({
|
|
23
42
|
driveId: strand.driveId,
|
|
24
43
|
documentId: strand.documentId,
|
|
@@ -29,6 +48,39 @@ export class SwitchboardPushTransmitter implements ITransmitter {
|
|
|
29
48
|
}));
|
|
30
49
|
}
|
|
31
50
|
|
|
51
|
+
const culledStrands: StrandUpdate[] = [];
|
|
52
|
+
let opsCounter = 0;
|
|
53
|
+
|
|
54
|
+
for (
|
|
55
|
+
let s = 0;
|
|
56
|
+
opsCounter <= SYNC_OPS_BATCH_LIMIT && s < strands.length;
|
|
57
|
+
s++
|
|
58
|
+
) {
|
|
59
|
+
const currentStrand = strands.at(s);
|
|
60
|
+
if (!currentStrand) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const newOps = Math.min(
|
|
64
|
+
SYNC_OPS_BATCH_LIMIT - opsCounter,
|
|
65
|
+
currentStrand.operations.length,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
culledStrands.push({
|
|
69
|
+
...currentStrand,
|
|
70
|
+
operations: currentStrand.operations.slice(0, newOps),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
opsCounter += newOps;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.debugLog(
|
|
77
|
+
` Total update: [${strands.map((s) => s.operations.length).join(", ")}] operations`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
this.debugLog(
|
|
81
|
+
`Culled update: [${culledStrands.map((s) => s.operations.length).join(", ")}] operations`,
|
|
82
|
+
);
|
|
83
|
+
|
|
32
84
|
// Send Graphql mutation to switchboard
|
|
33
85
|
try {
|
|
34
86
|
const { pushUpdates } = await requestGraphql<{
|
|
@@ -49,7 +101,7 @@ export class SwitchboardPushTransmitter implements ITransmitter {
|
|
|
49
101
|
}
|
|
50
102
|
`,
|
|
51
103
|
{
|
|
52
|
-
strands:
|
|
104
|
+
strands: culledStrands.map((strand) => ({
|
|
53
105
|
...strand,
|
|
54
106
|
operations: strand.operations.map((op) => ({
|
|
55
107
|
...op,
|