document-drive 1.0.0-alpha.72 → 1.0.0-alpha.74
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 +97 -43
- package/src/server/listener/manager.ts +55 -21
- package/src/server/listener/transmitter/internal.ts +5 -0
- package/src/server/listener/transmitter/pull-responder.ts +1 -1
- package/src/server/listener/transmitter/types.ts +1 -0
- package/src/server/types.ts +12 -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,
|
|
@@ -186,7 +187,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
186
187
|
const drive = await this.getDrive(driveId);
|
|
187
188
|
let driveTriggers = this.triggerMap.get(driveId);
|
|
188
189
|
|
|
189
|
-
const syncUnits = await this.
|
|
190
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
190
191
|
|
|
191
192
|
for (const trigger of drive.state.local.triggers) {
|
|
192
193
|
if (driveTriggers?.get(trigger.id)) {
|
|
@@ -252,7 +253,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
private async stopSyncRemoteDrive(driveId: string) {
|
|
255
|
-
const syncUnits = await this.
|
|
256
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
256
257
|
const fileNodes = syncUnits
|
|
257
258
|
.filter(syncUnit => syncUnit.documentId !== '')
|
|
258
259
|
.map(syncUnit => syncUnit.documentId);
|
|
@@ -338,6 +339,34 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
338
339
|
) {
|
|
339
340
|
const drive = await this.getDrive(driveId);
|
|
340
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);
|
|
341
370
|
const nodes = drive.state.global.nodes.filter(
|
|
342
371
|
node =>
|
|
343
372
|
isFileNode(node) &&
|
|
@@ -371,8 +400,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
371
400
|
});
|
|
372
401
|
}
|
|
373
402
|
|
|
374
|
-
const
|
|
375
|
-
|
|
403
|
+
const synchronizationUnitsQuery: Omit<SynchronizationUnit, "revision" | "lastUpdated">[] = [];
|
|
376
404
|
for (const node of nodes) {
|
|
377
405
|
const nodeUnits =
|
|
378
406
|
scope?.length || branch?.length
|
|
@@ -389,35 +417,22 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
389
417
|
if (!nodeUnits.length) {
|
|
390
418
|
continue;
|
|
391
419
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const lastOperation = operations[operations.length - 1];
|
|
401
|
-
synchronizationUnits.push({
|
|
402
|
-
syncId,
|
|
403
|
-
scope,
|
|
404
|
-
branch,
|
|
405
|
-
driveId,
|
|
406
|
-
documentId: node.id,
|
|
407
|
-
documentType: node.documentType,
|
|
408
|
-
lastUpdated:
|
|
409
|
-
lastOperation?.timestamp ?? document.lastModified,
|
|
410
|
-
revision: lastOperation?.index ?? 0
|
|
411
|
-
});
|
|
412
|
-
}
|
|
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
|
+
})));
|
|
413
428
|
}
|
|
414
|
-
return
|
|
429
|
+
return synchronizationUnitsQuery;
|
|
415
430
|
}
|
|
416
431
|
|
|
417
|
-
public async
|
|
432
|
+
public async getSynchronizationUnitIdInfo(
|
|
418
433
|
driveId: string,
|
|
419
434
|
syncId: string
|
|
420
|
-
): Promise<
|
|
435
|
+
): Promise<SynchronizationUnitQuery | undefined> {
|
|
421
436
|
const drive = await this.getDrive(driveId);
|
|
422
437
|
const node = drive.state.global.nodes.find(
|
|
423
438
|
node =>
|
|
@@ -426,14 +441,40 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
426
441
|
);
|
|
427
442
|
|
|
428
443
|
if (!node || !isFileNode(node)) {
|
|
429
|
-
|
|
444
|
+
return undefined;
|
|
430
445
|
}
|
|
431
446
|
|
|
432
|
-
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
448
|
+
const syncUnit = node.synchronizationUnits.find(
|
|
433
449
|
unit => unit.syncId === syncId
|
|
434
|
-
)
|
|
450
|
+
);
|
|
451
|
+
if (!syncUnit) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
435
454
|
|
|
436
|
-
|
|
455
|
+
return {
|
|
456
|
+
syncId,
|
|
457
|
+
scope: syncUnit.scope,
|
|
458
|
+
branch: syncUnit.branch,
|
|
459
|
+
driveId,
|
|
460
|
+
documentId: node.id,
|
|
461
|
+
documentType: node.documentType,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
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
|
|
437
478
|
const document = await this.getDocument(driveId, documentId);
|
|
438
479
|
const operations = document.operations[scope as OperationScope] ?? [];
|
|
439
480
|
const lastOperation = operations[operations.length - 1];
|
|
@@ -444,7 +485,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
444
485
|
branch,
|
|
445
486
|
driveId,
|
|
446
487
|
documentId,
|
|
447
|
-
documentType
|
|
488
|
+
documentType,
|
|
448
489
|
lastUpdated: lastOperation?.timestamp ?? document.lastModified,
|
|
449
490
|
revision: lastOperation?.index ?? 0
|
|
450
491
|
};
|
|
@@ -458,17 +499,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
458
499
|
fromRevision?: number | undefined;
|
|
459
500
|
}
|
|
460
501
|
): Promise<OperationUpdate[]> {
|
|
461
|
-
const
|
|
502
|
+
const syncUnit =
|
|
462
503
|
syncId === '0'
|
|
463
504
|
? { documentId: '', scope: 'global' }
|
|
464
|
-
: await this.
|
|
505
|
+
: await this.getSynchronizationUnitIdInfo(driveId, syncId);
|
|
506
|
+
|
|
507
|
+
if (!syncUnit) {
|
|
508
|
+
throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
|
|
509
|
+
}
|
|
465
510
|
|
|
466
511
|
const document =
|
|
467
512
|
syncId === '0'
|
|
468
513
|
? await this.getDrive(driveId)
|
|
469
|
-
: await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
|
|
514
|
+
: await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
|
|
470
515
|
|
|
471
|
-
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
|
|
472
517
|
const filteredOperations = operations.filter(
|
|
473
518
|
operation =>
|
|
474
519
|
Object.keys(filter).length === 0 ||
|
|
@@ -560,9 +605,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
560
605
|
}
|
|
561
606
|
|
|
562
607
|
async deleteDrive(id: string) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
608
|
+
const result = await Promise.allSettled([
|
|
609
|
+
this.stopSyncRemoteDrive(id),
|
|
610
|
+
this.listenerStateManager.removeDrive(id),
|
|
611
|
+
this.cache.deleteDocument('drives', id),
|
|
612
|
+
this.storage.deleteDrive(id)
|
|
613
|
+
]);
|
|
614
|
+
|
|
615
|
+
result.forEach(r => {
|
|
616
|
+
if (r.status === "rejected") {
|
|
617
|
+
throw r.reason;
|
|
618
|
+
}
|
|
619
|
+
});
|
|
566
620
|
}
|
|
567
621
|
|
|
568
622
|
getDrives() {
|
|
@@ -685,7 +739,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
685
739
|
|
|
686
740
|
async deleteDocument(driveId: string, id: string) {
|
|
687
741
|
try {
|
|
688
|
-
const syncUnits = await this.
|
|
742
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
|
|
689
743
|
await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
|
|
690
744
|
} catch (error) {
|
|
691
745
|
logger.warn('Error deleting document', error);
|
|
@@ -1259,7 +1313,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1259
1313
|
const signals: SignalResult[] = [];
|
|
1260
1314
|
let error: Error | undefined;
|
|
1261
1315
|
|
|
1262
|
-
const prevSyncUnits = await this.
|
|
1316
|
+
const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1263
1317
|
|
|
1264
1318
|
try {
|
|
1265
1319
|
await this._addDriveOperations(drive, async documentStorage => {
|
|
@@ -1300,7 +1354,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1300
1354
|
}
|
|
1301
1355
|
}
|
|
1302
1356
|
|
|
1303
|
-
const syncUnits = await this.
|
|
1357
|
+
const syncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1304
1358
|
|
|
1305
1359
|
const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
|
|
1306
1360
|
const syncUnitsIds = syncUnits.map(unit => unit.syncId);
|
|
@@ -53,7 +53,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
53
53
|
driveId: string,
|
|
54
54
|
listenerId: string
|
|
55
55
|
): Promise<ITransmitter | undefined> {
|
|
56
|
-
return this.transmitters[driveId]?.[listenerId];
|
|
56
|
+
return Promise.resolve(this.transmitters[driveId]?.[listenerId]);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async addListener(listener: Listener) {
|
|
@@ -106,7 +106,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
106
106
|
const driveTransmitters = this.transmitters[drive] || {};
|
|
107
107
|
driveTransmitters[listener.listenerId] = transmitter;
|
|
108
108
|
this.transmitters[drive] = driveTransmitters;
|
|
109
|
-
return transmitter;
|
|
109
|
+
return Promise.resolve(transmitter);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
async removeListener(driveId: string, listenerId: string) {
|
|
@@ -115,10 +115,10 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
return driveMap.delete(listenerId);
|
|
118
|
+
return Promise.resolve(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;
|
|
@@ -128,6 +128,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
128
128
|
listener.syncUnits.delete(syncUnit.syncId);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
return Promise.resolve();
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
async updateSynchronizationRevisions(
|
|
@@ -203,6 +204,8 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
203
204
|
} else {
|
|
204
205
|
listener.syncUnits.set(syncId, { listenerRev, lastUpdated });
|
|
205
206
|
}
|
|
207
|
+
|
|
208
|
+
return Promise.resolve();
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
triggerUpdate = debounce(
|
|
@@ -243,7 +246,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
243
246
|
|
|
244
247
|
const opData: OperationUpdate[] = [];
|
|
245
248
|
try {
|
|
246
|
-
const data = await this.drive.getOperationData(
|
|
249
|
+
const data = await this.drive.getOperationData( // TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
|
|
247
250
|
driveId,
|
|
248
251
|
syncUnit.syncId,
|
|
249
252
|
{
|
|
@@ -373,6 +376,21 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
373
376
|
);
|
|
374
377
|
}
|
|
375
378
|
|
|
379
|
+
getListenerSyncUnitIds(driveId: string, listenerId: string) {
|
|
380
|
+
const listener = this.listenerState.get(driveId)?.get(listenerId);
|
|
381
|
+
if (!listener) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
const filter = listener.listener.filter;
|
|
385
|
+
return this.drive.getSynchronizationUnitsIds(
|
|
386
|
+
driveId,
|
|
387
|
+
filter.documentId ?? ['*'],
|
|
388
|
+
filter.scope ?? ['*'],
|
|
389
|
+
filter.branch ?? ['*'],
|
|
390
|
+
filter.documentType ?? ['*']
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
376
394
|
async initDrive(drive: DocumentDriveDocument) {
|
|
377
395
|
const {
|
|
378
396
|
state: {
|
|
@@ -398,6 +416,14 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
398
416
|
}
|
|
399
417
|
}
|
|
400
418
|
|
|
419
|
+
async removeDrive(driveId: string): Promise<void> {
|
|
420
|
+
this.listenerState.delete(driveId);
|
|
421
|
+
const transmitters = this.transmitters[driveId];
|
|
422
|
+
if (transmitters) {
|
|
423
|
+
await Promise.all(Object.values(transmitters).map(t => t.disconnect?.()));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
401
427
|
getListener(driveId: string, listenerId: string): Promise<ListenerState> {
|
|
402
428
|
const drive = this.listenerState.get(driveId);
|
|
403
429
|
if (!drive) throw new Error('Drive not found');
|
|
@@ -420,32 +446,40 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
420
446
|
const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
|
|
421
447
|
|
|
422
448
|
for (const syncUnit of syncUnits) {
|
|
449
|
+
if (syncUnit.revision < 0) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
423
452
|
const entry = listener.syncUnits.get(syncUnit.syncId);
|
|
424
453
|
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
425
454
|
continue;
|
|
426
455
|
}
|
|
427
456
|
|
|
428
457
|
const { documentId, driveId, scope, branch } = syncUnit;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
try {
|
|
459
|
+
const operations = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
|
|
460
|
+
driveId,
|
|
461
|
+
syncUnit.syncId,
|
|
462
|
+
{
|
|
463
|
+
since,
|
|
464
|
+
fromRevision: entry?.listenerRev
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
if (!operations.length) {
|
|
469
|
+
continue;
|
|
435
470
|
}
|
|
436
|
-
);
|
|
437
471
|
|
|
438
|
-
|
|
472
|
+
strands.push({
|
|
473
|
+
driveId,
|
|
474
|
+
documentId,
|
|
475
|
+
scope: scope as OperationScope,
|
|
476
|
+
branch,
|
|
477
|
+
operations
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
logger.error(error);
|
|
439
481
|
continue;
|
|
440
482
|
}
|
|
441
|
-
|
|
442
|
-
strands.push({
|
|
443
|
-
driveId,
|
|
444
|
-
documentId,
|
|
445
|
-
scope: scope as OperationScope,
|
|
446
|
-
branch,
|
|
447
|
-
operations
|
|
448
|
-
});
|
|
449
483
|
}
|
|
450
484
|
|
|
451
485
|
return strands;
|
|
@@ -12,6 +12,7 @@ import { logger } from '../../../utils/logger';
|
|
|
12
12
|
|
|
13
13
|
export interface IReceiver {
|
|
14
14
|
transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
|
|
15
|
+
disconnect: () => Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type InternalTransmitterUpdate<
|
|
@@ -88,4 +89,8 @@ export class InternalTransmitter implements ITransmitter {
|
|
|
88
89
|
setReceiver(receiver: IReceiver) {
|
|
89
90
|
this.receiver = receiver;
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
async disconnect(): Promise<void> {
|
|
94
|
+
await this.receiver?.disconnect();
|
|
95
|
+
}
|
|
91
96
|
}
|
|
@@ -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,
|
|
@@ -341,6 +351,7 @@ export abstract class BaseListenerManager {
|
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
abstract initDrive(drive: DocumentDriveDocument): Promise<void>;
|
|
354
|
+
abstract removeDrive(driveId: DocumentDriveState["id"]): Promise<void>;
|
|
344
355
|
|
|
345
356
|
abstract addListener(listener: Listener): Promise<ITransmitter>;
|
|
346
357
|
abstract removeListener(
|
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
|
+
}
|