document-drive 1.0.0-alpha.72 → 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 +85 -40
- 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,
|
|
@@ -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<SynchronizationUnit> {
|
|
435
|
+
): Promise<Omit<SynchronizationUnit, "revision" | "lastUpdated"> | 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
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
435
474
|
|
|
436
|
-
const documentId =
|
|
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 ||
|
|
@@ -685,7 +730,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
685
730
|
|
|
686
731
|
async deleteDocument(driveId: string, id: string) {
|
|
687
732
|
try {
|
|
688
|
-
const syncUnits = await this.
|
|
733
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
|
|
689
734
|
await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
|
|
690
735
|
} catch (error) {
|
|
691
736
|
logger.warn('Error deleting document', error);
|
|
@@ -1259,7 +1304,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1259
1304
|
const signals: SignalResult[] = [];
|
|
1260
1305
|
let error: Error | undefined;
|
|
1261
1306
|
|
|
1262
|
-
const prevSyncUnits = await this.
|
|
1307
|
+
const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1263
1308
|
|
|
1264
1309
|
try {
|
|
1265
1310
|
await this._addDriveOperations(drive, async documentStorage => {
|
|
@@ -1300,7 +1345,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1300
1345
|
}
|
|
1301
1346
|
}
|
|
1302
1347
|
|
|
1303
|
-
const syncUnits = await this.
|
|
1348
|
+
const syncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1304
1349
|
|
|
1305
1350
|
const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
|
|
1306
1351
|
const syncUnitsIds = syncUnits.map(unit => unit.syncId);
|
|
@@ -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
|
+
}
|