document-drive 1.0.0-alpha.74 → 1.0.0-alpha.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/server/index.ts +466 -188
- package/src/storage/prisma.ts +113 -55
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Document Drive
|
package/package.json
CHANGED
package/src/server/index.ts
CHANGED
|
@@ -18,18 +18,27 @@ import {
|
|
|
18
18
|
Document,
|
|
19
19
|
DocumentHeader,
|
|
20
20
|
DocumentModel,
|
|
21
|
+
utils as DocumentUtils,
|
|
21
22
|
Operation,
|
|
22
|
-
OperationScope
|
|
23
|
-
utils as DocumentUtils
|
|
23
|
+
OperationScope
|
|
24
24
|
} from 'document-model/document';
|
|
25
25
|
import { createNanoEvents, Unsubscribe } from 'nanoevents';
|
|
26
26
|
import { ICache } from '../cache';
|
|
27
27
|
import InMemoryCache from '../cache/memory';
|
|
28
|
+
import { BaseQueueManager } from '../queue/base';
|
|
29
|
+
import {
|
|
30
|
+
ActionJob,
|
|
31
|
+
IQueueManager,
|
|
32
|
+
isActionJob,
|
|
33
|
+
isOperationJob,
|
|
34
|
+
Job,
|
|
35
|
+
OperationJob
|
|
36
|
+
} from '../queue/types';
|
|
28
37
|
import { MemoryStorage } from '../storage/memory';
|
|
29
38
|
import type {
|
|
30
39
|
DocumentDriveStorage,
|
|
31
40
|
DocumentStorage,
|
|
32
|
-
IDriveStorage
|
|
41
|
+
IDriveStorage
|
|
33
42
|
} from '../storage/types';
|
|
34
43
|
import { generateUUID, isBefore, isDocumentDrive } from '../utils';
|
|
35
44
|
import {
|
|
@@ -70,8 +79,6 @@ import {
|
|
|
70
79
|
type SynchronizationUnit
|
|
71
80
|
} from './types';
|
|
72
81
|
import { filterOperationsByRevision } from './utils';
|
|
73
|
-
import { BaseQueueManager } from '../queue/base';
|
|
74
|
-
import { ActionJob, IQueueManager, isActionJob, isOperationJob, Job, OperationJob } from '../queue/types';
|
|
75
82
|
|
|
76
83
|
export * from './listener';
|
|
77
84
|
export type * from './types';
|
|
@@ -96,7 +103,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
96
103
|
documentModels: DocumentModel[],
|
|
97
104
|
storage: IDriveStorage = new MemoryStorage(),
|
|
98
105
|
cache: ICache = new InMemoryCache(),
|
|
99
|
-
queueManager: IQueueManager = new BaseQueueManager()
|
|
106
|
+
queueManager: IQueueManager = new BaseQueueManager()
|
|
100
107
|
) {
|
|
101
108
|
super();
|
|
102
109
|
this.listenerStateManager = new ListenerManager(this);
|
|
@@ -115,7 +122,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
115
122
|
return undefined;
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
|
-
})
|
|
125
|
+
});
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
private updateSyncStatus(
|
|
@@ -132,26 +139,24 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
private async saveStrand(strand: StrandUpdate) {
|
|
135
|
-
const operations: Operation[] = strand.operations.map(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
+
const operations: Operation[] = strand.operations.map(op => ({
|
|
143
|
+
...op,
|
|
144
|
+
scope: strand.scope,
|
|
145
|
+
branch: strand.branch
|
|
146
|
+
}));
|
|
142
147
|
|
|
143
148
|
const result = await (!strand.documentId
|
|
144
149
|
? this.queueDriveOperations(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
strand.driveId,
|
|
151
|
+
operations as Operation<DocumentDriveAction | BaseAction>[],
|
|
152
|
+
false
|
|
153
|
+
)
|
|
149
154
|
: this.queueOperations(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
strand.driveId,
|
|
156
|
+
strand.documentId,
|
|
157
|
+
operations,
|
|
158
|
+
false
|
|
159
|
+
));
|
|
155
160
|
|
|
156
161
|
if (result.status === 'ERROR') {
|
|
157
162
|
this.updateSyncStatus(strand.driveId, result.status, result.error);
|
|
@@ -269,12 +274,40 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
private queueDelegate = {
|
|
272
|
-
checkDocumentExists: (
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
checkDocumentExists: (
|
|
278
|
+
driveId: string,
|
|
279
|
+
documentId: string
|
|
280
|
+
): Promise<boolean> =>
|
|
281
|
+
this.storage.checkDocumentExists(driveId, documentId),
|
|
282
|
+
processOperationJob: async ({
|
|
283
|
+
driveId,
|
|
284
|
+
documentId,
|
|
285
|
+
operations,
|
|
286
|
+
forceSync
|
|
287
|
+
}: OperationJob) => {
|
|
288
|
+
return documentId
|
|
289
|
+
? this.addOperations(driveId, documentId, operations, forceSync)
|
|
290
|
+
: this.addDriveOperations(
|
|
291
|
+
driveId,
|
|
292
|
+
operations as Operation<
|
|
293
|
+
DocumentDriveAction | BaseAction
|
|
294
|
+
>[],
|
|
295
|
+
forceSync
|
|
296
|
+
);
|
|
275
297
|
},
|
|
276
|
-
processActionJob: async ({
|
|
277
|
-
|
|
298
|
+
processActionJob: async ({
|
|
299
|
+
driveId,
|
|
300
|
+
documentId,
|
|
301
|
+
actions,
|
|
302
|
+
forceSync
|
|
303
|
+
}: ActionJob) => {
|
|
304
|
+
return documentId
|
|
305
|
+
? this.addActions(driveId, documentId, actions, forceSync)
|
|
306
|
+
: this.addDriveActions(
|
|
307
|
+
driveId,
|
|
308
|
+
actions as Operation<DocumentDriveAction | BaseAction>[],
|
|
309
|
+
forceSync
|
|
310
|
+
);
|
|
278
311
|
},
|
|
279
312
|
processJob: async (job: Job) => {
|
|
280
313
|
if (isOperationJob(job)) {
|
|
@@ -282,7 +315,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
282
315
|
} else if (isActionJob(job)) {
|
|
283
316
|
return this.queueDelegate.processActionJob(job);
|
|
284
317
|
} else {
|
|
285
|
-
throw new Error(
|
|
318
|
+
throw new Error('Unknown job type', job);
|
|
286
319
|
}
|
|
287
320
|
}
|
|
288
321
|
};
|
|
@@ -300,7 +333,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
300
333
|
await this.queueManager.init(this.queueDelegate, error => {
|
|
301
334
|
logger.error(`Error initializing queue manager`, error);
|
|
302
335
|
errors.push(error);
|
|
303
|
-
})
|
|
336
|
+
});
|
|
304
337
|
|
|
305
338
|
// if network connect comes online then
|
|
306
339
|
// triggers the listeners update
|
|
@@ -339,20 +372,35 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
339
372
|
) {
|
|
340
373
|
const drive = await this.getDrive(driveId);
|
|
341
374
|
|
|
342
|
-
const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
|
|
343
|
-
|
|
375
|
+
const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
|
|
376
|
+
driveId,
|
|
377
|
+
documentId,
|
|
378
|
+
scope,
|
|
379
|
+
branch,
|
|
380
|
+
documentType,
|
|
381
|
+
drive
|
|
382
|
+
);
|
|
383
|
+
const revisions = await this.storage.getSynchronizationUnitsRevision(
|
|
384
|
+
synchronizationUnitsQuery
|
|
385
|
+
);
|
|
344
386
|
|
|
345
|
-
const synchronizationUnits: SynchronizationUnit[] =
|
|
387
|
+
const synchronizationUnits: SynchronizationUnit[] =
|
|
388
|
+
synchronizationUnitsQuery.map(s => ({
|
|
389
|
+
...s,
|
|
390
|
+
lastUpdated: drive.created,
|
|
391
|
+
revision: -1
|
|
392
|
+
}));
|
|
346
393
|
for (const revision of revisions) {
|
|
347
|
-
const syncUnit = synchronizationUnits.find(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
394
|
+
const syncUnit = synchronizationUnits.find(
|
|
395
|
+
s =>
|
|
396
|
+
revision.driveId === s.driveId &&
|
|
397
|
+
revision.documentId === s.documentId &&
|
|
398
|
+
revision.scope === s.scope &&
|
|
399
|
+
revision.branch === s.branch
|
|
352
400
|
);
|
|
353
401
|
if (syncUnit) {
|
|
354
402
|
syncUnit.revision = revision.revision;
|
|
355
|
-
syncUnit.lastUpdated = revision.lastUpdated
|
|
403
|
+
syncUnit.lastUpdated = revision.lastUpdated;
|
|
356
404
|
}
|
|
357
405
|
}
|
|
358
406
|
return synchronizationUnits;
|
|
@@ -366,7 +414,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
366
414
|
documentType?: string[],
|
|
367
415
|
loadedDrive?: DocumentDriveDocument
|
|
368
416
|
): Promise<SynchronizationUnitQuery[]> {
|
|
369
|
-
const drive = loadedDrive ?? await this.getDrive(driveId);
|
|
417
|
+
const drive = loadedDrive ?? (await this.getDrive(driveId));
|
|
370
418
|
const nodes = drive.state.global.nodes.filter(
|
|
371
419
|
node =>
|
|
372
420
|
isFileNode(node) &&
|
|
@@ -400,31 +448,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
400
448
|
});
|
|
401
449
|
}
|
|
402
450
|
|
|
403
|
-
const synchronizationUnitsQuery: Omit<
|
|
451
|
+
const synchronizationUnitsQuery: Omit<
|
|
452
|
+
SynchronizationUnit,
|
|
453
|
+
'revision' | 'lastUpdated'
|
|
454
|
+
>[] = [];
|
|
404
455
|
for (const node of nodes) {
|
|
405
456
|
const nodeUnits =
|
|
406
457
|
scope?.length || branch?.length
|
|
407
458
|
? node.synchronizationUnits.filter(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
459
|
+
unit =>
|
|
460
|
+
(!scope?.length ||
|
|
461
|
+
scope.includes(unit.scope) ||
|
|
462
|
+
scope.includes('*')) &&
|
|
463
|
+
(!branch?.length ||
|
|
464
|
+
branch.includes(unit.branch) ||
|
|
465
|
+
branch.includes('*'))
|
|
466
|
+
)
|
|
416
467
|
: node.synchronizationUnits;
|
|
417
468
|
if (!nodeUnits.length) {
|
|
418
469
|
continue;
|
|
419
470
|
}
|
|
420
|
-
synchronizationUnitsQuery.push(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
471
|
+
synchronizationUnitsQuery.push(
|
|
472
|
+
...nodeUnits.map(n => ({
|
|
473
|
+
driveId,
|
|
474
|
+
documentId: node.id,
|
|
475
|
+
syncId: n.syncId,
|
|
476
|
+
documentType: node.documentType,
|
|
477
|
+
scope: n.scope,
|
|
478
|
+
branch: n.branch
|
|
479
|
+
}))
|
|
480
|
+
);
|
|
428
481
|
}
|
|
429
482
|
return synchronizationUnitsQuery;
|
|
430
483
|
}
|
|
@@ -458,7 +511,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
458
511
|
branch: syncUnit.branch,
|
|
459
512
|
driveId,
|
|
460
513
|
documentId: node.id,
|
|
461
|
-
documentType: node.documentType
|
|
514
|
+
documentType: node.documentType
|
|
462
515
|
};
|
|
463
516
|
}
|
|
464
517
|
|
|
@@ -466,7 +519,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
466
519
|
driveId: string,
|
|
467
520
|
syncId: string
|
|
468
521
|
): Promise<SynchronizationUnit | undefined> {
|
|
469
|
-
const syncUnit = await this.getSynchronizationUnitIdInfo(
|
|
522
|
+
const syncUnit = await this.getSynchronizationUnitIdInfo(
|
|
523
|
+
driveId,
|
|
524
|
+
syncId
|
|
525
|
+
);
|
|
470
526
|
|
|
471
527
|
if (!syncUnit) {
|
|
472
528
|
return undefined;
|
|
@@ -513,7 +569,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
513
569
|
? await this.getDrive(driveId)
|
|
514
570
|
: await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
|
|
515
571
|
|
|
516
|
-
const operations =
|
|
572
|
+
const operations =
|
|
573
|
+
document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
|
|
517
574
|
const filteredOperations = operations.filter(
|
|
518
575
|
operation =>
|
|
519
576
|
Object.keys(filter).length === 0 ||
|
|
@@ -563,7 +620,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
563
620
|
await this.storage.createDrive(id, document);
|
|
564
621
|
|
|
565
622
|
if (drive.global.slug) {
|
|
566
|
-
await this.cache.deleteDocument(
|
|
623
|
+
await this.cache.deleteDocument('drives-slug', drive.global.slug);
|
|
567
624
|
}
|
|
568
625
|
|
|
569
626
|
await this._initializeDrive(id);
|
|
@@ -571,7 +628,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
571
628
|
return document;
|
|
572
629
|
}
|
|
573
630
|
|
|
574
|
-
async addRemoteDrive(
|
|
631
|
+
async addRemoteDrive(
|
|
632
|
+
url: string,
|
|
633
|
+
options: RemoteDriveOptions
|
|
634
|
+
): Promise<DocumentDriveDocument> {
|
|
575
635
|
const { id, name, slug, icon } = await requestPublicDrive(url);
|
|
576
636
|
const {
|
|
577
637
|
pullFilter,
|
|
@@ -613,7 +673,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
613
673
|
]);
|
|
614
674
|
|
|
615
675
|
result.forEach(r => {
|
|
616
|
-
if (r.status ===
|
|
676
|
+
if (r.status === 'rejected') {
|
|
617
677
|
throw r.reason;
|
|
618
678
|
}
|
|
619
679
|
});
|
|
@@ -625,7 +685,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
625
685
|
|
|
626
686
|
async getDrive(drive: string, options?: GetDocumentOptions) {
|
|
627
687
|
try {
|
|
628
|
-
const document = await this.cache.getDocument('drives', drive);
|
|
688
|
+
const document = await this.cache.getDocument('drives', drive); // TODO support GetDocumentOptions
|
|
629
689
|
if (document && isDocumentDrive(document)) {
|
|
630
690
|
return document;
|
|
631
691
|
}
|
|
@@ -672,16 +732,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
672
732
|
|
|
673
733
|
async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
|
|
674
734
|
try {
|
|
675
|
-
const document = await this.cache.getDocument(drive, id);
|
|
735
|
+
const document = await this.cache.getDocument(drive, id); // TODO support GetDocumentOptions
|
|
676
736
|
if (document) {
|
|
677
737
|
return document;
|
|
678
738
|
}
|
|
679
739
|
} catch (e) {
|
|
680
740
|
logger.error('Error getting document from cache', e);
|
|
681
741
|
}
|
|
682
|
-
const documentStorage =
|
|
683
|
-
|
|
684
|
-
const document = this._buildDocument(documentStorage, options)
|
|
742
|
+
const documentStorage = await this.storage.getDocument(drive, id);
|
|
743
|
+
const document = this._buildDocument(documentStorage, options);
|
|
685
744
|
|
|
686
745
|
this.cache.setDocument(drive, id, document).catch(logger.error);
|
|
687
746
|
return document;
|
|
@@ -699,14 +758,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
699
758
|
let state = undefined;
|
|
700
759
|
if (input.document) {
|
|
701
760
|
if (input.documentType !== input.document.documentType) {
|
|
702
|
-
throw new Error(
|
|
761
|
+
throw new Error(
|
|
762
|
+
`Provided document is not ${input.documentType}`
|
|
763
|
+
);
|
|
703
764
|
}
|
|
704
765
|
const doc = this._buildDocument(input.document);
|
|
705
766
|
state = doc.state;
|
|
706
767
|
}
|
|
707
768
|
|
|
708
769
|
// if no document was provided then create a new one
|
|
709
|
-
const document =
|
|
770
|
+
const document =
|
|
771
|
+
input.document ??
|
|
710
772
|
this._getDocumentModel(input.documentType).utils.createDocument();
|
|
711
773
|
|
|
712
774
|
// stores document information
|
|
@@ -728,9 +790,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
728
790
|
const operations = Object.values(document.operations).flat();
|
|
729
791
|
if (operations.length) {
|
|
730
792
|
if (isDocumentDrive(document)) {
|
|
731
|
-
await this.storage.addDriveOperations(
|
|
793
|
+
await this.storage.addDriveOperations(
|
|
794
|
+
driveId,
|
|
795
|
+
operations as Operation<DocumentDriveAction>[],
|
|
796
|
+
document
|
|
797
|
+
);
|
|
732
798
|
} else {
|
|
733
|
-
await this.storage.addDocumentOperations(
|
|
799
|
+
await this.storage.addDocumentOperations(
|
|
800
|
+
driveId,
|
|
801
|
+
input.id,
|
|
802
|
+
operations,
|
|
803
|
+
document
|
|
804
|
+
);
|
|
734
805
|
}
|
|
735
806
|
}
|
|
736
807
|
|
|
@@ -739,7 +810,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
739
810
|
|
|
740
811
|
async deleteDocument(driveId: string, id: string) {
|
|
741
812
|
try {
|
|
742
|
-
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
|
|
813
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
|
|
814
|
+
id
|
|
815
|
+
]);
|
|
743
816
|
await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
|
|
744
817
|
} catch (error) {
|
|
745
818
|
logger.warn('Error deleting document', error);
|
|
@@ -788,7 +861,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
788
861
|
: merge(trunk, invertedTrunk, reshuffleByTimestamp);
|
|
789
862
|
|
|
790
863
|
const newOperations = newHistory.filter(
|
|
791
|
-
|
|
864
|
+
op => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
|
|
792
865
|
);
|
|
793
866
|
|
|
794
867
|
for (const nextOperation of newOperations) {
|
|
@@ -823,11 +896,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
823
896
|
e instanceof OperationError
|
|
824
897
|
? e
|
|
825
898
|
: new OperationError(
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
899
|
+
'ERROR',
|
|
900
|
+
nextOperation,
|
|
901
|
+
(e as Error).message,
|
|
902
|
+
(e as Error).cause
|
|
903
|
+
);
|
|
831
904
|
|
|
832
905
|
// TODO: don't break on errors...
|
|
833
906
|
break;
|
|
@@ -839,14 +912,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
839
912
|
document,
|
|
840
913
|
operationsApplied,
|
|
841
914
|
signals,
|
|
842
|
-
error
|
|
915
|
+
error
|
|
843
916
|
} as const;
|
|
844
917
|
}
|
|
845
918
|
|
|
846
919
|
private _buildDocument<T extends Document>(
|
|
847
|
-
documentStorage: DocumentStorage<T>,
|
|
920
|
+
documentStorage: DocumentStorage<T>,
|
|
921
|
+
options?: GetDocumentOptions
|
|
848
922
|
): T {
|
|
849
|
-
if (
|
|
923
|
+
if (
|
|
924
|
+
documentStorage.state &&
|
|
925
|
+
(!options || options.checkHashes === false)
|
|
926
|
+
) {
|
|
850
927
|
return documentStorage as T;
|
|
851
928
|
}
|
|
852
929
|
|
|
@@ -854,11 +931,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
854
931
|
documentStorage.documentType
|
|
855
932
|
);
|
|
856
933
|
|
|
857
|
-
const revisionOperations =
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
934
|
+
const revisionOperations =
|
|
935
|
+
options?.revisions !== undefined
|
|
936
|
+
? filterOperationsByRevision(
|
|
937
|
+
documentStorage.operations,
|
|
938
|
+
options.revisions
|
|
939
|
+
)
|
|
940
|
+
: documentStorage.operations;
|
|
941
|
+
const operations =
|
|
942
|
+
baseUtils.documentHelpers.garbageCollectDocumentOperations(
|
|
943
|
+
revisionOperations
|
|
944
|
+
);
|
|
862
945
|
|
|
863
946
|
return baseUtils.replayDocument(
|
|
864
947
|
documentStorage.initialState,
|
|
@@ -888,22 +971,33 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
888
971
|
let newDocument = document;
|
|
889
972
|
|
|
890
973
|
const scope = operation.scope;
|
|
891
|
-
const documentOperations =
|
|
892
|
-
{
|
|
974
|
+
const documentOperations =
|
|
975
|
+
DocumentUtils.documentHelpers.garbageCollectDocumentOperations({
|
|
893
976
|
...document.operations,
|
|
894
977
|
[scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
|
|
895
978
|
document.operations[scope],
|
|
896
|
-
operation
|
|
897
|
-
)
|
|
898
|
-
}
|
|
899
|
-
);
|
|
979
|
+
operation
|
|
980
|
+
)
|
|
981
|
+
});
|
|
900
982
|
|
|
901
983
|
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
902
984
|
// if the latest operation doesn't have a resulting state then tries
|
|
903
985
|
// to retrieve it from the db to avoid rerunning all the operations
|
|
904
986
|
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
905
|
-
lastRemainingOperation.resultingState = await (id
|
|
906
|
-
this.storage.
|
|
987
|
+
lastRemainingOperation.resultingState = await (id
|
|
988
|
+
? this.storage.getOperationResultingState?.(
|
|
989
|
+
drive,
|
|
990
|
+
id,
|
|
991
|
+
lastRemainingOperation.index,
|
|
992
|
+
lastRemainingOperation.scope,
|
|
993
|
+
'main'
|
|
994
|
+
)
|
|
995
|
+
: this.storage.getDriveOperationResultingState?.(
|
|
996
|
+
drive,
|
|
997
|
+
lastRemainingOperation.index,
|
|
998
|
+
lastRemainingOperation.scope,
|
|
999
|
+
'main'
|
|
1000
|
+
));
|
|
907
1001
|
}
|
|
908
1002
|
|
|
909
1003
|
const operationSignals: (() => Promise<SignalResult>)[] = [];
|
|
@@ -959,10 +1053,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
959
1053
|
appliedOperation[0]!.hash !== operation.hash &&
|
|
960
1054
|
!skipHashValidation
|
|
961
1055
|
) {
|
|
962
|
-
throw new ConflictOperationError(
|
|
963
|
-
operation,
|
|
964
|
-
appliedOperation[0]!
|
|
965
|
-
);
|
|
1056
|
+
throw new ConflictOperationError(operation, appliedOperation[0]!);
|
|
966
1057
|
}
|
|
967
1058
|
|
|
968
1059
|
for (const signalHandler of operationSignals) {
|
|
@@ -977,7 +1068,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
977
1068
|
};
|
|
978
1069
|
}
|
|
979
1070
|
|
|
980
|
-
addOperation(
|
|
1071
|
+
addOperation(
|
|
1072
|
+
drive: string,
|
|
1073
|
+
id: string,
|
|
1074
|
+
operation: Operation,
|
|
1075
|
+
forceSync = true
|
|
1076
|
+
): Promise<IOperationResult> {
|
|
981
1077
|
return this.addOperations(drive, id, [operation], forceSync);
|
|
982
1078
|
}
|
|
983
1079
|
|
|
@@ -993,14 +1089,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
993
1089
|
const documentStorage = await this.storage.getDocument(drive, id);
|
|
994
1090
|
const result = await callback(documentStorage);
|
|
995
1091
|
// saves the applied operations to storage
|
|
996
|
-
if (
|
|
997
|
-
result.operations.length > 0
|
|
998
|
-
) {
|
|
1092
|
+
if (result.operations.length > 0) {
|
|
999
1093
|
await this.storage.addDocumentOperations(
|
|
1000
1094
|
drive,
|
|
1001
1095
|
id,
|
|
1002
1096
|
result.operations,
|
|
1003
|
-
result.header
|
|
1097
|
+
result.header
|
|
1004
1098
|
);
|
|
1005
1099
|
}
|
|
1006
1100
|
} else {
|
|
@@ -1012,93 +1106,196 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1012
1106
|
}
|
|
1013
1107
|
}
|
|
1014
1108
|
|
|
1015
|
-
queueOperation(
|
|
1109
|
+
queueOperation(
|
|
1110
|
+
drive: string,
|
|
1111
|
+
id: string,
|
|
1112
|
+
operation: Operation,
|
|
1113
|
+
forceSync = true
|
|
1114
|
+
): Promise<IOperationResult> {
|
|
1016
1115
|
return this.queueOperations(drive, id, [operation], forceSync);
|
|
1017
1116
|
}
|
|
1018
1117
|
|
|
1019
|
-
async
|
|
1118
|
+
private async resultIfExistingOperations(
|
|
1119
|
+
drive: string,
|
|
1020
1120
|
id: string,
|
|
1021
|
-
operations: Operation[]
|
|
1022
|
-
|
|
1121
|
+
operations: Operation[]
|
|
1122
|
+
): Promise<IOperationResult | undefined> {
|
|
1123
|
+
try {
|
|
1124
|
+
const document = await this.getDocument(drive, id);
|
|
1125
|
+
const newOperation = operations.find(
|
|
1126
|
+
op =>
|
|
1127
|
+
!op.id ||
|
|
1128
|
+
!document.operations[op.scope].find(
|
|
1129
|
+
existingOp =>
|
|
1130
|
+
existingOp.id === op.id &&
|
|
1131
|
+
existingOp.index === op.index &&
|
|
1132
|
+
existingOp.type === op.type &&
|
|
1133
|
+
existingOp.hash === op.hash
|
|
1134
|
+
)
|
|
1135
|
+
);
|
|
1136
|
+
if (!newOperation) {
|
|
1137
|
+
return {
|
|
1138
|
+
status: 'SUCCESS',
|
|
1139
|
+
document,
|
|
1140
|
+
operations,
|
|
1141
|
+
signals: []
|
|
1142
|
+
};
|
|
1143
|
+
} else {
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
console.error(error); // TODO error
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1023
1151
|
|
|
1152
|
+
async queueOperations(
|
|
1153
|
+
drive: string,
|
|
1154
|
+
id: string,
|
|
1155
|
+
operations: Operation[],
|
|
1156
|
+
forceSync = true
|
|
1157
|
+
) {
|
|
1158
|
+
// if operations are already stored then returns cached document
|
|
1159
|
+
const result = await this.resultIfExistingOperations(
|
|
1160
|
+
drive,
|
|
1161
|
+
id,
|
|
1162
|
+
operations
|
|
1163
|
+
);
|
|
1164
|
+
if (result) {
|
|
1165
|
+
console.log('Duplicated operations!');
|
|
1166
|
+
return result;
|
|
1167
|
+
}
|
|
1024
1168
|
try {
|
|
1025
|
-
const jobId = await this.queueManager.addJob({
|
|
1169
|
+
const jobId = await this.queueManager.addJob({
|
|
1170
|
+
driveId: drive,
|
|
1171
|
+
documentId: id,
|
|
1172
|
+
operations,
|
|
1173
|
+
forceSync
|
|
1174
|
+
});
|
|
1026
1175
|
|
|
1027
1176
|
return new Promise<IOperationResult>((resolve, reject) => {
|
|
1028
|
-
const unsubscribe = this.queueManager.on(
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1177
|
+
const unsubscribe = this.queueManager.on(
|
|
1178
|
+
'jobCompleted',
|
|
1179
|
+
(job, result) => {
|
|
1180
|
+
if (job.jobId === jobId) {
|
|
1181
|
+
unsubscribe();
|
|
1182
|
+
unsubscribeError();
|
|
1183
|
+
resolve(result);
|
|
1184
|
+
}
|
|
1033
1185
|
}
|
|
1034
|
-
|
|
1035
|
-
const unsubscribeError = this.queueManager.on(
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1186
|
+
);
|
|
1187
|
+
const unsubscribeError = this.queueManager.on(
|
|
1188
|
+
'jobFailed',
|
|
1189
|
+
(job, error) => {
|
|
1190
|
+
if (job.jobId === jobId) {
|
|
1191
|
+
unsubscribe();
|
|
1192
|
+
unsubscribeError();
|
|
1193
|
+
reject(error);
|
|
1194
|
+
}
|
|
1040
1195
|
}
|
|
1041
|
-
|
|
1042
|
-
})
|
|
1196
|
+
);
|
|
1197
|
+
});
|
|
1043
1198
|
} catch (error) {
|
|
1044
1199
|
logger.error('Error adding job', error);
|
|
1045
1200
|
throw error;
|
|
1046
1201
|
}
|
|
1047
1202
|
}
|
|
1048
1203
|
|
|
1049
|
-
async queueAction(
|
|
1204
|
+
async queueAction(
|
|
1205
|
+
drive: string,
|
|
1206
|
+
id: string,
|
|
1207
|
+
action: Action,
|
|
1208
|
+
forceSync?: boolean | undefined
|
|
1209
|
+
): Promise<IOperationResult> {
|
|
1050
1210
|
return this.queueActions(drive, id, [action], forceSync);
|
|
1051
1211
|
}
|
|
1052
1212
|
|
|
1053
|
-
async queueActions(
|
|
1213
|
+
async queueActions(
|
|
1214
|
+
drive: string,
|
|
1215
|
+
id: string,
|
|
1216
|
+
actions: Action[],
|
|
1217
|
+
forceSync?: boolean | undefined
|
|
1218
|
+
): Promise<IOperationResult> {
|
|
1054
1219
|
try {
|
|
1055
|
-
const jobId = await this.queueManager.addJob({
|
|
1220
|
+
const jobId = await this.queueManager.addJob({
|
|
1221
|
+
driveId: drive,
|
|
1222
|
+
documentId: id,
|
|
1223
|
+
actions,
|
|
1224
|
+
forceSync
|
|
1225
|
+
});
|
|
1056
1226
|
|
|
1057
1227
|
return new Promise<IOperationResult>((resolve, reject) => {
|
|
1058
|
-
const unsubscribe = this.queueManager.on(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1228
|
+
const unsubscribe = this.queueManager.on(
|
|
1229
|
+
'jobCompleted',
|
|
1230
|
+
(job, result) => {
|
|
1231
|
+
if (job.jobId === jobId) {
|
|
1232
|
+
unsubscribe();
|
|
1233
|
+
unsubscribeError();
|
|
1234
|
+
resolve(result);
|
|
1235
|
+
}
|
|
1063
1236
|
}
|
|
1064
|
-
|
|
1065
|
-
const unsubscribeError = this.queueManager.on(
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1237
|
+
);
|
|
1238
|
+
const unsubscribeError = this.queueManager.on(
|
|
1239
|
+
'jobFailed',
|
|
1240
|
+
(job, error) => {
|
|
1241
|
+
if (job.jobId === jobId) {
|
|
1242
|
+
unsubscribe();
|
|
1243
|
+
unsubscribeError();
|
|
1244
|
+
reject(error);
|
|
1245
|
+
}
|
|
1070
1246
|
}
|
|
1071
|
-
|
|
1072
|
-
})
|
|
1247
|
+
);
|
|
1248
|
+
});
|
|
1073
1249
|
} catch (error) {
|
|
1074
1250
|
logger.error('Error adding job', error);
|
|
1075
1251
|
throw error;
|
|
1076
1252
|
}
|
|
1077
1253
|
}
|
|
1078
1254
|
|
|
1079
|
-
async queueDriveAction(
|
|
1255
|
+
async queueDriveAction(
|
|
1256
|
+
drive: string,
|
|
1257
|
+
action: DocumentDriveAction | BaseAction,
|
|
1258
|
+
forceSync?: boolean | undefined
|
|
1259
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1080
1260
|
return this.queueDriveActions(drive, [action], forceSync);
|
|
1081
1261
|
}
|
|
1082
1262
|
|
|
1083
|
-
async queueDriveActions(
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1263
|
+
async queueDriveActions(
|
|
1264
|
+
drive: string,
|
|
1265
|
+
actions: (DocumentDriveAction | BaseAction)[],
|
|
1266
|
+
forceSync?: boolean | undefined
|
|
1267
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1268
|
+
const jobId = await this.queueManager.addJob({
|
|
1269
|
+
driveId: drive,
|
|
1270
|
+
actions,
|
|
1271
|
+
forceSync
|
|
1272
|
+
});
|
|
1273
|
+
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1274
|
+
(resolve, reject) => {
|
|
1275
|
+
const unsubscribe = this.queueManager.on(
|
|
1276
|
+
'jobCompleted',
|
|
1277
|
+
(job, result) => {
|
|
1278
|
+
if (job.jobId === jobId) {
|
|
1279
|
+
unsubscribe();
|
|
1280
|
+
unsubscribeError();
|
|
1281
|
+
resolve(
|
|
1282
|
+
result as IOperationResult<DocumentDriveDocument>
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
const unsubscribeError = this.queueManager.on(
|
|
1288
|
+
'jobFailed',
|
|
1289
|
+
(job, error) => {
|
|
1290
|
+
if (job.jobId === jobId) {
|
|
1291
|
+
unsubscribe();
|
|
1292
|
+
unsubscribeError();
|
|
1293
|
+
reject(error);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
);
|
|
1102
1299
|
}
|
|
1103
1300
|
|
|
1104
1301
|
async addOperations(
|
|
@@ -1107,6 +1304,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1107
1304
|
operations: Operation[],
|
|
1108
1305
|
forceSync = true
|
|
1109
1306
|
) {
|
|
1307
|
+
// if operations are already stored then returns the result
|
|
1308
|
+
const result = await this.resultIfExistingOperations(
|
|
1309
|
+
drive,
|
|
1310
|
+
id,
|
|
1311
|
+
operations
|
|
1312
|
+
);
|
|
1313
|
+
if (result) {
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1110
1316
|
let document: Document | undefined;
|
|
1111
1317
|
const operationsApplied: Operation[] = [];
|
|
1112
1318
|
const signals: SignalResult[] = [];
|
|
@@ -1214,11 +1420,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1214
1420
|
error instanceof OperationError
|
|
1215
1421
|
? error
|
|
1216
1422
|
: new OperationError(
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1423
|
+
'ERROR',
|
|
1424
|
+
undefined,
|
|
1425
|
+
(error as Error).message,
|
|
1426
|
+
(error as Error).cause
|
|
1427
|
+
);
|
|
1222
1428
|
|
|
1223
1429
|
return {
|
|
1224
1430
|
status: operationError.status,
|
|
@@ -1273,33 +1479,92 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1273
1479
|
}
|
|
1274
1480
|
}
|
|
1275
1481
|
|
|
1276
|
-
queueDriveOperation(
|
|
1482
|
+
queueDriveOperation(
|
|
1483
|
+
drive: string,
|
|
1484
|
+
operation: Operation<DocumentDriveAction | BaseAction>,
|
|
1485
|
+
forceSync = true
|
|
1486
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1277
1487
|
return this.queueDriveOperations(drive, [operation], forceSync);
|
|
1278
1488
|
}
|
|
1279
1489
|
|
|
1490
|
+
private async resultIfExistingDriveOperations(
|
|
1491
|
+
driveId: string,
|
|
1492
|
+
operations: Operation<DocumentDriveAction | BaseAction>[]
|
|
1493
|
+
): Promise<IOperationResult<DocumentDriveDocument> | undefined> {
|
|
1494
|
+
try {
|
|
1495
|
+
const drive = await this.getDrive(driveId);
|
|
1496
|
+
const newOperation = operations.find(
|
|
1497
|
+
op =>
|
|
1498
|
+
!op.id ||
|
|
1499
|
+
!drive.operations[op.scope].find(
|
|
1500
|
+
existingOp =>
|
|
1501
|
+
existingOp.id === op.id &&
|
|
1502
|
+
existingOp.index === op.index &&
|
|
1503
|
+
existingOp.type === op.type &&
|
|
1504
|
+
existingOp.hash === op.hash
|
|
1505
|
+
)
|
|
1506
|
+
);
|
|
1507
|
+
if (!newOperation) {
|
|
1508
|
+
return {
|
|
1509
|
+
status: 'SUCCESS',
|
|
1510
|
+
document: drive,
|
|
1511
|
+
operations: operations,
|
|
1512
|
+
signals: []
|
|
1513
|
+
} as IOperationResult<DocumentDriveDocument>;
|
|
1514
|
+
} else {
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
console.error(error); // TODO error
|
|
1519
|
+
return undefined;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1280
1523
|
async queueDriveOperations(
|
|
1281
1524
|
drive: string,
|
|
1282
1525
|
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
1283
1526
|
forceSync = true
|
|
1284
1527
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
});
|
|
1294
|
-
const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
|
|
1295
|
-
if (job.jobId === jobId) {
|
|
1296
|
-
unsubscribe();
|
|
1297
|
-
unsubscribeError();
|
|
1298
|
-
reject(error);
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1528
|
+
// if operations are already stored then returns cached document
|
|
1529
|
+
const result = await this.resultIfExistingDriveOperations(
|
|
1530
|
+
drive,
|
|
1531
|
+
operations
|
|
1532
|
+
);
|
|
1533
|
+
if (result) {
|
|
1534
|
+
return result;
|
|
1535
|
+
}
|
|
1301
1536
|
|
|
1302
|
-
|
|
1537
|
+
const jobId = await this.queueManager.addJob({
|
|
1538
|
+
driveId: drive,
|
|
1539
|
+
operations,
|
|
1540
|
+
forceSync
|
|
1541
|
+
});
|
|
1542
|
+
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1543
|
+
(resolve, reject) => {
|
|
1544
|
+
const unsubscribe = this.queueManager.on(
|
|
1545
|
+
'jobCompleted',
|
|
1546
|
+
(job, result) => {
|
|
1547
|
+
if (job.jobId === jobId) {
|
|
1548
|
+
unsubscribe();
|
|
1549
|
+
unsubscribeError();
|
|
1550
|
+
resolve(
|
|
1551
|
+
result as IOperationResult<DocumentDriveDocument>
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
);
|
|
1556
|
+
const unsubscribeError = this.queueManager.on(
|
|
1557
|
+
'jobFailed',
|
|
1558
|
+
(job, error) => {
|
|
1559
|
+
if (job.jobId === jobId) {
|
|
1560
|
+
unsubscribe();
|
|
1561
|
+
unsubscribeError();
|
|
1562
|
+
reject(error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
);
|
|
1303
1568
|
}
|
|
1304
1569
|
|
|
1305
1570
|
async addDriveOperations(
|
|
@@ -1313,6 +1578,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1313
1578
|
const signals: SignalResult[] = [];
|
|
1314
1579
|
let error: Error | undefined;
|
|
1315
1580
|
|
|
1581
|
+
// if operations are already stored then returns cached drive
|
|
1582
|
+
const result = await this.resultIfExistingDriveOperations(
|
|
1583
|
+
drive,
|
|
1584
|
+
operations
|
|
1585
|
+
);
|
|
1586
|
+
if (result) {
|
|
1587
|
+
return result;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1316
1590
|
const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1317
1591
|
|
|
1318
1592
|
try {
|
|
@@ -1329,7 +1603,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1329
1603
|
|
|
1330
1604
|
return {
|
|
1331
1605
|
operations: result.operationsApplied,
|
|
1332
|
-
header: result.document
|
|
1606
|
+
header: result.document
|
|
1333
1607
|
};
|
|
1334
1608
|
});
|
|
1335
1609
|
|
|
@@ -1457,11 +1731,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1457
1731
|
error instanceof OperationError
|
|
1458
1732
|
? error
|
|
1459
1733
|
: new OperationError(
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1734
|
+
'ERROR',
|
|
1735
|
+
undefined,
|
|
1736
|
+
(error as Error).message,
|
|
1737
|
+
(error as Error).cause
|
|
1738
|
+
);
|
|
1465
1739
|
|
|
1466
1740
|
return {
|
|
1467
1741
|
status: operationError.status,
|
|
@@ -1525,7 +1799,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1525
1799
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1526
1800
|
const document = await this.getDrive(drive);
|
|
1527
1801
|
const operations = this._buildOperations(document, actions);
|
|
1528
|
-
const result = await this.addDriveOperations(
|
|
1802
|
+
const result = await this.addDriveOperations(
|
|
1803
|
+
drive,
|
|
1804
|
+
operations,
|
|
1805
|
+
forceSync
|
|
1806
|
+
);
|
|
1529
1807
|
return result;
|
|
1530
1808
|
}
|
|
1531
1809
|
|
package/src/storage/prisma.ts
CHANGED
|
@@ -8,32 +8,37 @@ import {
|
|
|
8
8
|
import type {
|
|
9
9
|
Action,
|
|
10
10
|
AttachmentInput,
|
|
11
|
-
DocumentOperations,
|
|
12
|
-
FileRegistry,
|
|
13
11
|
BaseAction,
|
|
14
12
|
Document,
|
|
15
13
|
DocumentHeader,
|
|
14
|
+
DocumentOperations,
|
|
16
15
|
ExtendedState,
|
|
16
|
+
FileRegistry,
|
|
17
17
|
Operation,
|
|
18
18
|
OperationScope,
|
|
19
19
|
State
|
|
20
20
|
} from 'document-model/document';
|
|
21
21
|
import { IBackOffOptions, backOff } from 'exponential-backoff';
|
|
22
22
|
import { ConflictOperationError } from '../server/error';
|
|
23
|
-
import { logger } from '../utils/logger';
|
|
24
|
-
import { DocumentDriveStorage, DocumentStorage, IDriveStorage, IStorageDelegate } from './types';
|
|
25
23
|
import type { SynchronizationUnitQuery } from '../server/types';
|
|
24
|
+
import { logger } from '../utils/logger';
|
|
25
|
+
import {
|
|
26
|
+
DocumentDriveStorage,
|
|
27
|
+
DocumentStorage,
|
|
28
|
+
IDriveStorage,
|
|
29
|
+
IStorageDelegate
|
|
30
|
+
} from './types';
|
|
26
31
|
|
|
27
32
|
type Transaction =
|
|
28
33
|
| Omit<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
PrismaClient<Prisma.PrismaClientOptions, never>,
|
|
35
|
+
| '$connect'
|
|
36
|
+
| '$disconnect'
|
|
37
|
+
| '$on'
|
|
38
|
+
| '$transaction'
|
|
39
|
+
| '$use'
|
|
40
|
+
| '$extends'
|
|
41
|
+
>
|
|
37
42
|
| ExtendedPrismaClient;
|
|
38
43
|
|
|
39
44
|
function storageToOperation(
|
|
@@ -42,6 +47,7 @@ function storageToOperation(
|
|
|
42
47
|
}
|
|
43
48
|
): Operation {
|
|
44
49
|
const operation: Operation = {
|
|
50
|
+
id: op.opId || undefined,
|
|
45
51
|
skip: op.skip,
|
|
46
52
|
hash: op.hash,
|
|
47
53
|
index: op.index,
|
|
@@ -187,6 +193,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
187
193
|
type: op.type,
|
|
188
194
|
scope: op.scope,
|
|
189
195
|
branch: 'main',
|
|
196
|
+
opId: op.id,
|
|
190
197
|
skip: op.skip,
|
|
191
198
|
context: op.context,
|
|
192
199
|
resultingState: op.resultingState
|
|
@@ -371,26 +378,41 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
371
378
|
throw new Error(`Document with id ${id} not found`);
|
|
372
379
|
}
|
|
373
380
|
|
|
374
|
-
const cachedOperations = await this.delegate?.getCachedOperations(
|
|
381
|
+
const cachedOperations = (await this.delegate?.getCachedOperations(
|
|
382
|
+
driveId,
|
|
383
|
+
id
|
|
384
|
+
)) ?? {
|
|
375
385
|
global: [],
|
|
376
386
|
local: []
|
|
377
387
|
};
|
|
378
|
-
const scopeIndex = Object.keys(cachedOperations).reduceRight<
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
acc
|
|
382
|
-
|
|
383
|
-
|
|
388
|
+
const scopeIndex = Object.keys(cachedOperations).reduceRight<
|
|
389
|
+
Record<OperationScope, number>
|
|
390
|
+
>(
|
|
391
|
+
(acc, value) => {
|
|
392
|
+
const scope = value as OperationScope;
|
|
393
|
+
const lastIndex = cachedOperations[scope]?.at(-1)?.index ?? -1;
|
|
394
|
+
acc[scope] = lastIndex;
|
|
395
|
+
return acc;
|
|
396
|
+
},
|
|
397
|
+
{ global: -1, local: -1 }
|
|
398
|
+
);
|
|
384
399
|
|
|
385
|
-
const conditions = Object.entries(scopeIndex).map(
|
|
386
|
-
|
|
400
|
+
const conditions = Object.entries(scopeIndex).map(
|
|
401
|
+
([scope, index]) => `("scope" = '${scope}' AND "index" > ${index})`
|
|
402
|
+
);
|
|
403
|
+
conditions.push(
|
|
404
|
+
`("scope" NOT IN (${Object.keys(cachedOperations)
|
|
405
|
+
.map(s => `'${s}'`)
|
|
406
|
+
.join(', ')}))`
|
|
407
|
+
);
|
|
387
408
|
|
|
388
409
|
// retrieves operations with resulting state
|
|
389
410
|
// for the last operation of each scope
|
|
390
411
|
// TODO prevent SQL injection
|
|
391
412
|
const queryOperations = await prisma.$queryRawUnsafe<
|
|
392
413
|
Prisma.$OperationPayload['scalars'][]
|
|
393
|
-
>(
|
|
414
|
+
>(
|
|
415
|
+
`WITH ranked_operations AS (
|
|
394
416
|
SELECT
|
|
395
417
|
*,
|
|
396
418
|
ROW_NUMBER() OVER (PARTITION BY scope ORDER BY index DESC) AS rn
|
|
@@ -416,37 +438,39 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
416
438
|
WHERE "driveId" = $1 AND "documentId" = $2
|
|
417
439
|
AND (${conditions.join(' OR ')})
|
|
418
440
|
ORDER BY scope, index;
|
|
419
|
-
`,
|
|
420
|
-
|
|
441
|
+
`,
|
|
442
|
+
driveId,
|
|
443
|
+
id
|
|
444
|
+
);
|
|
445
|
+
const operationIds = queryOperations.map(o => o.id);
|
|
421
446
|
const attachments = await prisma.attachment.findMany({
|
|
422
447
|
where: {
|
|
423
448
|
operationId: {
|
|
424
449
|
in: operationIds
|
|
425
450
|
}
|
|
426
|
-
}
|
|
451
|
+
}
|
|
427
452
|
});
|
|
428
453
|
|
|
429
454
|
// TODO add attachments from cached operations
|
|
430
455
|
const fileRegistry: FileRegistry = {};
|
|
431
456
|
|
|
432
|
-
const operationsByScope = queryOperations.reduce<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
);
|
|
457
|
+
const operationsByScope = queryOperations.reduce<
|
|
458
|
+
DocumentOperations<Action>
|
|
459
|
+
>((acc, operation) => {
|
|
460
|
+
const scope = operation.scope as OperationScope;
|
|
461
|
+
if (!acc[scope]) {
|
|
462
|
+
acc[scope] = [];
|
|
463
|
+
}
|
|
464
|
+
const result = storageToOperation(operation);
|
|
465
|
+
result.attachments = attachments.filter(
|
|
466
|
+
a => a.operationId === operation.id
|
|
467
|
+
);
|
|
468
|
+
result.attachments.forEach(({ hash, ...file }) => {
|
|
469
|
+
fileRegistry[hash] = file;
|
|
470
|
+
});
|
|
471
|
+
acc[scope].push(result);
|
|
472
|
+
return acc;
|
|
473
|
+
}, cachedOperations);
|
|
450
474
|
|
|
451
475
|
const dbDoc = result;
|
|
452
476
|
const doc: Document = {
|
|
@@ -529,13 +553,19 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
529
553
|
where: {
|
|
530
554
|
id
|
|
531
555
|
}
|
|
532
|
-
})
|
|
556
|
+
});
|
|
533
557
|
|
|
534
558
|
// delete drive itself
|
|
535
559
|
await this.deleteDocument('drives', id);
|
|
536
560
|
}
|
|
537
561
|
|
|
538
|
-
async getOperationResultingState(
|
|
562
|
+
async getOperationResultingState(
|
|
563
|
+
driveId: string,
|
|
564
|
+
documentId: string,
|
|
565
|
+
index: number,
|
|
566
|
+
scope: string,
|
|
567
|
+
branch: string
|
|
568
|
+
): Promise<unknown> {
|
|
539
569
|
const operation = await this.db.operation.findUnique({
|
|
540
570
|
where: {
|
|
541
571
|
unique_operation: {
|
|
@@ -550,8 +580,19 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
550
580
|
return operation?.resultingState?.toString();
|
|
551
581
|
}
|
|
552
582
|
|
|
553
|
-
getDriveOperationResultingState(
|
|
554
|
-
|
|
583
|
+
getDriveOperationResultingState(
|
|
584
|
+
drive: string,
|
|
585
|
+
index: number,
|
|
586
|
+
scope: string,
|
|
587
|
+
branch: string
|
|
588
|
+
): Promise<unknown> {
|
|
589
|
+
return this.getOperationResultingState(
|
|
590
|
+
'drives',
|
|
591
|
+
drive,
|
|
592
|
+
index,
|
|
593
|
+
scope,
|
|
594
|
+
branch
|
|
595
|
+
);
|
|
555
596
|
}
|
|
556
597
|
|
|
557
598
|
async getSynchronizationUnitsRevision(
|
|
@@ -567,9 +608,11 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
567
608
|
}[]
|
|
568
609
|
> {
|
|
569
610
|
// TODO add branch condition
|
|
570
|
-
const whereClauses = units
|
|
571
|
-
|
|
572
|
-
|
|
611
|
+
const whereClauses = units
|
|
612
|
+
.map((_, index) => {
|
|
613
|
+
return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
|
|
614
|
+
})
|
|
615
|
+
.join(' OR ');
|
|
573
616
|
|
|
574
617
|
const query = `
|
|
575
618
|
SELECT "driveId", "documentId", "scope", "branch", MAX("timestamp") as "lastUpdated", MAX("index") as revision FROM "Operation"
|
|
@@ -577,13 +620,28 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
577
620
|
GROUP BY "driveId", "documentId", "scope", "branch"
|
|
578
621
|
`;
|
|
579
622
|
|
|
580
|
-
const params = units
|
|
581
|
-
|
|
623
|
+
const params = units
|
|
624
|
+
.map(unit => [
|
|
625
|
+
unit.documentId ? unit.driveId : 'drives',
|
|
626
|
+
unit.documentId || unit.driveId,
|
|
627
|
+
unit.scope
|
|
628
|
+
])
|
|
629
|
+
.flat();
|
|
630
|
+
const results = await this.db.$queryRawUnsafe<
|
|
631
|
+
{
|
|
632
|
+
driveId: string;
|
|
633
|
+
documentId: string;
|
|
634
|
+
lastUpdated: string;
|
|
635
|
+
scope: OperationScope;
|
|
636
|
+
branch: string;
|
|
637
|
+
revision: number;
|
|
638
|
+
}[]
|
|
639
|
+
>(query, ...params);
|
|
582
640
|
return results.map(row => ({
|
|
583
641
|
...row,
|
|
584
|
-
driveId: row.driveId ===
|
|
585
|
-
documentId: row.driveId ===
|
|
586
|
-
lastUpdated: new Date(row.lastUpdated).toISOString()
|
|
642
|
+
driveId: row.driveId === 'drives' ? row.documentId : row.driveId,
|
|
643
|
+
documentId: row.driveId === 'drives' ? '' : row.documentId,
|
|
644
|
+
lastUpdated: new Date(row.lastUpdated).toISOString()
|
|
587
645
|
}));
|
|
588
646
|
}
|
|
589
647
|
}
|