document-drive 1.17.2 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/package.json +11 -9
- package/src/server/index.ts +341 -232
- package/src/server/listener/manager.ts +265 -144
- package/src/server/listener/transmitter/pull-responder.ts +64 -11
- package/src/server/listener/transmitter/switchboard-push.ts +56 -13
- package/src/server/types.ts +114 -144
|
@@ -6,20 +6,24 @@ import { OperationScope } from "document-model/document";
|
|
|
6
6
|
import { logger } from "../../utils/logger";
|
|
7
7
|
import { OperationError } from "../error";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
DefaultListenerManagerOptions,
|
|
10
|
+
DriveUpdateErrorHandler,
|
|
10
11
|
ErrorStatus,
|
|
11
12
|
GetStrandsOptions,
|
|
13
|
+
IBaseDocumentDriveServer,
|
|
14
|
+
IListenerManager,
|
|
12
15
|
Listener,
|
|
16
|
+
ListenerManagerOptions,
|
|
13
17
|
ListenerState,
|
|
14
18
|
ListenerUpdate,
|
|
15
19
|
OperationUpdate,
|
|
16
20
|
StrandUpdate,
|
|
17
21
|
SynchronizationUnit,
|
|
22
|
+
SynchronizationUnitQuery,
|
|
18
23
|
} from "../types";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import { ITransmitter, StrandUpdateSource } from "./transmitter/types";
|
|
24
|
+
import { StrandUpdateSource } from "./transmitter/types";
|
|
25
|
+
|
|
26
|
+
const ENABLE_SYNC_DEBUG = false;
|
|
23
27
|
|
|
24
28
|
function debounce<T extends unknown[], R>(
|
|
25
29
|
func: (...args: T) => Promise<R>,
|
|
@@ -45,30 +49,81 @@ function debounce<T extends unknown[], R>(
|
|
|
45
49
|
});
|
|
46
50
|
};
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
export class ListenerManager implements IListenerManager {
|
|
49
54
|
static LISTENER_UPDATE_DELAY = 250;
|
|
55
|
+
private debugID = `[LM #${Math.floor(Math.random() * 999)}]`;
|
|
56
|
+
protected driveServer: IBaseDocumentDriveServer;
|
|
57
|
+
protected options: ListenerManagerOptions;
|
|
58
|
+
|
|
59
|
+
// driveId -> listenerId -> listenerState
|
|
60
|
+
protected listenerStateByDriveId = new Map<
|
|
61
|
+
string,
|
|
62
|
+
Map<string, ListenerState>
|
|
63
|
+
>();
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
drive: IBaseDocumentDriveServer,
|
|
67
|
+
listenerState = new Map<string, Map<string, ListenerState>>(),
|
|
68
|
+
options: ListenerManagerOptions = DefaultListenerManagerOptions,
|
|
69
|
+
) {
|
|
70
|
+
this.debugLog(`constructor(...)`);
|
|
71
|
+
this.driveServer = drive;
|
|
72
|
+
this.listenerStateByDriveId = listenerState;
|
|
73
|
+
this.options = { ...DefaultListenerManagerOptions, ...options };
|
|
74
|
+
}
|
|
50
75
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
|
|
90
|
+
async initialize(handler: DriveUpdateErrorHandler) {
|
|
91
|
+
this.debugLog("initialize(...)");
|
|
92
|
+
// if network connect comes back online
|
|
93
|
+
// then triggers the listeners update
|
|
94
|
+
if (typeof window !== "undefined") {
|
|
95
|
+
window.addEventListener("online", () => {
|
|
96
|
+
this.triggerUpdate(false, { type: "local" }, handler).catch((error) => {
|
|
97
|
+
logger.error("Non handled error updating listeners", error);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
56
101
|
}
|
|
57
102
|
|
|
58
103
|
driveHasListeners(driveId: string) {
|
|
59
|
-
return this.
|
|
104
|
+
return this.listenerStateByDriveId.has(driveId);
|
|
60
105
|
}
|
|
61
106
|
|
|
62
|
-
async
|
|
63
|
-
|
|
107
|
+
async setListener(driveId: string, listener: Listener) {
|
|
108
|
+
this.debugLog(
|
|
109
|
+
`setListener(drive: ${driveId}, listener: ${listener.listenerId})`,
|
|
110
|
+
);
|
|
64
111
|
|
|
65
|
-
|
|
66
|
-
|
|
112
|
+
// slight code smell -- drive id may not need to be on listener or not passed in
|
|
113
|
+
if (driveId !== listener.driveId) {
|
|
114
|
+
throw new Error("Drive ID mismatch");
|
|
67
115
|
}
|
|
68
116
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
let existingState;
|
|
118
|
+
try {
|
|
119
|
+
existingState = this.getListenerState(driveId, listener.listenerId);
|
|
120
|
+
} catch {
|
|
121
|
+
existingState = {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// keep existing state if it exists
|
|
125
|
+
this.setListenerState(driveId, listener.listenerId, {
|
|
126
|
+
...existingState,
|
|
72
127
|
block: listener.block,
|
|
73
128
|
driveId: listener.driveId,
|
|
74
129
|
pendingTimeout: "0",
|
|
@@ -77,38 +132,13 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
77
132
|
syncUnits: new Map(),
|
|
78
133
|
});
|
|
79
134
|
|
|
80
|
-
let transmitter: ITransmitter | undefined;
|
|
81
|
-
|
|
82
|
-
switch (listener.callInfo?.transmitterType) {
|
|
83
|
-
case "SwitchboardPush": {
|
|
84
|
-
transmitter = new SwitchboardPushTransmitter(listener, this.drive);
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
case "PullResponder": {
|
|
89
|
-
transmitter = new PullResponderTransmitter(listener, this.drive, this);
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
case "Internal": {
|
|
93
|
-
transmitter = new InternalTransmitter(listener, this.drive);
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!transmitter) {
|
|
99
|
-
throw new Error("Transmitter not found");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const driveTransmitters = this.transmitters[drive] || {};
|
|
103
|
-
driveTransmitters[listener.listenerId] = transmitter;
|
|
104
|
-
this.transmitters[drive] = driveTransmitters;
|
|
105
|
-
|
|
106
135
|
this.triggerUpdate(true, { type: "local" });
|
|
107
|
-
return Promise.resolve(transmitter);
|
|
108
136
|
}
|
|
109
137
|
|
|
110
138
|
async removeListener(driveId: string, listenerId: string) {
|
|
111
|
-
|
|
139
|
+
this.debugLog("setListener()");
|
|
140
|
+
|
|
141
|
+
const driveMap = this.listenerStateByDriveId.get(driveId);
|
|
112
142
|
if (!driveMap) {
|
|
113
143
|
return false;
|
|
114
144
|
}
|
|
@@ -119,8 +149,8 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
119
149
|
async removeSyncUnits(
|
|
120
150
|
driveId: string,
|
|
121
151
|
syncUnits: Pick<SynchronizationUnit, "syncId">[],
|
|
122
|
-
) {
|
|
123
|
-
const listeners = this.
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
const listeners = this.listenerStateByDriveId.get(driveId);
|
|
124
154
|
if (!listeners) {
|
|
125
155
|
return;
|
|
126
156
|
}
|
|
@@ -140,38 +170,35 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
140
170
|
onError?: (error: Error, driveId: string, listener: ListenerState) => void,
|
|
141
171
|
forceSync = false,
|
|
142
172
|
) {
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
173
|
+
const listenerIdToListenerState = this.listenerStateByDriveId.get(driveId);
|
|
174
|
+
if (!listenerIdToListenerState) {
|
|
145
175
|
return [];
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
const outdatedListeners: Listener[] = [];
|
|
149
|
-
for (const [,
|
|
179
|
+
for (const [, listenerState] of listenerIdToListenerState) {
|
|
150
180
|
if (
|
|
151
181
|
outdatedListeners.find(
|
|
152
|
-
(l) => l.listenerId ===
|
|
182
|
+
(l) => l.listenerId === listenerState.listener.listenerId,
|
|
153
183
|
)
|
|
154
184
|
) {
|
|
155
185
|
continue;
|
|
156
186
|
}
|
|
157
187
|
|
|
158
|
-
const transmitter =
|
|
159
|
-
driveId,
|
|
160
|
-
listener.listener.listenerId,
|
|
161
|
-
);
|
|
188
|
+
const transmitter = listenerState.listener.transmitter;
|
|
162
189
|
if (!transmitter?.transmit) {
|
|
163
190
|
continue;
|
|
164
191
|
}
|
|
165
192
|
|
|
166
193
|
for (const syncUnit of syncUnits) {
|
|
167
|
-
if (!this._checkFilter(
|
|
194
|
+
if (!this._checkFilter(listenerState.listener.filter, syncUnit)) {
|
|
168
195
|
continue;
|
|
169
196
|
}
|
|
170
197
|
|
|
171
|
-
const listenerRev =
|
|
198
|
+
const listenerRev = listenerState.syncUnits.get(syncUnit.syncId);
|
|
172
199
|
|
|
173
200
|
if (!listenerRev || listenerRev.listenerRev < syncUnit.revision) {
|
|
174
|
-
outdatedListeners.push(
|
|
201
|
+
outdatedListeners.push(listenerState.listener);
|
|
175
202
|
break;
|
|
176
203
|
}
|
|
177
204
|
}
|
|
@@ -190,7 +217,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
190
217
|
syncId: string,
|
|
191
218
|
listenerRev: number,
|
|
192
219
|
): Promise<void> {
|
|
193
|
-
const drive = this.
|
|
220
|
+
const drive = this.listenerStateByDriveId.get(driveId);
|
|
194
221
|
if (!drive) {
|
|
195
222
|
return;
|
|
196
223
|
}
|
|
@@ -220,32 +247,51 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
220
247
|
private async _triggerUpdate(
|
|
221
248
|
source: StrandUpdateSource,
|
|
222
249
|
onError?: (error: Error, driveId: string, listener: ListenerState) => void,
|
|
250
|
+
maxContinues = 500,
|
|
223
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
|
+
|
|
224
261
|
const listenerUpdates: ListenerUpdate[] = [];
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
|
|
263
|
+
for (const [driveId, drive] of this.listenerStateByDriveId) {
|
|
264
|
+
for (const [listenerId, listenerState] of drive) {
|
|
265
|
+
const transmitter = listenerState.listener.transmitter;
|
|
266
|
+
|
|
228
267
|
if (!transmitter?.transmit) {
|
|
268
|
+
this.debugLog(`Transmitter not set on listener: ${listenerId}`);
|
|
229
269
|
continue;
|
|
230
270
|
}
|
|
231
271
|
|
|
232
|
-
const syncUnits = await this.getListenerSyncUnits(
|
|
233
|
-
driveId,
|
|
234
|
-
listener.listener.listenerId,
|
|
235
|
-
);
|
|
236
|
-
|
|
272
|
+
const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
|
|
237
273
|
const strandUpdates: StrandUpdate[] = [];
|
|
274
|
+
|
|
275
|
+
this.debugLog("syncUnits", syncUnits);
|
|
276
|
+
|
|
238
277
|
// TODO change to push one after the other, reusing operation data
|
|
239
278
|
const tasks = syncUnits.map((syncUnit) => async () => {
|
|
240
|
-
const unitState =
|
|
279
|
+
const unitState = listenerState.syncUnits.get(syncUnit.syncId);
|
|
241
280
|
|
|
242
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
|
+
);
|
|
243
285
|
return;
|
|
286
|
+
} else {
|
|
287
|
+
this.debugLog(
|
|
288
|
+
`Listener out-of-date for sync unit ${syncUnit.syncId}: ${unitState?.listenerRev} < ${syncUnit.revision}`,
|
|
289
|
+
);
|
|
244
290
|
}
|
|
245
291
|
|
|
246
292
|
const opData: OperationUpdate[] = [];
|
|
247
293
|
try {
|
|
248
|
-
const data = await this.
|
|
294
|
+
const data = await this.driveServer.getOperationData(
|
|
249
295
|
// TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
|
|
250
296
|
driveId,
|
|
251
297
|
syncUnit.syncId,
|
|
@@ -259,6 +305,9 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
259
305
|
}
|
|
260
306
|
|
|
261
307
|
if (!opData.length) {
|
|
308
|
+
this.debugLog(
|
|
309
|
+
`Abandoning push for ${syncUnit.syncId}: no operations found`,
|
|
310
|
+
);
|
|
262
311
|
return;
|
|
263
312
|
}
|
|
264
313
|
|
|
@@ -270,34 +319,53 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
270
319
|
scope: syncUnit.scope as OperationScope,
|
|
271
320
|
});
|
|
272
321
|
});
|
|
322
|
+
|
|
273
323
|
if (this.options.sequentialUpdates) {
|
|
324
|
+
this.debugLog(
|
|
325
|
+
`Collecting ${tasks.length} syncUnit strandUpdates in sequence`,
|
|
326
|
+
);
|
|
274
327
|
for (const task of tasks) {
|
|
275
328
|
await task();
|
|
276
329
|
}
|
|
277
330
|
} else {
|
|
331
|
+
this.debugLog(
|
|
332
|
+
`Collecting ${tasks.length} syncUnit strandUpdates in parallel`,
|
|
333
|
+
);
|
|
278
334
|
await Promise.all(tasks.map((task) => task()));
|
|
279
335
|
}
|
|
280
336
|
|
|
281
337
|
if (strandUpdates.length == 0) {
|
|
338
|
+
this.debugLog(`No strandUpdates needed for listener ${listenerId}`);
|
|
282
339
|
continue;
|
|
283
340
|
}
|
|
284
341
|
|
|
285
|
-
|
|
342
|
+
listenerState.pendingTimeout = new Date(
|
|
286
343
|
new Date().getTime() / 1000 + 300,
|
|
287
344
|
).toISOString();
|
|
288
|
-
|
|
345
|
+
|
|
346
|
+
listenerState.listenerStatus = "PENDING";
|
|
289
347
|
|
|
290
348
|
// TODO update listeners in parallel, blocking for listeners with block=true
|
|
291
349
|
try {
|
|
350
|
+
this.debugLog(
|
|
351
|
+
`_triggerUpdate(source: ${source.type}) > transmitter.transmit`,
|
|
352
|
+
);
|
|
353
|
+
|
|
292
354
|
const listenerRevisions = await transmitter.transmit(
|
|
293
355
|
strandUpdates,
|
|
294
356
|
source,
|
|
295
357
|
);
|
|
296
358
|
|
|
297
|
-
|
|
298
|
-
|
|
359
|
+
this.debugLog(
|
|
360
|
+
`_triggerUpdate(source: ${source.type}) > transmission succeeded`,
|
|
361
|
+
listenerRevisions,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
listenerState.pendingTimeout = "0";
|
|
365
|
+
listenerState.listenerStatus = "PENDING";
|
|
299
366
|
|
|
300
367
|
const lastUpdated = new Date().toISOString();
|
|
368
|
+
let continuationNeeded = false;
|
|
301
369
|
|
|
302
370
|
for (const revision of listenerRevisions) {
|
|
303
371
|
const syncUnit = syncUnits.find(
|
|
@@ -306,14 +374,45 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
306
374
|
revision.scope === unit.scope &&
|
|
307
375
|
revision.branch === unit.branch,
|
|
308
376
|
);
|
|
377
|
+
|
|
309
378
|
if (syncUnit) {
|
|
310
|
-
|
|
379
|
+
listenerState.syncUnits.set(syncUnit.syncId, {
|
|
311
380
|
lastUpdated,
|
|
312
381
|
listenerRev: revision.revision,
|
|
313
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 ^^
|
|
314
413
|
} else {
|
|
315
414
|
logger.warn(
|
|
316
|
-
`Received revision for untracked unit for listener ${
|
|
415
|
+
`Received revision for untracked unit for listener ${listenerState.listener.listenerId}`,
|
|
317
416
|
revision,
|
|
318
417
|
);
|
|
319
418
|
}
|
|
@@ -322,32 +421,46 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
322
421
|
for (const revision of listenerRevisions) {
|
|
323
422
|
const error = revision.status === "ERROR";
|
|
324
423
|
if (revision.error?.includes("Missing operations")) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
throw new OperationError(
|
|
334
|
-
revision.status as ErrorStatus,
|
|
335
|
-
undefined,
|
|
336
|
-
revision.error,
|
|
337
|
-
revision.error,
|
|
338
|
-
);
|
|
339
|
-
}
|
|
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
|
+
);
|
|
340
432
|
}
|
|
341
433
|
}
|
|
342
|
-
|
|
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
|
+
|
|
449
|
+
listenerState.listenerStatus = "SUCCESS";
|
|
343
450
|
} catch (e) {
|
|
344
451
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
345
|
-
onError?.(e as Error, driveId,
|
|
346
|
-
|
|
452
|
+
onError?.(e as Error, driveId, listenerState);
|
|
453
|
+
listenerState.listenerStatus =
|
|
347
454
|
e instanceof OperationError ? e.status : "ERROR";
|
|
348
455
|
}
|
|
349
456
|
}
|
|
350
457
|
}
|
|
458
|
+
|
|
459
|
+
this.debugLog(
|
|
460
|
+
`Returning listener updates (maxContinues: ${maxContinues})`,
|
|
461
|
+
listenerUpdates,
|
|
462
|
+
);
|
|
463
|
+
|
|
351
464
|
return listenerUpdates;
|
|
352
465
|
}
|
|
353
466
|
|
|
@@ -378,12 +491,12 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
378
491
|
listenerId: string,
|
|
379
492
|
loadedDrive?: DocumentDriveDocument,
|
|
380
493
|
) {
|
|
381
|
-
const listener = this.
|
|
494
|
+
const listener = this.listenerStateByDriveId.get(driveId)?.get(listenerId);
|
|
382
495
|
if (!listener) {
|
|
383
496
|
return [];
|
|
384
497
|
}
|
|
385
498
|
const filter = listener.listener.filter;
|
|
386
|
-
return this.
|
|
499
|
+
return this.driveServer.getSynchronizationUnits(
|
|
387
500
|
driveId,
|
|
388
501
|
filter.documentId ?? ["*"],
|
|
389
502
|
filter.scope ?? ["*"],
|
|
@@ -393,13 +506,16 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
393
506
|
);
|
|
394
507
|
}
|
|
395
508
|
|
|
396
|
-
getListenerSyncUnitIds(
|
|
397
|
-
|
|
509
|
+
getListenerSyncUnitIds(
|
|
510
|
+
driveId: string,
|
|
511
|
+
listenerId: string,
|
|
512
|
+
): Promise<SynchronizationUnitQuery[]> {
|
|
513
|
+
const listener = this.listenerStateByDriveId.get(driveId)?.get(listenerId);
|
|
398
514
|
if (!listener) {
|
|
399
|
-
return [];
|
|
515
|
+
return Promise.resolve([]);
|
|
400
516
|
}
|
|
401
517
|
const filter = listener.listener.filter;
|
|
402
|
-
return this.
|
|
518
|
+
return this.driveServer.getSynchronizationUnitsIds(
|
|
403
519
|
driveId,
|
|
404
520
|
filter.documentId ?? ["*"],
|
|
405
521
|
filter.scope ?? ["*"],
|
|
@@ -408,47 +524,23 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
408
524
|
);
|
|
409
525
|
}
|
|
410
526
|
|
|
411
|
-
async initDrive(drive: DocumentDriveDocument) {
|
|
412
|
-
const {
|
|
413
|
-
state: {
|
|
414
|
-
local: { listeners },
|
|
415
|
-
},
|
|
416
|
-
} = drive;
|
|
417
|
-
|
|
418
|
-
for (const listener of listeners) {
|
|
419
|
-
await this.addListener({
|
|
420
|
-
block: listener.block,
|
|
421
|
-
driveId: drive.state.global.id,
|
|
422
|
-
filter: {
|
|
423
|
-
branch: listener.filter.branch ?? [],
|
|
424
|
-
documentId: listener.filter.documentId ?? [],
|
|
425
|
-
documentType: listener.filter.documentType,
|
|
426
|
-
scope: listener.filter.scope ?? [],
|
|
427
|
-
},
|
|
428
|
-
listenerId: listener.listenerId,
|
|
429
|
-
system: listener.system,
|
|
430
|
-
callInfo: listener.callInfo ?? undefined,
|
|
431
|
-
label: listener.label ?? "",
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
527
|
async removeDrive(driveId: string): Promise<void> {
|
|
437
|
-
this.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
await Promise.all(
|
|
441
|
-
Object.values(transmitters).map((t) => t.disconnect?.()),
|
|
442
|
-
);
|
|
528
|
+
const listenerIdToListenerState = this.listenerStateByDriveId.get(driveId);
|
|
529
|
+
if (!listenerIdToListenerState) {
|
|
530
|
+
return;
|
|
443
531
|
}
|
|
444
|
-
}
|
|
445
532
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
533
|
+
// delete first
|
|
534
|
+
this.listenerStateByDriveId.delete(driveId);
|
|
535
|
+
|
|
536
|
+
for (const [_, listenerState] of listenerIdToListenerState) {
|
|
537
|
+
// guarantee that all disconnects are called
|
|
538
|
+
try {
|
|
539
|
+
await listenerState.listener.transmitter?.disconnect?.();
|
|
540
|
+
} catch (error) {
|
|
541
|
+
logger.error(error);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
452
544
|
}
|
|
453
545
|
|
|
454
546
|
async getStrands(
|
|
@@ -456,13 +548,13 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
456
548
|
listenerId: string,
|
|
457
549
|
options?: GetStrandsOptions,
|
|
458
550
|
): Promise<StrandUpdate[]> {
|
|
459
|
-
//
|
|
460
|
-
const
|
|
551
|
+
// this will throw if listenerState is not found
|
|
552
|
+
const listenerState = this.getListenerState(driveId, listenerId);
|
|
461
553
|
|
|
462
554
|
// fetch operations from drive and prepare strands
|
|
463
555
|
const strands: StrandUpdate[] = [];
|
|
464
556
|
|
|
465
|
-
const drive = await this.
|
|
557
|
+
const drive = await this.driveServer.getDrive(driveId);
|
|
466
558
|
const syncUnits = await this.getListenerSyncUnits(
|
|
467
559
|
driveId,
|
|
468
560
|
listenerId,
|
|
@@ -480,14 +572,14 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
480
572
|
if (syncUnit.revision < 0) {
|
|
481
573
|
return;
|
|
482
574
|
}
|
|
483
|
-
const entry =
|
|
575
|
+
const entry = listenerState.syncUnits.get(syncUnit.syncId);
|
|
484
576
|
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
485
577
|
return;
|
|
486
578
|
}
|
|
487
579
|
|
|
488
580
|
const { documentId, driveId, scope, branch } = syncUnit;
|
|
489
581
|
try {
|
|
490
|
-
const operations = await this.
|
|
582
|
+
const operations = await this.driveServer.getOperationData(
|
|
491
583
|
// DEAL WITH INVALID SYNC ID ERROR
|
|
492
584
|
driveId,
|
|
493
585
|
syncUnit.syncId,
|
|
@@ -528,4 +620,33 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
528
620
|
|
|
529
621
|
return strands;
|
|
530
622
|
}
|
|
623
|
+
|
|
624
|
+
getListenerState(driveId: string, listenerId: string) {
|
|
625
|
+
let listenerStateByListenerId = this.listenerStateByDriveId.get(driveId);
|
|
626
|
+
if (!listenerStateByListenerId) {
|
|
627
|
+
listenerStateByListenerId = new Map();
|
|
628
|
+
this.listenerStateByDriveId.set(driveId, listenerStateByListenerId);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const listenerState = listenerStateByListenerId.get(listenerId);
|
|
632
|
+
if (!listenerState) {
|
|
633
|
+
throw new Error("Listener not found");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return listenerState;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
setListenerState(
|
|
640
|
+
driveId: string,
|
|
641
|
+
listenerId: string,
|
|
642
|
+
listenerState: ListenerState,
|
|
643
|
+
) {
|
|
644
|
+
let listenerStateByListenerId = this.listenerStateByDriveId.get(driveId);
|
|
645
|
+
if (!listenerStateByListenerId) {
|
|
646
|
+
listenerStateByListenerId = new Map();
|
|
647
|
+
this.listenerStateByDriveId.set(driveId, listenerStateByListenerId);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
listenerStateByListenerId.set(listenerId, listenerState);
|
|
651
|
+
}
|
|
531
652
|
}
|