document-drive 1.0.0-alpha.90 → 1.0.0-alpha.92
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 +6 -4
- package/src/index.ts +1 -0
- package/src/queue/base.ts +5 -5
- package/src/server/error.ts +9 -0
- package/src/server/index.ts +68 -24
- package/src/server/listener/manager.ts +94 -75
- package/src/server/listener/transmitter/pull-responder.ts +5 -4
- package/src/server/types.ts +32 -8
- package/src/utils/index.ts +6 -0
- package/src/utils/run-asap.ts +159 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.92",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test:watch": "vitest watch"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"document-model": "^1.
|
|
38
|
+
"document-model": "^1.8.0",
|
|
39
39
|
"document-model-libs": "^1.57.0"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
@@ -65,9 +65,10 @@
|
|
|
65
65
|
"@types/uuid": "^9.0.8",
|
|
66
66
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
67
67
|
"@typescript-eslint/parser": "^6.21.0",
|
|
68
|
+
"@vitest/browser": "^2.0.5",
|
|
68
69
|
"@vitest/coverage-v8": "^2.0.5",
|
|
69
70
|
"document-model": "^1.7.0",
|
|
70
|
-
"document-model-libs": "^1.
|
|
71
|
+
"document-model-libs": "^1.83.0",
|
|
71
72
|
"eslint": "^8.57.0",
|
|
72
73
|
"eslint-config-prettier": "^9.1.0",
|
|
73
74
|
"fake-indexeddb": "^5.0.2",
|
|
@@ -80,7 +81,8 @@
|
|
|
80
81
|
"sequelize": "^6.37.2",
|
|
81
82
|
"sqlite3": "^5.1.7",
|
|
82
83
|
"typescript": "^5.5.3",
|
|
83
|
-
"vitest": "^2.0.5"
|
|
84
|
+
"vitest": "^2.0.5",
|
|
85
|
+
"webdriverio": "^9.0.9"
|
|
84
86
|
},
|
|
85
87
|
"packageManager": "pnpm@9.1.4+sha256.30a1801ac4e723779efed13a21f4c39f9eb6c9fbb4ced101bce06b422593d7c9"
|
|
86
88
|
}
|
package/src/index.ts
CHANGED
package/src/queue/base.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from 'document-model-libs/document-drive';
|
|
5
5
|
import { Action } from 'document-model/document';
|
|
6
6
|
import { Unsubscribe, createNanoEvents } from 'nanoevents';
|
|
7
|
-
import { generateUUID } from '../utils';
|
|
7
|
+
import { generateUUID, runAsap } from '../utils';
|
|
8
8
|
import { logger } from '../utils/logger';
|
|
9
9
|
import {
|
|
10
10
|
IJob,
|
|
@@ -212,10 +212,10 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
212
212
|
private retryNextJob(timeout?: number) {
|
|
213
213
|
const _timeout = timeout !== undefined ? timeout : this.timeout;
|
|
214
214
|
const retry =
|
|
215
|
-
_timeout
|
|
216
|
-
?
|
|
217
|
-
:
|
|
218
|
-
|
|
215
|
+
_timeout > 0
|
|
216
|
+
? (fn: () => void) => setTimeout(fn, _timeout)
|
|
217
|
+
: runAsap;
|
|
218
|
+
retry(() => this.processNextJob());
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
private async findFirstNonEmptyQueue(
|
package/src/server/error.ts
CHANGED
|
@@ -51,3 +51,12 @@ export class DriveNotFoundError extends Error {
|
|
|
51
51
|
this.driveId = driveId;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
export class SynchronizationUnitNotFoundError extends Error {
|
|
56
|
+
syncUnitId: string;
|
|
57
|
+
|
|
58
|
+
constructor(message: string, syncUnitId: string) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.syncUnitId = syncUnitId;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -41,7 +41,13 @@ import type {
|
|
|
41
41
|
DocumentStorage,
|
|
42
42
|
IDriveStorage
|
|
43
43
|
} from '../storage/types';
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
generateUUID,
|
|
46
|
+
isBefore,
|
|
47
|
+
isDocumentDrive,
|
|
48
|
+
RunAsap,
|
|
49
|
+
runAsapAsync
|
|
50
|
+
} from '../utils';
|
|
45
51
|
import { DefaultDrivesManager } from '../utils/default-drives-manager';
|
|
46
52
|
import {
|
|
47
53
|
attachBranch,
|
|
@@ -58,7 +64,8 @@ import { logger } from '../utils/logger';
|
|
|
58
64
|
import {
|
|
59
65
|
ConflictOperationError,
|
|
60
66
|
DriveAlreadyExistsError,
|
|
61
|
-
OperationError
|
|
67
|
+
OperationError,
|
|
68
|
+
SynchronizationUnitNotFoundError
|
|
62
69
|
} from './error';
|
|
63
70
|
import { ListenerManager } from './listener/manager';
|
|
64
71
|
import {
|
|
@@ -72,9 +79,11 @@ import {
|
|
|
72
79
|
import {
|
|
73
80
|
AddOperationOptions,
|
|
74
81
|
BaseDocumentDriveServer,
|
|
82
|
+
DefaultListenerManagerOptions,
|
|
75
83
|
DocumentDriveServerOptions,
|
|
76
84
|
DriveEvents,
|
|
77
85
|
GetDocumentOptions,
|
|
86
|
+
GetStrandsOptions,
|
|
78
87
|
IOperationResult,
|
|
79
88
|
ListenerState,
|
|
80
89
|
RemoteDriveOptions,
|
|
@@ -111,6 +120,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
111
120
|
|
|
112
121
|
private defaultDrivesManager: DefaultDrivesManager;
|
|
113
122
|
|
|
123
|
+
protected options: Required<DocumentDriveServerOptions>;
|
|
124
|
+
|
|
114
125
|
constructor(
|
|
115
126
|
documentModels: DocumentModel[],
|
|
116
127
|
storage: IDriveStorage = new MemoryStorage(),
|
|
@@ -119,7 +130,27 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
119
130
|
options?: DocumentDriveServerOptions
|
|
120
131
|
) {
|
|
121
132
|
super();
|
|
122
|
-
this.
|
|
133
|
+
this.options = {
|
|
134
|
+
defaultRemoteDrives: [],
|
|
135
|
+
removeOldRemoteDrives: {
|
|
136
|
+
strategy: 'preserve-all'
|
|
137
|
+
},
|
|
138
|
+
...options,
|
|
139
|
+
listenerManager: {
|
|
140
|
+
...DefaultListenerManagerOptions,
|
|
141
|
+
...options?.listenerManager
|
|
142
|
+
},
|
|
143
|
+
taskQueueMethod:
|
|
144
|
+
options?.taskQueueMethod === undefined
|
|
145
|
+
? RunAsap.runAsap
|
|
146
|
+
: options.taskQueueMethod
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
this.listenerStateManager = new ListenerManager(
|
|
150
|
+
this,
|
|
151
|
+
undefined,
|
|
152
|
+
options?.listenerManager
|
|
153
|
+
);
|
|
123
154
|
this.documentModels = documentModels;
|
|
124
155
|
this.storage = storage;
|
|
125
156
|
this.cache = cache;
|
|
@@ -559,6 +590,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
559
590
|
}
|
|
560
591
|
|
|
561
592
|
private async _initialize() {
|
|
593
|
+
await this.queueManager.init(this.queueDelegate, error => {
|
|
594
|
+
logger.error(`Error initializing queue manager`, error);
|
|
595
|
+
errors.push(error);
|
|
596
|
+
});
|
|
597
|
+
|
|
562
598
|
try {
|
|
563
599
|
await this.defaultDrivesManager.removeOldremoteDrives();
|
|
564
600
|
} catch (error) {
|
|
@@ -574,11 +610,6 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
574
610
|
});
|
|
575
611
|
}
|
|
576
612
|
|
|
577
|
-
await this.queueManager.init(this.queueDelegate, error => {
|
|
578
|
-
logger.error(`Error initializing queue manager`, error);
|
|
579
|
-
errors.push(error);
|
|
580
|
-
});
|
|
581
|
-
|
|
582
613
|
await this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
583
614
|
|
|
584
615
|
// if network connect comes back online
|
|
@@ -819,10 +850,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
819
850
|
async getOperationData(
|
|
820
851
|
driveId: string,
|
|
821
852
|
syncId: string,
|
|
822
|
-
filter:
|
|
823
|
-
since?: string | undefined;
|
|
824
|
-
fromRevision?: number | undefined;
|
|
825
|
-
},
|
|
853
|
+
filter: GetStrandsOptions,
|
|
826
854
|
loadedDrive?: DocumentDriveDocument
|
|
827
855
|
): Promise<OperationUpdate[]> {
|
|
828
856
|
const syncUnit =
|
|
@@ -845,6 +873,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
845
873
|
|
|
846
874
|
const operations =
|
|
847
875
|
document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
|
|
876
|
+
|
|
848
877
|
const filteredOperations = operations.filter(
|
|
849
878
|
operation =>
|
|
850
879
|
Object.keys(filter).length === 0 ||
|
|
@@ -854,7 +883,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
854
883
|
operation.index > filter.fromRevision))
|
|
855
884
|
);
|
|
856
885
|
|
|
857
|
-
|
|
886
|
+
const limitedOperations = filter.limit
|
|
887
|
+
? filteredOperations.slice(0, filter.limit)
|
|
888
|
+
: filteredOperations;
|
|
889
|
+
|
|
890
|
+
return limitedOperations.map(operation => ({
|
|
858
891
|
hash: operation.hash,
|
|
859
892
|
index: operation.index,
|
|
860
893
|
timestamp: operation.timestamp,
|
|
@@ -1135,6 +1168,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1135
1168
|
) {
|
|
1136
1169
|
const operationsApplied: Operation<A | BaseAction>[] = [];
|
|
1137
1170
|
const signals: SignalResult[] = [];
|
|
1171
|
+
|
|
1138
1172
|
const documentStorageWithState = await this._addDocumentResultingStage(
|
|
1139
1173
|
documentStorage,
|
|
1140
1174
|
drive,
|
|
@@ -1192,13 +1226,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1192
1226
|
}
|
|
1193
1227
|
|
|
1194
1228
|
try {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1229
|
+
// runs operation on next available tick, to avoid blocking the main thread
|
|
1230
|
+
const taskQueueMethod = this.options.taskQueueMethod;
|
|
1231
|
+
const task = () =>
|
|
1232
|
+
this._performOperation(
|
|
1233
|
+
drive,
|
|
1234
|
+
documentId,
|
|
1235
|
+
document,
|
|
1236
|
+
nextOperation,
|
|
1237
|
+
skipHashValidation
|
|
1238
|
+
);
|
|
1239
|
+
const appliedResult = await (taskQueueMethod
|
|
1240
|
+
? runAsapAsync(task, taskQueueMethod)
|
|
1241
|
+
: task());
|
|
1202
1242
|
document = appliedResult.document;
|
|
1203
1243
|
signals.push(...appliedResult.signals);
|
|
1204
1244
|
operationsApplied.push(appliedResult.operation);
|
|
@@ -2304,11 +2344,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
2304
2344
|
return this.listenerStateManager.getListener(driveId, listenerId);
|
|
2305
2345
|
}
|
|
2306
2346
|
|
|
2307
|
-
getSyncStatus(
|
|
2308
|
-
|
|
2347
|
+
getSyncStatus(
|
|
2348
|
+
syncUnitId: string
|
|
2349
|
+
): SyncStatus | SynchronizationUnitNotFoundError {
|
|
2350
|
+
const status = this.syncStatus.get(syncUnitId);
|
|
2309
2351
|
if (!status) {
|
|
2310
|
-
|
|
2311
|
-
|
|
2352
|
+
return new SynchronizationUnitNotFoundError(
|
|
2353
|
+
`Sync status not found for syncUnitId: ${syncUnitId}`,
|
|
2354
|
+
syncUnitId
|
|
2355
|
+
);
|
|
2312
2356
|
}
|
|
2313
2357
|
return this.getCombinedSyncUnitStatus(status);
|
|
2314
2358
|
}
|
|
@@ -8,6 +8,7 @@ import { OperationError } from '../error';
|
|
|
8
8
|
import {
|
|
9
9
|
BaseListenerManager,
|
|
10
10
|
ErrorStatus,
|
|
11
|
+
GetStrandsOptions,
|
|
11
12
|
Listener,
|
|
12
13
|
ListenerState,
|
|
13
14
|
ListenerUpdate,
|
|
@@ -45,7 +46,6 @@ function debounce<T extends unknown[], R>(
|
|
|
45
46
|
});
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
|
-
|
|
49
49
|
export class ListenerManager extends BaseListenerManager {
|
|
50
50
|
static LISTENER_UPDATE_DELAY = 250;
|
|
51
51
|
|
|
@@ -252,49 +252,51 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
252
252
|
);
|
|
253
253
|
|
|
254
254
|
const strandUpdates: StrandUpdate[] = [];
|
|
255
|
-
|
|
256
255
|
// TODO change to push one after the other, reusing operation data
|
|
257
|
-
|
|
258
|
-
syncUnits.
|
|
259
|
-
const unitState = listener.syncUnits.get(
|
|
260
|
-
syncUnit.syncId
|
|
261
|
-
);
|
|
256
|
+
const tasks = syncUnits.map(syncUnit => async () => {
|
|
257
|
+
const unitState = listener.syncUnits.get(syncUnit.syncId);
|
|
262
258
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
259
|
+
if (
|
|
260
|
+
unitState &&
|
|
261
|
+
unitState.listenerRev >= syncUnit.revision
|
|
262
|
+
) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
269
265
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
266
|
+
const opData: OperationUpdate[] = [];
|
|
267
|
+
try {
|
|
268
|
+
const data = await this.drive.getOperationData(
|
|
269
|
+
// TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
|
|
270
|
+
driveId,
|
|
271
|
+
syncUnit.syncId,
|
|
272
|
+
{
|
|
273
|
+
fromRevision: unitState?.listenerRev
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
opData.push(...data);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
logger.error(e);
|
|
279
|
+
}
|
|
284
280
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
281
|
+
if (!opData.length) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
288
284
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
285
|
+
strandUpdates.push({
|
|
286
|
+
driveId,
|
|
287
|
+
documentId: syncUnit.documentId,
|
|
288
|
+
branch: syncUnit.branch,
|
|
289
|
+
operations: opData,
|
|
290
|
+
scope: syncUnit.scope as OperationScope
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
if (this.options.sequentialUpdates) {
|
|
294
|
+
for (const task of tasks) {
|
|
295
|
+
await task();
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
await Promise.all(tasks.map(task => task()));
|
|
299
|
+
}
|
|
298
300
|
|
|
299
301
|
if (strandUpdates.length == 0) {
|
|
300
302
|
continue;
|
|
@@ -478,7 +480,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
478
480
|
async getStrands(
|
|
479
481
|
driveId: string,
|
|
480
482
|
listenerId: string,
|
|
481
|
-
|
|
483
|
+
options?: GetStrandsOptions
|
|
482
484
|
): Promise<StrandUpdate[]> {
|
|
483
485
|
// fetch listenerState from listenerManager
|
|
484
486
|
const listener = await this.getListener(driveId, listenerId);
|
|
@@ -493,46 +495,63 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
493
495
|
drive
|
|
494
496
|
);
|
|
495
497
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (syncUnit.revision < 0) {
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
const entry = listener.syncUnits.get(syncUnit.syncId);
|
|
502
|
-
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
498
|
+
const limit = options?.limit; // maximum number of operations to send across all sync units
|
|
499
|
+
let operationsCount = 0; // total amount of operations that have been retrieved
|
|
505
500
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
501
|
+
const tasks = syncUnits.map(syncUnit => async () => {
|
|
502
|
+
if (limit && operationsCount >= limit) {
|
|
503
|
+
// break;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (syncUnit.revision < 0) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const entry = listener.syncUnits.get(syncUnit.syncId);
|
|
510
|
+
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
518
513
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
514
|
+
const { documentId, driveId, scope, branch } = syncUnit;
|
|
515
|
+
try {
|
|
516
|
+
const operations = await this.drive.getOperationData(
|
|
517
|
+
// DEAL WITH INVALID SYNC ID ERROR
|
|
518
|
+
driveId,
|
|
519
|
+
syncUnit.syncId,
|
|
520
|
+
{
|
|
521
|
+
since: options?.since,
|
|
522
|
+
fromRevision:
|
|
523
|
+
options?.fromRevision ?? entry?.listenerRev,
|
|
524
|
+
limit: limit ? limit - operationsCount : undefined
|
|
525
|
+
},
|
|
526
|
+
drive
|
|
527
|
+
);
|
|
522
528
|
|
|
523
|
-
|
|
524
|
-
driveId,
|
|
525
|
-
documentId,
|
|
526
|
-
scope: scope as OperationScope,
|
|
527
|
-
branch,
|
|
528
|
-
operations
|
|
529
|
-
});
|
|
530
|
-
} catch (error) {
|
|
531
|
-
logger.error(error);
|
|
529
|
+
if (!operations.length) {
|
|
532
530
|
return;
|
|
533
531
|
}
|
|
534
|
-
|
|
535
|
-
|
|
532
|
+
|
|
533
|
+
operationsCount += operations.length;
|
|
534
|
+
|
|
535
|
+
strands.push({
|
|
536
|
+
driveId,
|
|
537
|
+
documentId,
|
|
538
|
+
scope: scope as OperationScope,
|
|
539
|
+
branch,
|
|
540
|
+
operations
|
|
541
|
+
});
|
|
542
|
+
} catch (error) {
|
|
543
|
+
logger.error(error);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
if (this.options.sequentialUpdates) {
|
|
549
|
+
for (const task of tasks) {
|
|
550
|
+
await task();
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
await Promise.all(tasks.map(task => task()));
|
|
554
|
+
}
|
|
536
555
|
|
|
537
556
|
return strands;
|
|
538
557
|
}
|
|
@@ -7,6 +7,7 @@ import { logger as defaultLogger } from '../../../utils/logger';
|
|
|
7
7
|
import { OperationError } from '../../error';
|
|
8
8
|
import {
|
|
9
9
|
BaseDocumentDriveServer,
|
|
10
|
+
GetStrandsOptions,
|
|
10
11
|
IOperationResult,
|
|
11
12
|
Listener,
|
|
12
13
|
ListenerRevision,
|
|
@@ -41,7 +42,7 @@ export type StrandUpdateGraphQL = Omit<StrandUpdate, 'operations'> & {
|
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
export interface IPullResponderTransmitter extends ITransmitter {
|
|
44
|
-
getStrands(
|
|
45
|
+
getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]>;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
@@ -59,11 +60,11 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
59
60
|
this.manager = manager;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
getStrands(
|
|
63
|
+
getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]> {
|
|
63
64
|
return this.manager.getStrands(
|
|
64
65
|
this.listener.driveId,
|
|
65
66
|
this.listener.listenerId,
|
|
66
|
-
|
|
67
|
+
options
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -135,7 +136,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
135
136
|
driveId: string,
|
|
136
137
|
url: string,
|
|
137
138
|
listenerId: string,
|
|
138
|
-
|
|
139
|
+
options?: GetStrandsOptions // TODO add support for since
|
|
139
140
|
): Promise<StrandUpdate[]> {
|
|
140
141
|
const {
|
|
141
142
|
system: {
|
package/src/server/types.ts
CHANGED
|
@@ -20,8 +20,9 @@ import type {
|
|
|
20
20
|
State
|
|
21
21
|
} from 'document-model/document';
|
|
22
22
|
import { Unsubscribe } from 'nanoevents';
|
|
23
|
+
import { RunAsap } from '../utils';
|
|
23
24
|
import { DriveInfo } from '../utils/graphql';
|
|
24
|
-
import { OperationError } from './error';
|
|
25
|
+
import { OperationError, SynchronizationUnitNotFoundError } from './error';
|
|
25
26
|
import {
|
|
26
27
|
ITransmitter,
|
|
27
28
|
PullResponderTrigger,
|
|
@@ -235,6 +236,18 @@ export type RemoveOldRemoteDrivesOption =
|
|
|
235
236
|
export type DocumentDriveServerOptions = {
|
|
236
237
|
defaultRemoteDrives?: Array<DefaultRemoteDriveInput>;
|
|
237
238
|
removeOldRemoteDrives?: RemoveOldRemoteDrivesOption;
|
|
239
|
+
/* method to queue heavy tasks that might block the event loop.
|
|
240
|
+
* If set to null then it will queued as micro task.
|
|
241
|
+
* Defaults to the most appropriate method according to the system
|
|
242
|
+
*/
|
|
243
|
+
taskQueueMethod?: RunAsap.RunAsap<unknown> | null;
|
|
244
|
+
listenerManager?: ListenerManagerOptions;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export type GetStrandsOptions = {
|
|
248
|
+
limit?: number;
|
|
249
|
+
since?: string;
|
|
250
|
+
fromRevision?: number;
|
|
238
251
|
};
|
|
239
252
|
|
|
240
253
|
export abstract class BaseDocumentDriveServer {
|
|
@@ -361,7 +374,9 @@ export abstract class BaseDocumentDriveServer {
|
|
|
361
374
|
options?: AddOperationOptions
|
|
362
375
|
): Promise<IOperationResult<DocumentDriveDocument>>;
|
|
363
376
|
|
|
364
|
-
abstract getSyncStatus(
|
|
377
|
+
abstract getSyncStatus(
|
|
378
|
+
syncUnitId: string
|
|
379
|
+
): SyncStatus | SynchronizationUnitNotFoundError;
|
|
365
380
|
|
|
366
381
|
/** Synchronization methods */
|
|
367
382
|
abstract getSynchronizationUnits(
|
|
@@ -390,10 +405,7 @@ export abstract class BaseDocumentDriveServer {
|
|
|
390
405
|
abstract getOperationData(
|
|
391
406
|
driveId: string,
|
|
392
407
|
syncId: string,
|
|
393
|
-
filter:
|
|
394
|
-
since?: string;
|
|
395
|
-
fromRevision?: number;
|
|
396
|
-
},
|
|
408
|
+
filter: GetStrandsOptions,
|
|
397
409
|
loadedDrive?: DocumentDriveDocument
|
|
398
410
|
): Promise<OperationUpdate[]>;
|
|
399
411
|
|
|
@@ -430,8 +442,17 @@ export abstract class BaseDocumentDriveServer {
|
|
|
430
442
|
): Promise<PullResponderTrigger>;
|
|
431
443
|
}
|
|
432
444
|
|
|
445
|
+
export type ListenerManagerOptions = {
|
|
446
|
+
sequentialUpdates?: boolean;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
export const DefaultListenerManagerOptions = {
|
|
450
|
+
sequentialUpdates: true
|
|
451
|
+
};
|
|
452
|
+
|
|
433
453
|
export abstract class BaseListenerManager {
|
|
434
454
|
protected drive: BaseDocumentDriveServer;
|
|
455
|
+
protected options: ListenerManagerOptions;
|
|
435
456
|
protected listenerState = new Map<string, Map<string, ListenerState>>();
|
|
436
457
|
protected transmitters: Record<
|
|
437
458
|
DocumentDriveState['id'],
|
|
@@ -440,10 +461,12 @@ export abstract class BaseListenerManager {
|
|
|
440
461
|
|
|
441
462
|
constructor(
|
|
442
463
|
drive: BaseDocumentDriveServer,
|
|
443
|
-
listenerState = new Map<string, Map<string, ListenerState>>()
|
|
464
|
+
listenerState = new Map<string, Map<string, ListenerState>>(),
|
|
465
|
+
options: ListenerManagerOptions = DefaultListenerManagerOptions
|
|
444
466
|
) {
|
|
445
467
|
this.drive = drive;
|
|
446
468
|
this.listenerState = listenerState;
|
|
469
|
+
this.options = { ...DefaultListenerManagerOptions, ...options };
|
|
447
470
|
}
|
|
448
471
|
|
|
449
472
|
abstract initDrive(drive: DocumentDriveDocument): Promise<void>;
|
|
@@ -466,8 +489,9 @@ export abstract class BaseListenerManager {
|
|
|
466
489
|
): Promise<ITransmitter | undefined>;
|
|
467
490
|
|
|
468
491
|
abstract getStrands(
|
|
492
|
+
driveId: string,
|
|
469
493
|
listenerId: string,
|
|
470
|
-
|
|
494
|
+
options?: GetStrandsOptions
|
|
471
495
|
): Promise<StrandUpdate[]>;
|
|
472
496
|
|
|
473
497
|
abstract updateSynchronizationRevisions(
|
package/src/utils/index.ts
CHANGED
|
@@ -10,9 +10,15 @@ import {
|
|
|
10
10
|
Operation,
|
|
11
11
|
OperationScope
|
|
12
12
|
} from 'document-model/document';
|
|
13
|
+
// import setAsap from 'setasap';
|
|
13
14
|
import { v4 as uuidv4 } from 'uuid';
|
|
14
15
|
import { OperationError } from '../server/error';
|
|
15
16
|
import { DocumentDriveStorage, DocumentStorage } from '../storage';
|
|
17
|
+
import { RunAsap } from './run-asap';
|
|
18
|
+
export * from './run-asap';
|
|
19
|
+
|
|
20
|
+
export const runAsap = RunAsap.runAsap;
|
|
21
|
+
export const runAsapAsync = RunAsap.runAsapAsync;
|
|
16
22
|
|
|
17
23
|
export function isDocumentDriveStorage(
|
|
18
24
|
document: DocumentStorage
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
2
|
+
export namespace RunAsap {
|
|
3
|
+
export type Task<T = void> = () => T;
|
|
4
|
+
export type AbortTask = () => void;
|
|
5
|
+
export type RunAsap<T> = (task: Task<T>) => AbortTask;
|
|
6
|
+
|
|
7
|
+
export const useMessageChannel = (() => {
|
|
8
|
+
if (typeof MessageChannel === 'undefined') {
|
|
9
|
+
return new Error('MessageChannel is not supported');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (task: Task) => {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const signal = controller.signal;
|
|
15
|
+
const mc = new MessageChannel();
|
|
16
|
+
mc.port1.postMessage(null);
|
|
17
|
+
mc.port2.addEventListener(
|
|
18
|
+
'message',
|
|
19
|
+
() => {
|
|
20
|
+
task();
|
|
21
|
+
mc.port1.close();
|
|
22
|
+
mc.port2.close();
|
|
23
|
+
},
|
|
24
|
+
{ once: true, signal: signal }
|
|
25
|
+
);
|
|
26
|
+
mc.port2.start();
|
|
27
|
+
return () => controller.abort();
|
|
28
|
+
};
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
export const usePostMessage = (() => {
|
|
32
|
+
const _main: unknown =
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
34
|
+
(typeof window === 'object' && window) ||
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
36
|
+
(typeof global === 'object' && global) ||
|
|
37
|
+
(typeof self === 'object' && self);
|
|
38
|
+
if (!_main) {
|
|
39
|
+
return new Error('No global object found');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const main = _main as Window;
|
|
43
|
+
if (
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
45
|
+
!main.postMessage ||
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
47
|
+
!main.addEventListener ||
|
|
48
|
+
(main as { importScripts?: unknown }).importScripts // web workers can't this method
|
|
49
|
+
) {
|
|
50
|
+
return new Error('postMessage is not supported');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let index = 0;
|
|
54
|
+
const tasks = new Map<number, Task>();
|
|
55
|
+
|
|
56
|
+
function getNewIndex() {
|
|
57
|
+
if (index === 9007199254740991) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
return ++index;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MESSAGE_PREFIX = 'com.usePostMessage' + Math.random();
|
|
64
|
+
|
|
65
|
+
main.addEventListener(
|
|
66
|
+
'message',
|
|
67
|
+
e => {
|
|
68
|
+
const event = e as MessageEvent<string>;
|
|
69
|
+
if (typeof event.data !== 'string') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
event.source !== main ||
|
|
74
|
+
!event.data.startsWith(MESSAGE_PREFIX)
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const index = event.data.split(':').at(1);
|
|
79
|
+
if (index === undefined) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const i = +index;
|
|
83
|
+
const task = tasks.get(i);
|
|
84
|
+
if (task) {
|
|
85
|
+
task();
|
|
86
|
+
tasks.delete(i);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
false
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (task: Task) => {
|
|
93
|
+
const i = getNewIndex();
|
|
94
|
+
tasks.set(i, task);
|
|
95
|
+
main.postMessage(MESSAGE_PREFIX + ':' + i, { targetOrigin: '*' });
|
|
96
|
+
return () => {
|
|
97
|
+
tasks.delete(i);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
export const useSetImmediate = (() => {
|
|
103
|
+
if (typeof window !== 'undefined') {
|
|
104
|
+
return new Error('setImmediate is not supported on the browser');
|
|
105
|
+
}
|
|
106
|
+
if (typeof setImmediate === 'undefined') {
|
|
107
|
+
return new Error('setImmediate is not supported');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (task: Task) => {
|
|
111
|
+
const id = setImmediate(task);
|
|
112
|
+
return () => clearImmediate(id);
|
|
113
|
+
};
|
|
114
|
+
})();
|
|
115
|
+
|
|
116
|
+
export const useSetTimeout = (() => {
|
|
117
|
+
return (task: Task) => {
|
|
118
|
+
const id = setTimeout(task, 0);
|
|
119
|
+
return () => clearTimeout(id);
|
|
120
|
+
};
|
|
121
|
+
})();
|
|
122
|
+
|
|
123
|
+
// queues the task in the macro tasks queue, so it doesn't
|
|
124
|
+
// prevent the event loop from movin on the next tick
|
|
125
|
+
export function runAsap<T = void>(task: Task<T>): AbortTask {
|
|
126
|
+
// if on node use setImmediate
|
|
127
|
+
if (!(useSetImmediate instanceof Error)) {
|
|
128
|
+
return useSetImmediate(task);
|
|
129
|
+
}
|
|
130
|
+
// on browser use MessageChannel if available
|
|
131
|
+
else if (!(useMessageChannel instanceof Error)) {
|
|
132
|
+
return useMessageChannel(task);
|
|
133
|
+
}
|
|
134
|
+
// otherwise use window.postMessage
|
|
135
|
+
else if (!(usePostMessage instanceof Error)) {
|
|
136
|
+
return usePostMessage(task);
|
|
137
|
+
}
|
|
138
|
+
// fallback to setTimeout with 0 delay
|
|
139
|
+
else {
|
|
140
|
+
return useSetTimeout(task);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function runAsapAsync<T = void>(
|
|
145
|
+
task: RunAsap.Task<Promise<T>>,
|
|
146
|
+
queueMethod: RunAsap<void> = runAsap
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
if (queueMethod instanceof Error) {
|
|
149
|
+
throw new Error('queueMethod is not supported', {
|
|
150
|
+
cause: queueMethod
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
queueMethod(() => {
|
|
155
|
+
task().then(resolve).catch(reject);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|