document-drive 1.0.0-alpha.71 → 1.0.0-alpha.73
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 +1 -1
- package/src/server/index.ts +190 -51
- package/src/server/listener/manager.ts +41 -18
- package/src/server/listener/transmitter/pull-responder.ts +1 -1
- package/src/server/types.ts +11 -1
- package/src/storage/browser.ts +69 -5
- package/src/storage/filesystem.ts +60 -2
- package/src/storage/memory.ts +67 -4
- package/src/storage/prisma.ts +34 -0
- package/src/storage/sequelize.ts +58 -0
- package/src/storage/types.ts +10 -1
package/package.json
CHANGED
package/src/server/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
ListenerState,
|
|
62
62
|
RemoteDriveOptions,
|
|
63
63
|
StrandUpdate,
|
|
64
|
+
SynchronizationUnitQuery,
|
|
64
65
|
SyncStatus,
|
|
65
66
|
type CreateDocumentInput,
|
|
66
67
|
type DriveInput,
|
|
@@ -87,7 +88,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
87
88
|
DocumentDriveState['id'],
|
|
88
89
|
Map<Trigger['id'], CancelPullLoop>
|
|
89
90
|
>();
|
|
90
|
-
private syncStatus = new Map<
|
|
91
|
+
private syncStatus = new Map<string, SyncStatus>();
|
|
91
92
|
|
|
92
93
|
private queueManager: IQueueManager;
|
|
93
94
|
|
|
@@ -186,6 +187,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
186
187
|
const drive = await this.getDrive(driveId);
|
|
187
188
|
let driveTriggers = this.triggerMap.get(driveId);
|
|
188
189
|
|
|
190
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
191
|
+
|
|
189
192
|
for (const trigger of drive.state.local.triggers) {
|
|
190
193
|
if (driveTriggers?.get(trigger.id)) {
|
|
191
194
|
continue;
|
|
@@ -196,6 +199,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
this.updateSyncStatus(driveId, 'SYNCING');
|
|
202
|
+
|
|
203
|
+
for (const syncUnit of syncUnits) {
|
|
204
|
+
this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
|
|
205
|
+
}
|
|
206
|
+
|
|
199
207
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
200
208
|
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
201
209
|
driveId,
|
|
@@ -211,12 +219,31 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
211
219
|
);
|
|
212
220
|
},
|
|
213
221
|
revisions => {
|
|
214
|
-
const errorRevision = revisions.
|
|
222
|
+
const errorRevision = revisions.filter(
|
|
215
223
|
r => r.status !== 'SUCCESS'
|
|
216
224
|
);
|
|
217
|
-
if (
|
|
225
|
+
if (errorRevision.length < 1) {
|
|
218
226
|
this.updateSyncStatus(driveId, 'SUCCESS');
|
|
219
227
|
}
|
|
228
|
+
|
|
229
|
+
for (const syncUnit of syncUnits) {
|
|
230
|
+
const fileErrorRevision = errorRevision.find(
|
|
231
|
+
r => r.documentId === syncUnit.documentId
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (fileErrorRevision) {
|
|
235
|
+
this.updateSyncStatus(
|
|
236
|
+
syncUnit.syncId,
|
|
237
|
+
fileErrorRevision.status,
|
|
238
|
+
fileErrorRevision.error
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
this.updateSyncStatus(
|
|
242
|
+
syncUnit.syncId,
|
|
243
|
+
'SUCCESS'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
220
247
|
}
|
|
221
248
|
);
|
|
222
249
|
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
@@ -226,9 +253,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
226
253
|
}
|
|
227
254
|
|
|
228
255
|
private async stopSyncRemoteDrive(driveId: string) {
|
|
256
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
257
|
+
const fileNodes = syncUnits
|
|
258
|
+
.filter(syncUnit => syncUnit.documentId !== '')
|
|
259
|
+
.map(syncUnit => syncUnit.documentId);
|
|
260
|
+
|
|
229
261
|
const triggers = this.triggerMap.get(driveId);
|
|
230
262
|
triggers?.forEach(cancel => cancel());
|
|
231
263
|
this.updateSyncStatus(driveId, null);
|
|
264
|
+
|
|
265
|
+
for (const fileNode of fileNodes) {
|
|
266
|
+
this.updateSyncStatus(fileNode, null);
|
|
267
|
+
}
|
|
232
268
|
return this.triggerMap.delete(driveId);
|
|
233
269
|
}
|
|
234
270
|
|
|
@@ -303,6 +339,34 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
303
339
|
) {
|
|
304
340
|
const drive = await this.getDrive(driveId);
|
|
305
341
|
|
|
342
|
+
const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType, drive);
|
|
343
|
+
const revisions = await this.storage.getSynchronizationUnitsRevision(synchronizationUnitsQuery);
|
|
344
|
+
|
|
345
|
+
const synchronizationUnits: SynchronizationUnit[] = synchronizationUnitsQuery.map(s => ({ ...s, lastUpdated: drive.created, revision: -1 }));
|
|
346
|
+
for (const revision of revisions) {
|
|
347
|
+
const syncUnit = synchronizationUnits.find(s =>
|
|
348
|
+
revision.driveId === s.driveId &&
|
|
349
|
+
revision.documentId === s.documentId &&
|
|
350
|
+
revision.scope === s.scope &&
|
|
351
|
+
revision.branch === s.branch
|
|
352
|
+
);
|
|
353
|
+
if (syncUnit) {
|
|
354
|
+
syncUnit.revision = revision.revision;
|
|
355
|
+
syncUnit.lastUpdated = revision.lastUpdated
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return synchronizationUnits;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public async getSynchronizationUnitsIds(
|
|
362
|
+
driveId: string,
|
|
363
|
+
documentId?: string[],
|
|
364
|
+
scope?: string[],
|
|
365
|
+
branch?: string[],
|
|
366
|
+
documentType?: string[],
|
|
367
|
+
loadedDrive?: DocumentDriveDocument
|
|
368
|
+
): Promise<SynchronizationUnitQuery[]> {
|
|
369
|
+
const drive = loadedDrive ?? await this.getDrive(driveId);
|
|
306
370
|
const nodes = drive.state.global.nodes.filter(
|
|
307
371
|
node =>
|
|
308
372
|
isFileNode(node) &&
|
|
@@ -336,8 +400,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
336
400
|
});
|
|
337
401
|
}
|
|
338
402
|
|
|
339
|
-
const
|
|
340
|
-
|
|
403
|
+
const synchronizationUnitsQuery: Omit<SynchronizationUnit, "revision" | "lastUpdated">[] = [];
|
|
341
404
|
for (const node of nodes) {
|
|
342
405
|
const nodeUnits =
|
|
343
406
|
scope?.length || branch?.length
|
|
@@ -354,35 +417,22 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
354
417
|
if (!nodeUnits.length) {
|
|
355
418
|
continue;
|
|
356
419
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const lastOperation = operations[operations.length - 1];
|
|
366
|
-
synchronizationUnits.push({
|
|
367
|
-
syncId,
|
|
368
|
-
scope,
|
|
369
|
-
branch,
|
|
370
|
-
driveId,
|
|
371
|
-
documentId: node.id,
|
|
372
|
-
documentType: node.documentType,
|
|
373
|
-
lastUpdated:
|
|
374
|
-
lastOperation?.timestamp ?? document.lastModified,
|
|
375
|
-
revision: lastOperation?.index ?? 0
|
|
376
|
-
});
|
|
377
|
-
}
|
|
420
|
+
synchronizationUnitsQuery.push(...nodeUnits.map(n => ({
|
|
421
|
+
driveId,
|
|
422
|
+
documentId: node.id,
|
|
423
|
+
syncId: n.syncId,
|
|
424
|
+
documentType: node.documentType,
|
|
425
|
+
scope: n.scope,
|
|
426
|
+
branch: n.branch
|
|
427
|
+
})));
|
|
378
428
|
}
|
|
379
|
-
return
|
|
429
|
+
return synchronizationUnitsQuery;
|
|
380
430
|
}
|
|
381
431
|
|
|
382
|
-
public async
|
|
432
|
+
public async getSynchronizationUnitIdInfo(
|
|
383
433
|
driveId: string,
|
|
384
434
|
syncId: string
|
|
385
|
-
): Promise<SynchronizationUnit> {
|
|
435
|
+
): Promise<Omit<SynchronizationUnit, "revision" | "lastUpdated"> | undefined> {
|
|
386
436
|
const drive = await this.getDrive(driveId);
|
|
387
437
|
const node = drive.state.global.nodes.find(
|
|
388
438
|
node =>
|
|
@@ -391,14 +441,40 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
391
441
|
);
|
|
392
442
|
|
|
393
443
|
if (!node || !isFileNode(node)) {
|
|
394
|
-
|
|
444
|
+
return undefined;
|
|
395
445
|
}
|
|
396
446
|
|
|
397
|
-
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
448
|
+
const syncUnit = node.synchronizationUnits.find(
|
|
398
449
|
unit => unit.syncId === syncId
|
|
399
|
-
)
|
|
450
|
+
);
|
|
451
|
+
if (!syncUnit) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
syncId,
|
|
457
|
+
scope: syncUnit.scope,
|
|
458
|
+
branch: syncUnit.branch,
|
|
459
|
+
driveId,
|
|
460
|
+
documentId: node.id,
|
|
461
|
+
documentType: node.documentType,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
400
464
|
|
|
401
|
-
|
|
465
|
+
public async getSynchronizationUnit(
|
|
466
|
+
driveId: string,
|
|
467
|
+
syncId: string
|
|
468
|
+
): Promise<SynchronizationUnit | undefined> {
|
|
469
|
+
const syncUnit = await this.getSynchronizationUnitIdInfo(driveId, syncId);
|
|
470
|
+
|
|
471
|
+
if (!syncUnit) {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const { scope, branch, documentId, documentType } = syncUnit;
|
|
476
|
+
|
|
477
|
+
// TODO: REPLACE WITH GET DOCUMENT OPERATIONS
|
|
402
478
|
const document = await this.getDocument(driveId, documentId);
|
|
403
479
|
const operations = document.operations[scope as OperationScope] ?? [];
|
|
404
480
|
const lastOperation = operations[operations.length - 1];
|
|
@@ -409,7 +485,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
409
485
|
branch,
|
|
410
486
|
driveId,
|
|
411
487
|
documentId,
|
|
412
|
-
documentType
|
|
488
|
+
documentType,
|
|
413
489
|
lastUpdated: lastOperation?.timestamp ?? document.lastModified,
|
|
414
490
|
revision: lastOperation?.index ?? 0
|
|
415
491
|
};
|
|
@@ -423,17 +499,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
423
499
|
fromRevision?: number | undefined;
|
|
424
500
|
}
|
|
425
501
|
): Promise<OperationUpdate[]> {
|
|
426
|
-
const
|
|
502
|
+
const syncUnit =
|
|
427
503
|
syncId === '0'
|
|
428
504
|
? { documentId: '', scope: 'global' }
|
|
429
|
-
: await this.
|
|
505
|
+
: await this.getSynchronizationUnitIdInfo(driveId, syncId);
|
|
506
|
+
|
|
507
|
+
if (!syncUnit) {
|
|
508
|
+
throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
|
|
509
|
+
}
|
|
430
510
|
|
|
431
511
|
const document =
|
|
432
512
|
syncId === '0'
|
|
433
513
|
? await this.getDrive(driveId)
|
|
434
|
-
: await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
|
|
514
|
+
: await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
|
|
435
515
|
|
|
436
|
-
const operations = document.operations[scope as OperationScope] ?? []; // TODO filter by branch also
|
|
516
|
+
const operations = document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
|
|
437
517
|
const filteredOperations = operations.filter(
|
|
438
518
|
operation =>
|
|
439
519
|
Object.keys(filter).length === 0 ||
|
|
@@ -650,7 +730,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
650
730
|
|
|
651
731
|
async deleteDocument(driveId: string, id: string) {
|
|
652
732
|
try {
|
|
653
|
-
const syncUnits = await this.
|
|
733
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
|
|
654
734
|
await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
|
|
655
735
|
} catch (error) {
|
|
656
736
|
logger.warn('Error deleting document', error);
|
|
@@ -1075,21 +1155,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1075
1155
|
.updateSynchronizationRevisions(
|
|
1076
1156
|
drive,
|
|
1077
1157
|
syncUnits,
|
|
1078
|
-
() =>
|
|
1158
|
+
() => {
|
|
1159
|
+
this.updateSyncStatus(drive, 'SYNCING');
|
|
1160
|
+
|
|
1161
|
+
for (const syncUnit of syncUnits) {
|
|
1162
|
+
this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1079
1165
|
this.handleListenerError.bind(this),
|
|
1080
1166
|
forceSync
|
|
1081
1167
|
)
|
|
1082
|
-
.then(
|
|
1083
|
-
updates
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1168
|
+
.then(updates => {
|
|
1169
|
+
updates.length && this.updateSyncStatus(drive, 'SUCCESS');
|
|
1170
|
+
|
|
1171
|
+
for (const syncUnit of syncUnits) {
|
|
1172
|
+
this.updateSyncStatus(syncUnit.syncId, 'SUCCESS');
|
|
1173
|
+
}
|
|
1174
|
+
})
|
|
1087
1175
|
.catch(error => {
|
|
1088
1176
|
logger.error(
|
|
1089
1177
|
'Non handled error updating sync revision',
|
|
1090
1178
|
error
|
|
1091
1179
|
);
|
|
1092
1180
|
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
1181
|
+
|
|
1182
|
+
for (const syncUnit of syncUnits) {
|
|
1183
|
+
this.updateSyncStatus(
|
|
1184
|
+
syncUnit.syncId,
|
|
1185
|
+
'ERROR',
|
|
1186
|
+
error as Error
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1093
1189
|
});
|
|
1094
1190
|
|
|
1095
1191
|
// after applying all the valid operations,throws
|
|
@@ -1208,6 +1304,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1208
1304
|
const signals: SignalResult[] = [];
|
|
1209
1305
|
let error: Error | undefined;
|
|
1210
1306
|
|
|
1307
|
+
const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1308
|
+
|
|
1211
1309
|
try {
|
|
1212
1310
|
await this._addDriveOperations(drive, async documentStorage => {
|
|
1213
1311
|
const result = await this._processOperations<
|
|
@@ -1247,6 +1345,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1247
1345
|
}
|
|
1248
1346
|
}
|
|
1249
1347
|
|
|
1348
|
+
const syncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1349
|
+
|
|
1350
|
+
const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
|
|
1351
|
+
const syncUnitsIds = syncUnits.map(unit => unit.syncId);
|
|
1352
|
+
|
|
1353
|
+
const newSyncUnits = syncUnitsIds.filter(
|
|
1354
|
+
syncUnitId => !prevSyncUnitsIds.includes(syncUnitId)
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
const removedSyncUnits = prevSyncUnitsIds.filter(
|
|
1358
|
+
syncUnitId => !syncUnitsIds.includes(syncUnitId)
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1250
1361
|
// update listener cache
|
|
1251
1362
|
const lastOperation = operationsApplied
|
|
1252
1363
|
.filter(op => op.scope === 'global')
|
|
@@ -1268,21 +1379,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1268
1379
|
revision: lastOperation.index
|
|
1269
1380
|
}
|
|
1270
1381
|
],
|
|
1271
|
-
() =>
|
|
1382
|
+
() => {
|
|
1383
|
+
this.updateSyncStatus(drive, 'SYNCING');
|
|
1384
|
+
|
|
1385
|
+
for (const syncUnitId of [
|
|
1386
|
+
...newSyncUnits,
|
|
1387
|
+
...removedSyncUnits
|
|
1388
|
+
]) {
|
|
1389
|
+
this.updateSyncStatus(syncUnitId, 'SYNCING');
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1272
1392
|
this.handleListenerError.bind(this),
|
|
1273
1393
|
forceSync
|
|
1274
1394
|
)
|
|
1275
|
-
.then(
|
|
1276
|
-
updates
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1395
|
+
.then(updates => {
|
|
1396
|
+
if (updates.length) {
|
|
1397
|
+
this.updateSyncStatus(drive, 'SUCCESS');
|
|
1398
|
+
|
|
1399
|
+
for (const syncUnitId of newSyncUnits) {
|
|
1400
|
+
this.updateSyncStatus(syncUnitId, 'SUCCESS');
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
for (const syncUnitId of removedSyncUnits) {
|
|
1404
|
+
this.updateSyncStatus(syncUnitId, null);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
})
|
|
1280
1408
|
.catch(error => {
|
|
1281
1409
|
logger.error(
|
|
1282
1410
|
'Non handled error updating sync revision',
|
|
1283
1411
|
error
|
|
1284
1412
|
);
|
|
1285
1413
|
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
1414
|
+
|
|
1415
|
+
for (const syncUnitId of [
|
|
1416
|
+
...newSyncUnits,
|
|
1417
|
+
...removedSyncUnits
|
|
1418
|
+
]) {
|
|
1419
|
+
this.updateSyncStatus(
|
|
1420
|
+
syncUnitId,
|
|
1421
|
+
'ERROR',
|
|
1422
|
+
error as Error
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1286
1425
|
});
|
|
1287
1426
|
}
|
|
1288
1427
|
|
|
@@ -118,7 +118,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
118
118
|
return driveMap.delete(listenerId);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
async removeSyncUnits(driveId: string, syncUnits: SynchronizationUnit[]) {
|
|
121
|
+
async removeSyncUnits(driveId: string, syncUnits: Pick<SynchronizationUnit, "syncId">[]) {
|
|
122
122
|
const listeners = this.listenerState.get(driveId);
|
|
123
123
|
if (!listeners) {
|
|
124
124
|
return;
|
|
@@ -243,7 +243,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
243
243
|
|
|
244
244
|
const opData: OperationUpdate[] = [];
|
|
245
245
|
try {
|
|
246
|
-
const data = await this.drive.getOperationData(
|
|
246
|
+
const data = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
|
|
247
247
|
driveId,
|
|
248
248
|
syncUnit.syncId,
|
|
249
249
|
{
|
|
@@ -373,6 +373,21 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
373
373
|
);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
getListenerSyncUnitIds(driveId: string, listenerId: string) {
|
|
377
|
+
const listener = this.listenerState.get(driveId)?.get(listenerId);
|
|
378
|
+
if (!listener) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
const filter = listener.listener.filter;
|
|
382
|
+
return this.drive.getSynchronizationUnitsIds(
|
|
383
|
+
driveId,
|
|
384
|
+
filter.documentId ?? ['*'],
|
|
385
|
+
filter.scope ?? ['*'],
|
|
386
|
+
filter.branch ?? ['*'],
|
|
387
|
+
filter.documentType ?? ['*']
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
376
391
|
async initDrive(drive: DocumentDriveDocument) {
|
|
377
392
|
const {
|
|
378
393
|
state: {
|
|
@@ -420,32 +435,40 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
420
435
|
const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
|
|
421
436
|
|
|
422
437
|
for (const syncUnit of syncUnits) {
|
|
438
|
+
if (syncUnit.revision < 0) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
423
441
|
const entry = listener.syncUnits.get(syncUnit.syncId);
|
|
424
442
|
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
425
443
|
continue;
|
|
426
444
|
}
|
|
427
445
|
|
|
428
446
|
const { documentId, driveId, scope, branch } = syncUnit;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
try {
|
|
448
|
+
const operations = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
|
|
449
|
+
driveId,
|
|
450
|
+
syncUnit.syncId,
|
|
451
|
+
{
|
|
452
|
+
since,
|
|
453
|
+
fromRevision: entry?.listenerRev
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (!operations.length) {
|
|
458
|
+
continue;
|
|
435
459
|
}
|
|
436
|
-
);
|
|
437
460
|
|
|
438
|
-
|
|
461
|
+
strands.push({
|
|
462
|
+
driveId,
|
|
463
|
+
documentId,
|
|
464
|
+
scope: scope as OperationScope,
|
|
465
|
+
branch,
|
|
466
|
+
operations
|
|
467
|
+
});
|
|
468
|
+
} catch (error) {
|
|
469
|
+
logger.error(error);
|
|
439
470
|
continue;
|
|
440
471
|
}
|
|
441
|
-
|
|
442
|
-
strands.push({
|
|
443
|
-
driveId,
|
|
444
|
-
documentId,
|
|
445
|
-
scope: scope as OperationScope,
|
|
446
|
-
branch,
|
|
447
|
-
operations
|
|
448
|
-
});
|
|
449
472
|
}
|
|
450
473
|
|
|
451
474
|
return strands;
|
|
@@ -72,7 +72,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
72
72
|
listenerId: string,
|
|
73
73
|
revisions: ListenerRevision[]
|
|
74
74
|
): Promise<boolean> {
|
|
75
|
-
const syncUnits = await this.manager.
|
|
75
|
+
const syncUnits = await this.manager.getListenerSyncUnitIds(
|
|
76
76
|
driveId,
|
|
77
77
|
listenerId
|
|
78
78
|
);
|
package/src/server/types.ts
CHANGED
|
@@ -59,6 +59,8 @@ export type SynchronizationUnit = {
|
|
|
59
59
|
revision: number;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
export type SynchronizationUnitQuery = Omit<SynchronizationUnit, "revision" | "lastUpdated">;
|
|
63
|
+
|
|
62
64
|
export type Listener = {
|
|
63
65
|
driveId: string;
|
|
64
66
|
listenerId: string;
|
|
@@ -286,7 +288,15 @@ export abstract class BaseDocumentDriveServer {
|
|
|
286
288
|
abstract getSynchronizationUnit(
|
|
287
289
|
driveId: string,
|
|
288
290
|
syncId: string
|
|
289
|
-
): Promise<SynchronizationUnit>;
|
|
291
|
+
): Promise<SynchronizationUnit | undefined>;
|
|
292
|
+
|
|
293
|
+
abstract getSynchronizationUnitsIds(
|
|
294
|
+
driveId: string,
|
|
295
|
+
documentId?: string[],
|
|
296
|
+
scope?: string[],
|
|
297
|
+
branch?: string[],
|
|
298
|
+
documentType?: string[]
|
|
299
|
+
): Promise<SynchronizationUnitQuery[]>;
|
|
290
300
|
|
|
291
301
|
abstract getOperationData(
|
|
292
302
|
driveId: string,
|
package/src/storage/browser.ts
CHANGED
|
@@ -3,10 +3,15 @@ import {
|
|
|
3
3
|
BaseAction,
|
|
4
4
|
Document,
|
|
5
5
|
DocumentHeader,
|
|
6
|
-
Operation
|
|
6
|
+
Operation,
|
|
7
|
+
OperationScope
|
|
7
8
|
} from 'document-model/document';
|
|
8
|
-
import { mergeOperations } from '..';
|
|
9
|
-
import {
|
|
9
|
+
import { mergeOperations, type SynchronizationUnitQuery } from '..';
|
|
10
|
+
import {
|
|
11
|
+
DocumentDriveStorage,
|
|
12
|
+
DocumentStorage,
|
|
13
|
+
IDriveStorage,
|
|
14
|
+
} from './types';
|
|
10
15
|
|
|
11
16
|
export class BrowserStorage implements IDriveStorage {
|
|
12
17
|
private db: Promise<LocalForage>;
|
|
@@ -18,7 +23,9 @@ export class BrowserStorage implements IDriveStorage {
|
|
|
18
23
|
constructor(namespace?: string) {
|
|
19
24
|
this.db = import('localforage').then(localForage =>
|
|
20
25
|
localForage.default.createInstance({
|
|
21
|
-
name: namespace
|
|
26
|
+
name: namespace
|
|
27
|
+
? `${namespace}:${BrowserStorage.DBName}`
|
|
28
|
+
: BrowserStorage.DBName
|
|
22
29
|
})
|
|
23
30
|
);
|
|
24
31
|
}
|
|
@@ -68,7 +75,7 @@ export class BrowserStorage implements IDriveStorage {
|
|
|
68
75
|
drive: string,
|
|
69
76
|
id: string,
|
|
70
77
|
operations: Operation[],
|
|
71
|
-
header: DocumentHeader
|
|
78
|
+
header: DocumentHeader
|
|
72
79
|
): Promise<void> {
|
|
73
80
|
const document = await this.getDocument(drive, id);
|
|
74
81
|
if (!document) {
|
|
@@ -154,4 +161,61 @@ export class BrowserStorage implements IDriveStorage {
|
|
|
154
161
|
});
|
|
155
162
|
return;
|
|
156
163
|
}
|
|
164
|
+
|
|
165
|
+
async getSynchronizationUnitsRevision(
|
|
166
|
+
units: SynchronizationUnitQuery[]
|
|
167
|
+
): Promise<
|
|
168
|
+
{
|
|
169
|
+
driveId: string;
|
|
170
|
+
documentId: string;
|
|
171
|
+
scope: string;
|
|
172
|
+
branch: string;
|
|
173
|
+
lastUpdated: string;
|
|
174
|
+
revision: number;
|
|
175
|
+
}[]
|
|
176
|
+
> {
|
|
177
|
+
const results = await Promise.allSettled(
|
|
178
|
+
units.map(async unit => {
|
|
179
|
+
try {
|
|
180
|
+
const document = await (unit.documentId
|
|
181
|
+
? this.getDocument(unit.driveId, unit.documentId)
|
|
182
|
+
: this.getDrive(unit.driveId));
|
|
183
|
+
if (!document) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const operation =
|
|
187
|
+
document.operations[unit.scope as OperationScope]?.at(
|
|
188
|
+
-1
|
|
189
|
+
);
|
|
190
|
+
if (operation) {
|
|
191
|
+
return {
|
|
192
|
+
driveId: unit.driveId,
|
|
193
|
+
documentId: unit.documentId,
|
|
194
|
+
scope: unit.scope,
|
|
195
|
+
branch: unit.branch,
|
|
196
|
+
lastUpdated: operation.timestamp,
|
|
197
|
+
revision: operation.index
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
return results.reduce<
|
|
206
|
+
{
|
|
207
|
+
driveId: string;
|
|
208
|
+
documentId: string;
|
|
209
|
+
scope: string;
|
|
210
|
+
branch: string;
|
|
211
|
+
lastUpdated: string;
|
|
212
|
+
revision: number;
|
|
213
|
+
}[]
|
|
214
|
+
>((acc, curr) => {
|
|
215
|
+
if (curr.status === 'fulfilled' && curr.value !== undefined) {
|
|
216
|
+
acc.push(curr.value);
|
|
217
|
+
}
|
|
218
|
+
return acc;
|
|
219
|
+
}, []);
|
|
220
|
+
}
|
|
157
221
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DocumentDriveAction } from 'document-model-libs/document-drive';
|
|
2
|
-
import { BaseAction, DocumentHeader, Operation } from 'document-model/document';
|
|
2
|
+
import { BaseAction, DocumentHeader, Operation, OperationScope } from 'document-model/document';
|
|
3
3
|
import type { Dirent } from 'fs';
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
@@ -12,7 +12,8 @@ import fs from 'fs/promises';
|
|
|
12
12
|
import stringify from 'json-stringify-deterministic';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import sanitize from 'sanitize-filename';
|
|
15
|
-
import {
|
|
15
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
16
|
+
import { mergeOperations } from '../utils';
|
|
16
17
|
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
17
18
|
|
|
18
19
|
type FSError = {
|
|
@@ -235,4 +236,61 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
235
236
|
operations: mergedOperations
|
|
236
237
|
});
|
|
237
238
|
}
|
|
239
|
+
|
|
240
|
+
async getSynchronizationUnitsRevision(
|
|
241
|
+
units: SynchronizationUnitQuery[]
|
|
242
|
+
): Promise<
|
|
243
|
+
{
|
|
244
|
+
driveId: string;
|
|
245
|
+
documentId: string;
|
|
246
|
+
scope: string;
|
|
247
|
+
branch: string;
|
|
248
|
+
lastUpdated: string;
|
|
249
|
+
revision: number;
|
|
250
|
+
}[]
|
|
251
|
+
> {
|
|
252
|
+
const results = await Promise.allSettled(
|
|
253
|
+
units.map(async unit => {
|
|
254
|
+
try {
|
|
255
|
+
const document = await (unit.documentId
|
|
256
|
+
? this.getDocument(unit.driveId, unit.documentId)
|
|
257
|
+
: this.getDrive(unit.driveId));
|
|
258
|
+
if (!document) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const operation =
|
|
262
|
+
document.operations[unit.scope as OperationScope]?.at(
|
|
263
|
+
-1
|
|
264
|
+
);
|
|
265
|
+
if (operation) {
|
|
266
|
+
return {
|
|
267
|
+
driveId: unit.driveId,
|
|
268
|
+
documentId: unit.documentId,
|
|
269
|
+
scope: unit.scope,
|
|
270
|
+
branch: unit.branch,
|
|
271
|
+
lastUpdated: operation.timestamp,
|
|
272
|
+
revision: operation.index
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
return results.reduce<
|
|
281
|
+
{
|
|
282
|
+
driveId: string;
|
|
283
|
+
documentId: string;
|
|
284
|
+
scope: string;
|
|
285
|
+
branch: string;
|
|
286
|
+
lastUpdated: string;
|
|
287
|
+
revision: number;
|
|
288
|
+
}[]
|
|
289
|
+
>((acc, curr) => {
|
|
290
|
+
if (curr.status === 'fulfilled' && curr.value !== undefined) {
|
|
291
|
+
acc.push(curr.value);
|
|
292
|
+
}
|
|
293
|
+
return acc;
|
|
294
|
+
}, []);
|
|
295
|
+
}
|
|
238
296
|
}
|
package/src/storage/memory.ts
CHANGED
|
@@ -3,10 +3,16 @@ import {
|
|
|
3
3
|
BaseAction,
|
|
4
4
|
Document,
|
|
5
5
|
DocumentHeader,
|
|
6
|
-
Operation
|
|
6
|
+
Operation,
|
|
7
|
+
OperationScope
|
|
7
8
|
} from 'document-model/document';
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
DocumentDriveStorage,
|
|
11
|
+
DocumentStorage,
|
|
12
|
+
IDriveStorage,
|
|
13
|
+
} from './types';
|
|
14
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
15
|
+
import { mergeOperations } from '../utils';
|
|
10
16
|
|
|
11
17
|
export class MemoryStorage implements IDriveStorage {
|
|
12
18
|
private documents: Record<string, Record<string, DocumentStorage>>;
|
|
@@ -19,7 +25,7 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
checkDocumentExists(drive: string, id: string): Promise<boolean> {
|
|
22
|
-
return Promise.resolve(this.documents[drive]?.[id] !== undefined)
|
|
28
|
+
return Promise.resolve(this.documents[drive]?.[id] !== undefined);
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
async getDocuments(drive: string) {
|
|
@@ -153,4 +159,61 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
153
159
|
delete this.documents[id];
|
|
154
160
|
delete this.drives[id];
|
|
155
161
|
}
|
|
162
|
+
|
|
163
|
+
async getSynchronizationUnitsRevision(
|
|
164
|
+
units: SynchronizationUnitQuery[]
|
|
165
|
+
): Promise<
|
|
166
|
+
{
|
|
167
|
+
driveId: string;
|
|
168
|
+
documentId: string;
|
|
169
|
+
scope: string;
|
|
170
|
+
branch: string;
|
|
171
|
+
lastUpdated: string;
|
|
172
|
+
revision: number;
|
|
173
|
+
}[]
|
|
174
|
+
> {
|
|
175
|
+
const results = await Promise.allSettled(
|
|
176
|
+
units.map(async unit => {
|
|
177
|
+
try {
|
|
178
|
+
const document = await (unit.documentId
|
|
179
|
+
? this.getDocument(unit.driveId, unit.documentId)
|
|
180
|
+
: this.getDrive(unit.driveId));
|
|
181
|
+
if (!document) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const operation =
|
|
185
|
+
document.operations[unit.scope as OperationScope]?.at(
|
|
186
|
+
-1
|
|
187
|
+
);
|
|
188
|
+
if (operation) {
|
|
189
|
+
return {
|
|
190
|
+
driveId: unit.driveId,
|
|
191
|
+
documentId: unit.documentId,
|
|
192
|
+
scope: unit.scope,
|
|
193
|
+
branch: unit.branch,
|
|
194
|
+
lastUpdated: operation.timestamp,
|
|
195
|
+
revision: operation.index
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
return results.reduce<
|
|
204
|
+
{
|
|
205
|
+
driveId: string;
|
|
206
|
+
documentId: string;
|
|
207
|
+
scope: string;
|
|
208
|
+
branch: string;
|
|
209
|
+
lastUpdated: string;
|
|
210
|
+
revision: number;
|
|
211
|
+
}[]
|
|
212
|
+
>((acc, curr) => {
|
|
213
|
+
if (curr.status === 'fulfilled' && curr.value !== undefined) {
|
|
214
|
+
acc.push(curr.value);
|
|
215
|
+
}
|
|
216
|
+
return acc;
|
|
217
|
+
}, []);
|
|
218
|
+
}
|
|
156
219
|
}
|
package/src/storage/prisma.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { IBackOffOptions, backOff } from 'exponential-backoff';
|
|
|
22
22
|
import { ConflictOperationError } from '../server/error';
|
|
23
23
|
import { logger } from '../utils/logger';
|
|
24
24
|
import { DocumentDriveStorage, DocumentStorage, IDriveStorage, IStorageDelegate } from './types';
|
|
25
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
25
26
|
|
|
26
27
|
type Transaction =
|
|
27
28
|
| Omit<
|
|
@@ -552,4 +553,37 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
552
553
|
getDriveOperationResultingState(drive: string, index: number, scope: string, branch: string): Promise<unknown> {
|
|
553
554
|
return this.getOperationResultingState("drives", drive, index, scope, branch);
|
|
554
555
|
}
|
|
556
|
+
|
|
557
|
+
async getSynchronizationUnitsRevision(
|
|
558
|
+
units: SynchronizationUnitQuery[]
|
|
559
|
+
): Promise<
|
|
560
|
+
{
|
|
561
|
+
driveId: string;
|
|
562
|
+
documentId: string;
|
|
563
|
+
scope: string;
|
|
564
|
+
branch: string;
|
|
565
|
+
lastUpdated: string;
|
|
566
|
+
revision: number;
|
|
567
|
+
}[]
|
|
568
|
+
> {
|
|
569
|
+
// TODO add branch condition
|
|
570
|
+
const whereClauses = units.map((_, index) => {
|
|
571
|
+
return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
|
|
572
|
+
}).join(' OR ');
|
|
573
|
+
|
|
574
|
+
const query = `
|
|
575
|
+
SELECT "driveId", "documentId", "scope", "branch", MAX("timestamp") as "lastUpdated", MAX("index") as revision FROM "Operation"
|
|
576
|
+
WHERE ${whereClauses}
|
|
577
|
+
GROUP BY "driveId", "documentId", "scope", "branch"
|
|
578
|
+
`;
|
|
579
|
+
|
|
580
|
+
const params = units.map(unit => [unit.documentId ? unit.driveId : "drives", unit.documentId || unit.driveId, unit.scope]).flat();
|
|
581
|
+
const results = await this.db.$queryRawUnsafe<{ driveId: string, documentId: string, lastUpdated: string, scope: OperationScope, branch: string, revision: number }[]>(query, ...params);
|
|
582
|
+
return results.map(row => ({
|
|
583
|
+
...row,
|
|
584
|
+
driveId: row.driveId === "drives" ? row.documentId : row.driveId,
|
|
585
|
+
documentId: row.driveId === "drives" ? '' : row.documentId,
|
|
586
|
+
lastUpdated: new Date(row.lastUpdated).toISOString(),
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
555
589
|
}
|
package/src/storage/sequelize.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'document-model/document';
|
|
12
12
|
import { DataTypes, Options, Sequelize } from 'sequelize';
|
|
13
13
|
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
14
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
14
15
|
|
|
15
16
|
export class SequelizeStorage implements IDriveStorage {
|
|
16
17
|
private db: Sequelize;
|
|
@@ -448,4 +449,61 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
448
449
|
});
|
|
449
450
|
}
|
|
450
451
|
}
|
|
452
|
+
|
|
453
|
+
async getSynchronizationUnitsRevision(
|
|
454
|
+
units: SynchronizationUnitQuery[]
|
|
455
|
+
): Promise<
|
|
456
|
+
{
|
|
457
|
+
driveId: string;
|
|
458
|
+
documentId: string;
|
|
459
|
+
scope: string;
|
|
460
|
+
branch: string;
|
|
461
|
+
lastUpdated: string;
|
|
462
|
+
revision: number;
|
|
463
|
+
}[]
|
|
464
|
+
> {
|
|
465
|
+
const results = await Promise.allSettled(
|
|
466
|
+
units.map(async unit => {
|
|
467
|
+
try {
|
|
468
|
+
const document = await (unit.documentId
|
|
469
|
+
? this.getDocument(unit.driveId, unit.documentId)
|
|
470
|
+
: this.getDrive(unit.driveId));
|
|
471
|
+
if (!document) {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
const operation =
|
|
475
|
+
document.operations[unit.scope as OperationScope]?.at(
|
|
476
|
+
-1
|
|
477
|
+
);
|
|
478
|
+
if (operation) {
|
|
479
|
+
return {
|
|
480
|
+
driveId: unit.driveId,
|
|
481
|
+
documentId: unit.documentId,
|
|
482
|
+
scope: unit.scope,
|
|
483
|
+
branch: unit.branch,
|
|
484
|
+
lastUpdated: operation.timestamp,
|
|
485
|
+
revision: operation.index
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
return results.reduce<
|
|
494
|
+
{
|
|
495
|
+
driveId: string;
|
|
496
|
+
documentId: string;
|
|
497
|
+
scope: string;
|
|
498
|
+
branch: string;
|
|
499
|
+
lastUpdated: string;
|
|
500
|
+
revision: number;
|
|
501
|
+
}[]
|
|
502
|
+
>((acc, curr) => {
|
|
503
|
+
if (curr.status === 'fulfilled' && curr.value !== undefined) {
|
|
504
|
+
acc.push(curr.value);
|
|
505
|
+
}
|
|
506
|
+
return acc;
|
|
507
|
+
}, []);
|
|
508
|
+
}
|
|
451
509
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
DocumentOperations,
|
|
11
11
|
Operation,
|
|
12
12
|
} from 'document-model/document';
|
|
13
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
13
14
|
|
|
14
15
|
export type DocumentStorage<D extends Document = Document> = Omit<
|
|
15
16
|
D,
|
|
@@ -48,6 +49,14 @@ export interface IStorage {
|
|
|
48
49
|
deleteDocument(drive: string, id: string): Promise<void>;
|
|
49
50
|
getOperationResultingState?(drive: string, id: string, index: number, scope: string, branch: string): Promise<unknown>;
|
|
50
51
|
setStorageDelegate?(delegate: IStorageDelegate): void;
|
|
52
|
+
getSynchronizationUnitsRevision(units: SynchronizationUnitQuery[]): Promise<{
|
|
53
|
+
driveId: string;
|
|
54
|
+
documentId: string;
|
|
55
|
+
scope: string;
|
|
56
|
+
branch: string;
|
|
57
|
+
lastUpdated: string;
|
|
58
|
+
revision: number;
|
|
59
|
+
}[]>
|
|
51
60
|
}
|
|
52
61
|
export interface IDriveStorage extends IStorage {
|
|
53
62
|
getDrives(): Promise<string[]>;
|
|
@@ -69,4 +78,4 @@ export interface IDriveStorage extends IStorage {
|
|
|
69
78
|
}>
|
|
70
79
|
): Promise<void>;
|
|
71
80
|
getDriveOperationResultingState?(drive: string, index: number, scope: string, branch: string): Promise<unknown>;
|
|
72
|
-
}
|
|
81
|
+
}
|