document-drive 1.0.0-experimental.8 → 1.0.0-websockets

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.
@@ -18,18 +18,27 @@ import {
18
18
  Document,
19
19
  DocumentHeader,
20
20
  DocumentModel,
21
+ utils as DocumentUtils,
21
22
  Operation,
22
- OperationScope,
23
- State
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 {
@@ -51,7 +60,8 @@ import {
51
60
  InternalTransmitter,
52
61
  IReceiver,
53
62
  ITransmitter,
54
- PullResponderTransmitter
63
+ PullResponderTransmitter,
64
+ SubscriptionTransmitter
55
65
  } from './listener/transmitter';
56
66
  import {
57
67
  BaseDocumentDriveServer,
@@ -61,6 +71,7 @@ import {
61
71
  ListenerState,
62
72
  RemoteDriveOptions,
63
73
  StrandUpdate,
74
+ SynchronizationUnitQuery,
64
75
  SyncStatus,
65
76
  type CreateDocumentInput,
66
77
  type DriveInput,
@@ -69,8 +80,6 @@ import {
69
80
  type SynchronizationUnit
70
81
  } from './types';
71
82
  import { filterOperationsByRevision } from './utils';
72
- import { BaseQueueManager } from '../queue/base';
73
- import { IQueueManager } from '../queue/types';
74
83
 
75
84
  export * from './listener';
76
85
  export type * from './types';
@@ -87,7 +96,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
87
96
  DocumentDriveState['id'],
88
97
  Map<Trigger['id'], CancelPullLoop>
89
98
  >();
90
- private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
99
+ private syncStatus = new Map<string, SyncStatus>();
91
100
 
92
101
  private queueManager: IQueueManager;
93
102
 
@@ -95,7 +104,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
95
104
  documentModels: DocumentModel[],
96
105
  storage: IDriveStorage = new MemoryStorage(),
97
106
  cache: ICache = new InMemoryCache(),
98
- queueManager: IQueueManager = new BaseQueueManager(),
107
+ queueManager: IQueueManager = new BaseQueueManager()
99
108
  ) {
100
109
  super();
101
110
  this.listenerStateManager = new ListenerManager(this);
@@ -103,6 +112,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
103
112
  this.storage = storage;
104
113
  this.cache = cache;
105
114
  this.queueManager = queueManager;
115
+
116
+ this.storage.setStorageDelegate?.({
117
+ getCachedOperations: async (drive, id) => {
118
+ try {
119
+ const document = await this.cache.getDocument(drive, id);
120
+ return document?.operations;
121
+ } catch (error) {
122
+ logger.error(error);
123
+ return undefined;
124
+ }
125
+ }
126
+ });
106
127
  }
107
128
 
108
129
  private updateSyncStatus(
@@ -119,31 +140,30 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
119
140
  }
120
141
 
121
142
  private async saveStrand(strand: StrandUpdate) {
122
- const operations: Operation[] = strand.operations.map(
123
- (op) => ({
124
- ...op,
125
- scope: strand.scope,
126
- branch: strand.branch
127
- })
128
- );
143
+ const operations: Operation[] = strand.operations.map(op => ({
144
+ ...op,
145
+ scope: strand.scope,
146
+ branch: strand.branch
147
+ }));
129
148
 
130
149
  const result = await (!strand.documentId
131
150
  ? this.queueDriveOperations(
132
- strand.driveId,
133
- operations as Operation<DocumentDriveAction | BaseAction>[],
134
- false
135
- )
151
+ strand.driveId,
152
+ operations as Operation<DocumentDriveAction | BaseAction>[],
153
+ false
154
+ )
136
155
  : this.queueOperations(
137
- strand.driveId,
138
- strand.documentId,
139
- operations,
140
- false
141
- ));
156
+ strand.driveId,
157
+ strand.documentId,
158
+ operations,
159
+ false
160
+ ));
142
161
 
143
162
  if (result.status === 'ERROR') {
144
163
  this.updateSyncStatus(strand.driveId, result.status, result.error);
164
+ } else {
165
+ this.emit('strandUpdate', strand);
145
166
  }
146
- this.emit('strandUpdate', strand);
147
167
  return result;
148
168
  }
149
169
 
@@ -174,6 +194,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
174
194
  const drive = await this.getDrive(driveId);
175
195
  let driveTriggers = this.triggerMap.get(driveId);
176
196
 
197
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
198
+
177
199
  for (const trigger of drive.state.local.triggers) {
178
200
  if (driveTriggers?.get(trigger.id)) {
179
201
  continue;
@@ -184,8 +206,56 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
184
206
  }
185
207
 
186
208
  this.updateSyncStatus(driveId, 'SYNCING');
209
+
210
+ for (const syncUnit of syncUnits) {
211
+ this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
212
+ }
213
+
214
+ let cancelTrigger: (() => void) | undefined = undefined;
187
215
  if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
188
- const cancelPullLoop = PullResponderTransmitter.setupPull(
216
+ cancelTrigger = PullResponderTransmitter.setupPull(
217
+ driveId,
218
+ trigger,
219
+ this.saveStrand.bind(this),
220
+ error => {
221
+ this.updateSyncStatus(
222
+ driveId,
223
+ error instanceof OperationError
224
+ ? error.status
225
+ : 'ERROR',
226
+ error
227
+ );
228
+ },
229
+ revisions => {
230
+ const errorRevision = revisions.filter(
231
+ r => r.status !== 'SUCCESS'
232
+ );
233
+ if (errorRevision.length < 1) {
234
+ this.updateSyncStatus(driveId, 'SUCCESS');
235
+ }
236
+
237
+ for (const syncUnit of syncUnits) {
238
+ const fileErrorRevision = errorRevision.find(
239
+ r => r.documentId === syncUnit.documentId
240
+ );
241
+
242
+ if (fileErrorRevision) {
243
+ this.updateSyncStatus(
244
+ syncUnit.syncId,
245
+ fileErrorRevision.status,
246
+ fileErrorRevision.error
247
+ );
248
+ } else {
249
+ this.updateSyncStatus(
250
+ syncUnit.syncId,
251
+ 'SUCCESS'
252
+ );
253
+ }
254
+ }
255
+ }
256
+ );
257
+ } else if (SubscriptionTransmitter.isTrigger(trigger)) {
258
+ cancelTrigger = SubscriptionTransmitter.setup(
189
259
  driveId,
190
260
  trigger,
191
261
  this.saveStrand.bind(this),
@@ -207,19 +277,78 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
207
277
  }
208
278
  }
209
279
  );
210
- driveTriggers.set(trigger.id, cancelPullLoop);
280
+ }
281
+
282
+ if (cancelTrigger) {
283
+ driveTriggers.set(trigger.id, cancelTrigger);
211
284
  this.triggerMap.set(driveId, driveTriggers);
212
285
  }
213
286
  }
214
287
  }
215
288
 
216
289
  private async stopSyncRemoteDrive(driveId: string) {
290
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
291
+ const fileNodes = syncUnits
292
+ .filter(syncUnit => syncUnit.documentId !== '')
293
+ .map(syncUnit => syncUnit.documentId);
294
+
217
295
  const triggers = this.triggerMap.get(driveId);
218
296
  triggers?.forEach(cancel => cancel());
219
297
  this.updateSyncStatus(driveId, null);
298
+
299
+ for (const fileNode of fileNodes) {
300
+ this.updateSyncStatus(fileNode, null);
301
+ }
220
302
  return this.triggerMap.delete(driveId);
221
303
  }
222
304
 
305
+ private queueDelegate = {
306
+ checkDocumentExists: (
307
+ driveId: string,
308
+ documentId: string
309
+ ): Promise<boolean> =>
310
+ this.storage.checkDocumentExists(driveId, documentId),
311
+ processOperationJob: async ({
312
+ driveId,
313
+ documentId,
314
+ operations,
315
+ forceSync
316
+ }: OperationJob) => {
317
+ return documentId
318
+ ? this.addOperations(driveId, documentId, operations, forceSync)
319
+ : this.addDriveOperations(
320
+ driveId,
321
+ operations as Operation<
322
+ DocumentDriveAction | BaseAction
323
+ >[],
324
+ forceSync
325
+ );
326
+ },
327
+ processActionJob: async ({
328
+ driveId,
329
+ documentId,
330
+ actions,
331
+ forceSync
332
+ }: ActionJob) => {
333
+ return documentId
334
+ ? this.addActions(driveId, documentId, actions, forceSync)
335
+ : this.addDriveActions(
336
+ driveId,
337
+ actions as Operation<DocumentDriveAction | BaseAction>[],
338
+ forceSync
339
+ );
340
+ },
341
+ processJob: async (job: Job) => {
342
+ if (isOperationJob(job)) {
343
+ return this.queueDelegate.processOperationJob(job);
344
+ } else if (isActionJob(job)) {
345
+ return this.queueDelegate.processActionJob(job);
346
+ } else {
347
+ throw new Error('Unknown job type', job);
348
+ }
349
+ }
350
+ };
351
+
223
352
  async initialize() {
224
353
  const errors: Error[] = [];
225
354
  const drives = await this.getDrives();
@@ -230,16 +359,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
230
359
  });
231
360
  }
232
361
 
233
- await this.queueManager.init({
234
- checkDocumentExists: (driveId: string, documentId: string): Promise<boolean> => this.storage.checkDocumentExists(driveId, documentId),
235
- processOperationJob: ({ driveId, documentId, operations, forceSync }) => documentId ?
236
- this.addOperations(driveId, documentId, operations, forceSync)
237
- : this.addDriveOperations(driveId, operations as Operation<DocumentDriveAction | BaseAction>[], forceSync)
238
-
239
- }, error => {
362
+ await this.queueManager.init(this.queueDelegate, error => {
240
363
  logger.error(`Error initializing queue manager`, error);
241
364
  errors.push(error);
242
- })
365
+ });
243
366
 
244
367
  // if network connect comes online then
245
368
  // triggers the listeners update
@@ -278,6 +401,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
278
401
  ) {
279
402
  const drive = await this.getDrive(driveId);
280
403
 
404
+ const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
405
+ driveId,
406
+ documentId,
407
+ scope,
408
+ branch,
409
+ documentType,
410
+ drive
411
+ );
412
+ const revisions = await this.storage.getSynchronizationUnitsRevision(
413
+ synchronizationUnitsQuery
414
+ );
415
+
416
+ const synchronizationUnits: SynchronizationUnit[] =
417
+ synchronizationUnitsQuery.map(s => ({
418
+ ...s,
419
+ lastUpdated: drive.created,
420
+ revision: -1
421
+ }));
422
+ for (const revision of revisions) {
423
+ const syncUnit = synchronizationUnits.find(
424
+ s =>
425
+ revision.driveId === s.driveId &&
426
+ revision.documentId === s.documentId &&
427
+ revision.scope === s.scope &&
428
+ revision.branch === s.branch
429
+ );
430
+ if (syncUnit) {
431
+ syncUnit.revision = revision.revision;
432
+ syncUnit.lastUpdated = revision.lastUpdated;
433
+ }
434
+ }
435
+ return synchronizationUnits;
436
+ }
437
+
438
+ public async getSynchronizationUnitsIds(
439
+ driveId: string,
440
+ documentId?: string[],
441
+ scope?: string[],
442
+ branch?: string[],
443
+ documentType?: string[],
444
+ loadedDrive?: DocumentDriveDocument
445
+ ): Promise<SynchronizationUnitQuery[]> {
446
+ const drive = loadedDrive ?? (await this.getDrive(driveId));
281
447
  const nodes = drive.state.global.nodes.filter(
282
448
  node =>
283
449
  isFileNode(node) &&
@@ -311,53 +477,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
311
477
  });
312
478
  }
313
479
 
314
- const synchronizationUnits: SynchronizationUnit[] = [];
315
-
480
+ const synchronizationUnitsQuery: Omit<
481
+ SynchronizationUnit,
482
+ 'revision' | 'lastUpdated'
483
+ >[] = [];
316
484
  for (const node of nodes) {
317
485
  const nodeUnits =
318
486
  scope?.length || branch?.length
319
487
  ? node.synchronizationUnits.filter(
320
- unit =>
321
- (!scope?.length ||
322
- scope.includes(unit.scope) ||
323
- scope.includes('*')) &&
324
- (!branch?.length ||
325
- branch.includes(unit.branch) ||
326
- branch.includes('*'))
327
- )
488
+ unit =>
489
+ (!scope?.length ||
490
+ scope.includes(unit.scope) ||
491
+ scope.includes('*')) &&
492
+ (!branch?.length ||
493
+ branch.includes(unit.branch) ||
494
+ branch.includes('*'))
495
+ )
328
496
  : node.synchronizationUnits;
329
497
  if (!nodeUnits.length) {
330
498
  continue;
331
499
  }
332
-
333
- const document = await (node.id
334
- ? this.getDocument(driveId, node.id)
335
- : this.getDrive(driveId));
336
-
337
- for (const { syncId, scope, branch } of nodeUnits) {
338
- const operations =
339
- document.operations[scope as OperationScope] ?? [];
340
- const lastOperation = operations[operations.length - 1];
341
- synchronizationUnits.push({
342
- syncId,
343
- scope,
344
- branch,
500
+ synchronizationUnitsQuery.push(
501
+ ...nodeUnits.map(n => ({
345
502
  driveId,
346
503
  documentId: node.id,
504
+ syncId: n.syncId,
347
505
  documentType: node.documentType,
348
- lastUpdated:
349
- lastOperation?.timestamp ?? document.lastModified,
350
- revision: lastOperation?.index ?? 0
351
- });
352
- }
506
+ scope: n.scope,
507
+ branch: n.branch
508
+ }))
509
+ );
353
510
  }
354
- return synchronizationUnits;
511
+ return synchronizationUnitsQuery;
355
512
  }
356
513
 
357
- public async getSynchronizationUnit(
514
+ public async getSynchronizationUnitIdInfo(
358
515
  driveId: string,
359
516
  syncId: string
360
- ): Promise<SynchronizationUnit> {
517
+ ): Promise<SynchronizationUnitQuery | undefined> {
361
518
  const drive = await this.getDrive(driveId);
362
519
  const node = drive.state.global.nodes.find(
363
520
  node =>
@@ -366,14 +523,43 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
366
523
  );
367
524
 
368
525
  if (!node || !isFileNode(node)) {
369
- throw new Error('Synchronization unit not found');
526
+ return undefined;
370
527
  }
371
528
 
372
- const { scope, branch } = node.synchronizationUnits.find(
529
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
530
+ const syncUnit = node.synchronizationUnits.find(
373
531
  unit => unit.syncId === syncId
374
- )!;
532
+ );
533
+ if (!syncUnit) {
534
+ return undefined;
535
+ }
375
536
 
376
- const documentId = node.id;
537
+ return {
538
+ syncId,
539
+ scope: syncUnit.scope,
540
+ branch: syncUnit.branch,
541
+ driveId,
542
+ documentId: node.id,
543
+ documentType: node.documentType
544
+ };
545
+ }
546
+
547
+ public async getSynchronizationUnit(
548
+ driveId: string,
549
+ syncId: string
550
+ ): Promise<SynchronizationUnit | undefined> {
551
+ const syncUnit = await this.getSynchronizationUnitIdInfo(
552
+ driveId,
553
+ syncId
554
+ );
555
+
556
+ if (!syncUnit) {
557
+ return undefined;
558
+ }
559
+
560
+ const { scope, branch, documentId, documentType } = syncUnit;
561
+
562
+ // TODO: REPLACE WITH GET DOCUMENT OPERATIONS
377
563
  const document = await this.getDocument(driveId, documentId);
378
564
  const operations = document.operations[scope as OperationScope] ?? [];
379
565
  const lastOperation = operations[operations.length - 1];
@@ -384,7 +570,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
384
570
  branch,
385
571
  driveId,
386
572
  documentId,
387
- documentType: node.documentType,
573
+ documentType,
388
574
  lastUpdated: lastOperation?.timestamp ?? document.lastModified,
389
575
  revision: lastOperation?.index ?? 0
390
576
  };
@@ -398,17 +584,22 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
398
584
  fromRevision?: number | undefined;
399
585
  }
400
586
  ): Promise<OperationUpdate[]> {
401
- const { documentId, scope } =
587
+ const syncUnit =
402
588
  syncId === '0'
403
589
  ? { documentId: '', scope: 'global' }
404
- : await this.getSynchronizationUnit(driveId, syncId);
590
+ : await this.getSynchronizationUnitIdInfo(driveId, syncId);
591
+
592
+ if (!syncUnit) {
593
+ throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
594
+ }
405
595
 
406
596
  const document =
407
597
  syncId === '0'
408
598
  ? await this.getDrive(driveId)
409
- : await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
599
+ : await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
410
600
 
411
- const operations = document.operations[scope as OperationScope] ?? []; // TODO filter by branch also
601
+ const operations =
602
+ document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
412
603
  const filteredOperations = operations.filter(
413
604
  operation =>
414
605
  Object.keys(filter).length === 0 ||
@@ -425,7 +616,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
425
616
  type: operation.type,
426
617
  input: operation.input as object,
427
618
  skip: operation.skip,
428
- context: operation.context
619
+ context: operation.context,
620
+ id: operation.id
429
621
  }));
430
622
  }
431
623
 
@@ -455,12 +647,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
455
647
  });
456
648
 
457
649
  await this.storage.createDrive(id, document);
650
+
651
+ if (drive.global.slug) {
652
+ await this.cache.deleteDocument('drives-slug', drive.global.slug);
653
+ }
654
+
458
655
  await this._initializeDrive(id);
459
656
 
460
657
  return document;
461
658
  }
462
659
 
463
- async addRemoteDrive(url: string, options: RemoteDriveOptions): Promise<DocumentDriveDocument> {
660
+ async addRemoteDrive(
661
+ url: string,
662
+ options: RemoteDriveOptions
663
+ ): Promise<DocumentDriveDocument> {
464
664
  const { id, name, slug, icon } = await requestPublicDrive(url);
465
665
  const {
466
666
  pullFilter,
@@ -471,11 +671,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
471
671
  triggers
472
672
  } = options;
473
673
 
474
- const pullTrigger =
475
- await PullResponderTransmitter.createPullResponderTrigger(id, url, {
476
- pullFilter,
477
- pullInterval
478
- });
674
+ const trigger = await SubscriptionTransmitter.createTrigger(id, url, {
675
+ pullFilter
676
+ });
479
677
 
480
678
  return await this.addDrive({
481
679
  global: {
@@ -485,7 +683,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
485
683
  icon: icon ?? null
486
684
  },
487
685
  local: {
488
- triggers: [...triggers, pullTrigger],
686
+ triggers: [...triggers, trigger],
489
687
  listeners: listeners,
490
688
  availableOffline,
491
689
  sharingType
@@ -494,9 +692,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
494
692
  }
495
693
 
496
694
  async deleteDrive(id: string) {
497
- this.stopSyncRemoteDrive(id);
498
- await this.cache.deleteDocument('drives', id);
499
- return this.storage.deleteDrive(id);
695
+ const result = await Promise.allSettled([
696
+ this.stopSyncRemoteDrive(id),
697
+ this.listenerStateManager.removeDrive(id),
698
+ this.cache.deleteDocument('drives', id),
699
+ this.storage.deleteDrive(id)
700
+ ]);
701
+
702
+ result.forEach(r => {
703
+ if (r.status === 'rejected') {
704
+ throw r.reason;
705
+ }
706
+ });
500
707
  }
501
708
 
502
709
  getDrives() {
@@ -505,7 +712,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
505
712
 
506
713
  async getDrive(drive: string, options?: GetDocumentOptions) {
507
714
  try {
508
- const document = await this.cache.getDocument('drives', drive);
715
+ const document = await this.cache.getDocument('drives', drive); // TODO support GetDocumentOptions
509
716
  if (document && isDocumentDrive(document)) {
510
717
  return document;
511
718
  }
@@ -528,7 +735,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
528
735
 
529
736
  async getDriveBySlug(slug: string, options?: GetDocumentOptions) {
530
737
  try {
531
- const document = await this.cache.getDocument('drives', slug);
738
+ const document = await this.cache.getDocument('drives-slug', slug);
532
739
  if (document && isDocumentDrive(document)) {
533
740
  return document;
534
741
  }
@@ -544,7 +751,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
544
751
  );
545
752
  } else {
546
753
  this.cache
547
- .setDocument('drives', slug, document)
754
+ .setDocument('drives-slug', slug, document)
548
755
  .catch(logger.error);
549
756
  return document;
550
757
  }
@@ -552,16 +759,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
552
759
 
553
760
  async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
554
761
  try {
555
- const document = await this.cache.getDocument(drive, id);
762
+ const document = await this.cache.getDocument(drive, id); // TODO support GetDocumentOptions
556
763
  if (document) {
557
764
  return document;
558
765
  }
559
766
  } catch (e) {
560
767
  logger.error('Error getting document from cache', e);
561
768
  }
562
- const documentStorage =
563
- await this.storage.getDocument(drive, id);
564
- const document = this._buildDocument(documentStorage, options)
769
+ const documentStorage = await this.storage.getDocument(drive, id);
770
+ const document = this._buildDocument(documentStorage, options);
565
771
 
566
772
  this.cache.setDocument(drive, id, document).catch(logger.error);
567
773
  return document;
@@ -579,14 +785,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
579
785
  let state = undefined;
580
786
  if (input.document) {
581
787
  if (input.documentType !== input.document.documentType) {
582
- throw new Error(`Provided document is not ${input.documentType}`);
788
+ throw new Error(
789
+ `Provided document is not ${input.documentType}`
790
+ );
583
791
  }
584
792
  const doc = this._buildDocument(input.document);
585
793
  state = doc.state;
586
794
  }
587
795
 
588
796
  // if no document was provided then create a new one
589
- const document = input.document ??
797
+ const document =
798
+ input.document ??
590
799
  this._getDocumentModel(input.documentType).utils.createDocument();
591
800
 
592
801
  // stores document information
@@ -608,9 +817,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
608
817
  const operations = Object.values(document.operations).flat();
609
818
  if (operations.length) {
610
819
  if (isDocumentDrive(document)) {
611
- await this.storage.addDriveOperations(driveId, operations as Operation<DocumentDriveAction>[], document);
820
+ await this.storage.addDriveOperations(
821
+ driveId,
822
+ operations as Operation<DocumentDriveAction>[],
823
+ document
824
+ );
612
825
  } else {
613
- await this.storage.addDocumentOperations(driveId, input.id, operations, document)
826
+ await this.storage.addDocumentOperations(
827
+ driveId,
828
+ input.id,
829
+ operations,
830
+ document
831
+ );
614
832
  }
615
833
  }
616
834
 
@@ -619,7 +837,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
619
837
 
620
838
  async deleteDocument(driveId: string, id: string) {
621
839
  try {
622
- const syncUnits = await this.getSynchronizationUnits(driveId, [id]);
840
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
841
+ id
842
+ ]);
623
843
  await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
624
844
  } catch (error) {
625
845
  logger.warn('Error deleting document', error);
@@ -630,12 +850,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
630
850
 
631
851
  async _processOperations<T extends Document, A extends Action>(
632
852
  drive: string,
853
+ documentId: string | undefined,
633
854
  storageDocument: DocumentStorage<T>,
634
855
  operations: Operation<A | BaseAction>[]
635
856
  ) {
636
857
  const operationsApplied: Operation<A | BaseAction>[] = [];
637
858
  const signals: SignalResult[] = [];
638
- let document: T = storageDocument as T;
859
+ let document: T = this._buildDocument(storageDocument);
639
860
 
640
861
  let error: OperationError | undefined; // TODO: replace with an array of errors/consistency issues
641
862
  const operationsByScope = groupOperationsByScope(operations);
@@ -667,7 +888,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
667
888
  : merge(trunk, invertedTrunk, reshuffleByTimestamp);
668
889
 
669
890
  const newOperations = newHistory.filter(
670
- (op: any) => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
891
+ op => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
671
892
  );
672
893
 
673
894
  for (const nextOperation of newOperations) {
@@ -676,15 +897,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
676
897
  // when dealing with a merge (tail.length > 0) we have to skip hash validation
677
898
  // for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
678
899
  if (tail.length > 0) {
679
- skipHashValidation = [...invertedTrunk, ...tail].some(
680
- invertedTrunkOp =>
681
- invertedTrunkOp.hash === nextOperation.hash
900
+ const sourceOperation = operations.find(
901
+ op => op.hash === nextOperation.hash
682
902
  );
903
+
904
+ skipHashValidation =
905
+ !sourceOperation ||
906
+ sourceOperation.index !== nextOperation.index ||
907
+ sourceOperation.skip !== nextOperation.skip;
683
908
  }
684
909
 
685
910
  try {
686
911
  const appliedResult = await this._performOperation(
687
912
  drive,
913
+ documentId,
688
914
  document,
689
915
  nextOperation,
690
916
  skipHashValidation
@@ -697,11 +923,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
697
923
  e instanceof OperationError
698
924
  ? e
699
925
  : new OperationError(
700
- 'ERROR',
701
- nextOperation,
702
- (e as Error).message,
703
- (e as Error).cause
704
- );
926
+ 'ERROR',
927
+ nextOperation,
928
+ (e as Error).message,
929
+ (e as Error).cause
930
+ );
705
931
 
706
932
  // TODO: don't break on errors...
707
933
  break;
@@ -713,22 +939,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
713
939
  document,
714
940
  operationsApplied,
715
941
  signals,
716
- error,
942
+ error
717
943
  } as const;
718
944
  }
719
945
 
720
946
  private _buildDocument<T extends Document>(
721
- documentStorage: DocumentStorage<T>, options?: GetDocumentOptions
947
+ documentStorage: DocumentStorage<T>,
948
+ options?: GetDocumentOptions
722
949
  ): T {
950
+ if (
951
+ documentStorage.state &&
952
+ (!options || options.checkHashes === false)
953
+ ) {
954
+ return documentStorage as T;
955
+ }
956
+
723
957
  const documentModel = this._getDocumentModel(
724
958
  documentStorage.documentType
725
959
  );
726
960
 
727
- const revisionOperations = options?.revisions !== undefined ? filterOperationsByRevision(
728
- documentStorage.operations,
729
- options.revisions
730
- ) : documentStorage.operations;
731
- const operations = baseUtils.documentHelpers.grabageCollectDocumentOperations(revisionOperations);
961
+ const revisionOperations =
962
+ options?.revisions !== undefined
963
+ ? filterOperationsByRevision(
964
+ documentStorage.operations,
965
+ options.revisions
966
+ )
967
+ : documentStorage.operations;
968
+ const operations =
969
+ baseUtils.documentHelpers.garbageCollectDocumentOperations(
970
+ revisionOperations
971
+ );
732
972
 
733
973
  return baseUtils.replayDocument(
734
974
  documentStorage.initialState,
@@ -737,12 +977,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
737
977
  undefined,
738
978
  documentStorage,
739
979
  undefined,
740
- { checkHashes: options?.checkHashes ?? true }
980
+ {
981
+ ...options,
982
+ checkHashes: options?.checkHashes ?? true,
983
+ reuseOperationResultingState: options?.checkHashes ?? true
984
+ }
741
985
  ) as T;
742
986
  }
743
987
 
744
988
  private async _performOperation<T extends Document>(
745
989
  drive: string,
990
+ id: string | undefined,
746
991
  document: T,
747
992
  operation: Operation,
748
993
  skipHashValidation = false
@@ -752,6 +997,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
752
997
  const signalResults: SignalResult[] = [];
753
998
  let newDocument = document;
754
999
 
1000
+ const scope = operation.scope;
1001
+ const documentOperations =
1002
+ DocumentUtils.documentHelpers.garbageCollectDocumentOperations({
1003
+ ...document.operations,
1004
+ [scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
1005
+ document.operations[scope],
1006
+ operation
1007
+ )
1008
+ });
1009
+
1010
+ const lastRemainingOperation = documentOperations[scope].at(-1);
1011
+ // if the latest operation doesn't have a resulting state then tries
1012
+ // to retrieve it from the db to avoid rerunning all the operations
1013
+ if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
1014
+ lastRemainingOperation.resultingState = await (id
1015
+ ? this.storage.getOperationResultingState?.(
1016
+ drive,
1017
+ id,
1018
+ lastRemainingOperation.index,
1019
+ lastRemainingOperation.scope,
1020
+ 'main'
1021
+ )
1022
+ : this.storage.getDriveOperationResultingState?.(
1023
+ drive,
1024
+ lastRemainingOperation.index,
1025
+ lastRemainingOperation.scope,
1026
+ 'main'
1027
+ ));
1028
+ }
1029
+
755
1030
  const operationSignals: (() => Promise<SignalResult>)[] = [];
756
1031
  newDocument = documentModel.reducer(
757
1032
  newDocument,
@@ -788,7 +1063,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
788
1063
  );
789
1064
  }
790
1065
  },
791
- { skip: operation.skip }
1066
+ { skip: operation.skip, reuseOperationResultingState: true }
792
1067
  ) as T;
793
1068
 
794
1069
  const appliedOperation = newDocument.operations[operation.scope].filter(
@@ -799,16 +1074,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
799
1074
  throw new OperationError(
800
1075
  'ERROR',
801
1076
  operation,
802
- `Operation with index ${operation.index}:${operation.skip} was not applied.`
1077
+ `Operation with index ${operation.index}:${operation.skip || 0} was not applied.`
803
1078
  );
804
1079
  } else if (
805
1080
  appliedOperation[0]!.hash !== operation.hash &&
806
1081
  !skipHashValidation
807
1082
  ) {
808
- throw new ConflictOperationError(
809
- operation,
810
- appliedOperation[0]!
811
- );
1083
+ throw new ConflictOperationError(operation, appliedOperation[0]!);
812
1084
  }
813
1085
 
814
1086
  for (const signalHandler of operationSignals) {
@@ -823,7 +1095,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
823
1095
  };
824
1096
  }
825
1097
 
826
- addOperation(drive: string, id: string, operation: Operation, forceSync = true): Promise<IOperationResult> {
1098
+ addOperation(
1099
+ drive: string,
1100
+ id: string,
1101
+ operation: Operation,
1102
+ forceSync = true
1103
+ ): Promise<IOperationResult> {
827
1104
  return this.addOperations(drive, id, [operation], forceSync);
828
1105
  }
829
1106
 
@@ -833,21 +1110,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
833
1110
  callback: (document: DocumentStorage) => Promise<{
834
1111
  operations: Operation[];
835
1112
  header: DocumentHeader;
836
- newState: State<any, any> | undefined;
837
1113
  }>
838
1114
  ) {
839
1115
  if (!this.storage.addDocumentOperationsWithTransaction) {
840
1116
  const documentStorage = await this.storage.getDocument(drive, id);
841
1117
  const result = await callback(documentStorage);
842
1118
  // saves the applied operations to storage
843
- if (
844
- result.operations.length > 0
845
- ) {
1119
+ if (result.operations.length > 0) {
846
1120
  await this.storage.addDocumentOperations(
847
1121
  drive,
848
1122
  id,
849
1123
  result.operations,
850
- result.header,
1124
+ result.header
851
1125
  );
852
1126
  }
853
1127
  } else {
@@ -859,47 +1133,213 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
859
1133
  }
860
1134
  }
861
1135
 
862
- queueOperation(drive: string, id: string, operation: Operation, forceSync = true): Promise<IOperationResult> {
1136
+ queueOperation(
1137
+ drive: string,
1138
+ id: string,
1139
+ operation: Operation,
1140
+ forceSync = true
1141
+ ): Promise<IOperationResult> {
863
1142
  return this.queueOperations(drive, id, [operation], forceSync);
864
1143
  }
865
1144
 
866
- async queueOperations(drive: string,
1145
+ private async resultIfExistingOperations(
1146
+ drive: string,
1147
+ id: string,
1148
+ operations: Operation[]
1149
+ ): Promise<IOperationResult | undefined> {
1150
+ try {
1151
+ const document = await this.getDocument(drive, id);
1152
+ const newOperation = operations.find(
1153
+ op =>
1154
+ !op.id ||
1155
+ !document.operations[op.scope].find(
1156
+ existingOp =>
1157
+ existingOp.id === op.id &&
1158
+ existingOp.index === op.index &&
1159
+ existingOp.type === op.type &&
1160
+ existingOp.hash === op.hash
1161
+ )
1162
+ );
1163
+ if (!newOperation) {
1164
+ return {
1165
+ status: 'SUCCESS',
1166
+ document,
1167
+ operations,
1168
+ signals: []
1169
+ };
1170
+ } else {
1171
+ return undefined;
1172
+ }
1173
+ } catch (error) {
1174
+ console.error(error); // TODO error
1175
+ return undefined;
1176
+ }
1177
+ }
1178
+
1179
+ async queueOperations(
1180
+ drive: string,
867
1181
  id: string,
868
1182
  operations: Operation[],
869
- forceSync = true) {
1183
+ forceSync = true
1184
+ ) {
1185
+ // if operations are already stored then returns cached document
1186
+ const result = await this.resultIfExistingOperations(
1187
+ drive,
1188
+ id,
1189
+ operations
1190
+ );
1191
+ if (result) {
1192
+ console.log('Duplicated operations!');
1193
+ return result;
1194
+ }
1195
+ try {
1196
+ const jobId = await this.queueManager.addJob({
1197
+ driveId: drive,
1198
+ documentId: id,
1199
+ operations,
1200
+ forceSync
1201
+ });
870
1202
 
1203
+ return new Promise<IOperationResult>((resolve, reject) => {
1204
+ const unsubscribe = this.queueManager.on(
1205
+ 'jobCompleted',
1206
+ (job, result) => {
1207
+ if (job.jobId === jobId) {
1208
+ unsubscribe();
1209
+ unsubscribeError();
1210
+ resolve(result);
1211
+ }
1212
+ }
1213
+ );
1214
+ const unsubscribeError = this.queueManager.on(
1215
+ 'jobFailed',
1216
+ (job, error) => {
1217
+ if (job.jobId === jobId) {
1218
+ unsubscribe();
1219
+ unsubscribeError();
1220
+ reject(error);
1221
+ }
1222
+ }
1223
+ );
1224
+ });
1225
+ } catch (error) {
1226
+ logger.error('Error adding job', error);
1227
+ throw error;
1228
+ }
1229
+ }
1230
+
1231
+ async queueAction(
1232
+ drive: string,
1233
+ id: string,
1234
+ action: Action,
1235
+ forceSync?: boolean | undefined
1236
+ ): Promise<IOperationResult> {
1237
+ return this.queueActions(drive, id, [action], forceSync);
1238
+ }
1239
+
1240
+ async queueActions(
1241
+ drive: string,
1242
+ id: string,
1243
+ actions: Action[],
1244
+ forceSync?: boolean | undefined
1245
+ ): Promise<IOperationResult> {
871
1246
  try {
872
- const jobId = await this.queueManager.addJob({ driveId: drive, documentId: id, operations, forceSync });
1247
+ const jobId = await this.queueManager.addJob({
1248
+ driveId: drive,
1249
+ documentId: id,
1250
+ actions,
1251
+ forceSync
1252
+ });
873
1253
 
874
1254
  return new Promise<IOperationResult>((resolve, reject) => {
875
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
876
- if (job.jobId === jobId) {
877
- unsubscribe();
878
- unsubscribeError();
879
- resolve(result);
1255
+ const unsubscribe = this.queueManager.on(
1256
+ 'jobCompleted',
1257
+ (job, result) => {
1258
+ if (job.jobId === jobId) {
1259
+ unsubscribe();
1260
+ unsubscribeError();
1261
+ resolve(result);
1262
+ }
880
1263
  }
881
- });
882
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
883
- console.log("test")
884
- if (job.jobId === jobId) {
885
- unsubscribe();
886
- unsubscribeError();
887
- reject(error);
1264
+ );
1265
+ const unsubscribeError = this.queueManager.on(
1266
+ 'jobFailed',
1267
+ (job, error) => {
1268
+ if (job.jobId === jobId) {
1269
+ unsubscribe();
1270
+ unsubscribeError();
1271
+ reject(error);
1272
+ }
888
1273
  }
889
- });
890
- })
1274
+ );
1275
+ });
891
1276
  } catch (error) {
892
1277
  logger.error('Error adding job', error);
893
1278
  throw error;
894
1279
  }
895
1280
  }
896
1281
 
1282
+ async queueDriveAction(
1283
+ drive: string,
1284
+ action: DocumentDriveAction | BaseAction,
1285
+ forceSync?: boolean | undefined
1286
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1287
+ return this.queueDriveActions(drive, [action], forceSync);
1288
+ }
1289
+
1290
+ async queueDriveActions(
1291
+ drive: string,
1292
+ actions: (DocumentDriveAction | BaseAction)[],
1293
+ forceSync?: boolean | undefined
1294
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1295
+ const jobId = await this.queueManager.addJob({
1296
+ driveId: drive,
1297
+ actions,
1298
+ forceSync
1299
+ });
1300
+ return new Promise<IOperationResult<DocumentDriveDocument>>(
1301
+ (resolve, reject) => {
1302
+ const unsubscribe = this.queueManager.on(
1303
+ 'jobCompleted',
1304
+ (job, result) => {
1305
+ if (job.jobId === jobId) {
1306
+ unsubscribe();
1307
+ unsubscribeError();
1308
+ resolve(
1309
+ result as IOperationResult<DocumentDriveDocument>
1310
+ );
1311
+ }
1312
+ }
1313
+ );
1314
+ const unsubscribeError = this.queueManager.on(
1315
+ 'jobFailed',
1316
+ (job, error) => {
1317
+ if (job.jobId === jobId) {
1318
+ unsubscribe();
1319
+ unsubscribeError();
1320
+ reject(error);
1321
+ }
1322
+ }
1323
+ );
1324
+ }
1325
+ );
1326
+ }
1327
+
897
1328
  async addOperations(
898
1329
  drive: string,
899
1330
  id: string,
900
1331
  operations: Operation[],
901
1332
  forceSync = true
902
1333
  ) {
1334
+ // if operations are already stored then returns the result
1335
+ const result = await this.resultIfExistingOperations(
1336
+ drive,
1337
+ id,
1338
+ operations
1339
+ );
1340
+ if (result) {
1341
+ return result;
1342
+ }
903
1343
  let document: Document | undefined;
904
1344
  const operationsApplied: Operation[] = [];
905
1345
  const signals: SignalResult[] = [];
@@ -909,6 +1349,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
909
1349
  await this._addOperations(drive, id, async documentStorage => {
910
1350
  const result = await this._processOperations(
911
1351
  drive,
1352
+ id,
912
1353
  documentStorage,
913
1354
  operations
914
1355
  );
@@ -956,21 +1397,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
956
1397
  .updateSynchronizationRevisions(
957
1398
  drive,
958
1399
  syncUnits,
959
- () => this.updateSyncStatus(drive, 'SYNCING'),
1400
+ () => {
1401
+ this.updateSyncStatus(drive, 'SYNCING');
1402
+
1403
+ for (const syncUnit of syncUnits) {
1404
+ this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
1405
+ }
1406
+ },
960
1407
  this.handleListenerError.bind(this),
961
1408
  forceSync
962
1409
  )
963
- .then(
964
- updates =>
965
- updates.length &&
966
- this.updateSyncStatus(drive, 'SUCCESS')
967
- )
1410
+ .then(updates => {
1411
+ updates.length && this.updateSyncStatus(drive, 'SUCCESS');
1412
+
1413
+ for (const syncUnit of syncUnits) {
1414
+ this.updateSyncStatus(syncUnit.syncId, 'SUCCESS');
1415
+ }
1416
+ })
968
1417
  .catch(error => {
969
1418
  logger.error(
970
1419
  'Non handled error updating sync revision',
971
1420
  error
972
1421
  );
973
1422
  this.updateSyncStatus(drive, 'ERROR', error as Error);
1423
+
1424
+ for (const syncUnit of syncUnits) {
1425
+ this.updateSyncStatus(
1426
+ syncUnit.syncId,
1427
+ 'ERROR',
1428
+ error as Error
1429
+ );
1430
+ }
974
1431
  });
975
1432
 
976
1433
  // after applying all the valid operations,throws
@@ -990,11 +1447,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
990
1447
  error instanceof OperationError
991
1448
  ? error
992
1449
  : new OperationError(
993
- 'ERROR',
994
- undefined,
995
- (error as Error).message,
996
- (error as Error).cause
997
- );
1450
+ 'ERROR',
1451
+ undefined,
1452
+ (error as Error).message,
1453
+ (error as Error).cause
1454
+ );
998
1455
 
999
1456
  return {
1000
1457
  status: operationError.status,
@@ -1049,33 +1506,92 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1049
1506
  }
1050
1507
  }
1051
1508
 
1052
- queueDriveOperation(drive: string, operation: Operation<DocumentDriveAction | BaseAction>, forceSync = true): Promise<IOperationResult<DocumentDriveDocument>> {
1509
+ queueDriveOperation(
1510
+ drive: string,
1511
+ operation: Operation<DocumentDriveAction | BaseAction>,
1512
+ forceSync = true
1513
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1053
1514
  return this.queueDriveOperations(drive, [operation], forceSync);
1054
1515
  }
1055
1516
 
1517
+ private async resultIfExistingDriveOperations(
1518
+ driveId: string,
1519
+ operations: Operation<DocumentDriveAction | BaseAction>[]
1520
+ ): Promise<IOperationResult<DocumentDriveDocument> | undefined> {
1521
+ try {
1522
+ const drive = await this.getDrive(driveId);
1523
+ const newOperation = operations.find(
1524
+ op =>
1525
+ !op.id ||
1526
+ !drive.operations[op.scope].find(
1527
+ existingOp =>
1528
+ existingOp.id === op.id &&
1529
+ existingOp.index === op.index &&
1530
+ existingOp.type === op.type &&
1531
+ existingOp.hash === op.hash
1532
+ )
1533
+ );
1534
+ if (!newOperation) {
1535
+ return {
1536
+ status: 'SUCCESS',
1537
+ document: drive,
1538
+ operations: operations,
1539
+ signals: []
1540
+ } as IOperationResult<DocumentDriveDocument>;
1541
+ } else {
1542
+ return undefined;
1543
+ }
1544
+ } catch (error) {
1545
+ console.error(error); // TODO error
1546
+ return undefined;
1547
+ }
1548
+ }
1549
+
1056
1550
  async queueDriveOperations(
1057
1551
  drive: string,
1058
1552
  operations: Operation<DocumentDriveAction | BaseAction>[],
1059
1553
  forceSync = true
1060
1554
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1061
- const jobId = await this.queueManager.addJob({ driveId: drive, operations, forceSync });
1062
- return new Promise<IOperationResult<DocumentDriveDocument>>((resolve, reject) => {
1063
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1064
- if (job.jobId === jobId) {
1065
- unsubscribe();
1066
- unsubscribeError();
1067
- resolve(result as IOperationResult<DocumentDriveDocument>);
1068
- }
1069
- });
1070
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1071
- if (job.jobId === jobId) {
1072
- unsubscribe();
1073
- unsubscribeError();
1074
- reject(error);
1075
- }
1076
- });
1555
+ // if operations are already stored then returns cached document
1556
+ const result = await this.resultIfExistingDriveOperations(
1557
+ drive,
1558
+ operations
1559
+ );
1560
+ if (result) {
1561
+ return result;
1562
+ }
1077
1563
 
1078
- })
1564
+ const jobId = await this.queueManager.addJob({
1565
+ driveId: drive,
1566
+ operations,
1567
+ forceSync
1568
+ });
1569
+ return new Promise<IOperationResult<DocumentDriveDocument>>(
1570
+ (resolve, reject) => {
1571
+ const unsubscribe = this.queueManager.on(
1572
+ 'jobCompleted',
1573
+ (job, result) => {
1574
+ if (job.jobId === jobId) {
1575
+ unsubscribe();
1576
+ unsubscribeError();
1577
+ resolve(
1578
+ result as IOperationResult<DocumentDriveDocument>
1579
+ );
1580
+ }
1581
+ }
1582
+ );
1583
+ const unsubscribeError = this.queueManager.on(
1584
+ 'jobFailed',
1585
+ (job, error) => {
1586
+ if (job.jobId === jobId) {
1587
+ unsubscribe();
1588
+ unsubscribeError();
1589
+ reject(error);
1590
+ }
1591
+ }
1592
+ );
1593
+ }
1594
+ );
1079
1595
  }
1080
1596
 
1081
1597
  async addDriveOperations(
@@ -1089,12 +1605,23 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1089
1605
  const signals: SignalResult[] = [];
1090
1606
  let error: Error | undefined;
1091
1607
 
1608
+ // if operations are already stored then returns cached drive
1609
+ const result = await this.resultIfExistingDriveOperations(
1610
+ drive,
1611
+ operations
1612
+ );
1613
+ if (result) {
1614
+ return result;
1615
+ }
1616
+
1617
+ const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
1618
+
1092
1619
  try {
1093
1620
  await this._addDriveOperations(drive, async documentStorage => {
1094
1621
  const result = await this._processOperations<
1095
1622
  DocumentDriveDocument,
1096
1623
  DocumentDriveAction
1097
- >(drive, documentStorage, operations.slice());
1624
+ >(drive, undefined, documentStorage, operations.slice());
1098
1625
 
1099
1626
  document = result.document;
1100
1627
  operationsApplied.push(...result.operationsApplied);
@@ -1103,7 +1630,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1103
1630
 
1104
1631
  return {
1105
1632
  operations: result.operationsApplied,
1106
- header: result.document,
1633
+ header: result.document
1107
1634
  };
1108
1635
  });
1109
1636
 
@@ -1128,6 +1655,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1128
1655
  }
1129
1656
  }
1130
1657
 
1658
+ const syncUnits = await this.getSynchronizationUnitsIds(drive);
1659
+
1660
+ const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
1661
+ const syncUnitsIds = syncUnits.map(unit => unit.syncId);
1662
+
1663
+ const newSyncUnits = syncUnitsIds.filter(
1664
+ syncUnitId => !prevSyncUnitsIds.includes(syncUnitId)
1665
+ );
1666
+
1667
+ const removedSyncUnits = prevSyncUnitsIds.filter(
1668
+ syncUnitId => !syncUnitsIds.includes(syncUnitId)
1669
+ );
1670
+
1131
1671
  // update listener cache
1132
1672
  const lastOperation = operationsApplied
1133
1673
  .filter(op => op.scope === 'global')
@@ -1149,21 +1689,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1149
1689
  revision: lastOperation.index
1150
1690
  }
1151
1691
  ],
1152
- () => this.updateSyncStatus(drive, 'SYNCING'),
1692
+ () => {
1693
+ this.updateSyncStatus(drive, 'SYNCING');
1694
+
1695
+ for (const syncUnitId of [
1696
+ ...newSyncUnits,
1697
+ ...removedSyncUnits
1698
+ ]) {
1699
+ this.updateSyncStatus(syncUnitId, 'SYNCING');
1700
+ }
1701
+ },
1153
1702
  this.handleListenerError.bind(this),
1154
1703
  forceSync
1155
1704
  )
1156
- .then(
1157
- updates =>
1158
- updates.length &&
1159
- this.updateSyncStatus(drive, 'SUCCESS')
1160
- )
1705
+ .then(updates => {
1706
+ if (updates.length) {
1707
+ this.updateSyncStatus(drive, 'SUCCESS');
1708
+
1709
+ for (const syncUnitId of newSyncUnits) {
1710
+ this.updateSyncStatus(syncUnitId, 'SUCCESS');
1711
+ }
1712
+
1713
+ for (const syncUnitId of removedSyncUnits) {
1714
+ this.updateSyncStatus(syncUnitId, null);
1715
+ }
1716
+ }
1717
+ })
1161
1718
  .catch(error => {
1162
1719
  logger.error(
1163
1720
  'Non handled error updating sync revision',
1164
1721
  error
1165
1722
  );
1166
1723
  this.updateSyncStatus(drive, 'ERROR', error as Error);
1724
+
1725
+ for (const syncUnitId of [
1726
+ ...newSyncUnits,
1727
+ ...removedSyncUnits
1728
+ ]) {
1729
+ this.updateSyncStatus(
1730
+ syncUnitId,
1731
+ 'ERROR',
1732
+ error as Error
1733
+ );
1734
+ }
1167
1735
  });
1168
1736
  }
1169
1737
 
@@ -1190,11 +1758,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1190
1758
  error instanceof OperationError
1191
1759
  ? error
1192
1760
  : new OperationError(
1193
- 'ERROR',
1194
- undefined,
1195
- (error as Error).message,
1196
- (error as Error).cause
1197
- );
1761
+ 'ERROR',
1762
+ undefined,
1763
+ (error as Error).message,
1764
+ (error as Error).cause
1765
+ );
1198
1766
 
1199
1767
  return {
1200
1768
  status: operationError.status,
@@ -1226,36 +1794,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1226
1794
  async addAction(
1227
1795
  drive: string,
1228
1796
  id: string,
1229
- action: Action
1797
+ action: Action,
1798
+ forceSync = true
1230
1799
  ): Promise<IOperationResult> {
1231
- return this.addActions(drive, id, [action]);
1800
+ return this.addActions(drive, id, [action], forceSync);
1232
1801
  }
1233
1802
 
1234
1803
  async addActions(
1235
1804
  drive: string,
1236
1805
  id: string,
1237
- actions: Action[]
1806
+ actions: Action[],
1807
+ forceSync = true
1238
1808
  ): Promise<IOperationResult> {
1239
1809
  const document = await this.getDocument(drive, id);
1240
1810
  const operations = this._buildOperations(document, actions);
1241
- return this.queueOperations(drive, id, operations);
1811
+ return this.addOperations(drive, id, operations, forceSync);
1242
1812
  }
1243
1813
 
1244
1814
  async addDriveAction(
1245
1815
  drive: string,
1246
- action: DocumentDriveAction | BaseAction
1816
+ action: DocumentDriveAction | BaseAction,
1817
+ forceSync = true
1247
1818
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1248
- return this.addDriveActions(drive, [action]);
1819
+ return this.addDriveActions(drive, [action], forceSync);
1249
1820
  }
1250
1821
 
1251
1822
  async addDriveActions(
1252
1823
  drive: string,
1253
- actions: (DocumentDriveAction | BaseAction)[]
1824
+ actions: (DocumentDriveAction | BaseAction)[],
1825
+ forceSync = true
1254
1826
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1255
1827
  const document = await this.getDrive(drive);
1256
1828
  const operations = this._buildOperations(document, actions);
1257
- const result = await this.queueDriveOperations(drive, operations);
1258
- return result as IOperationResult<DocumentDriveDocument>;
1829
+ const result = await this.addDriveOperations(
1830
+ drive,
1831
+ operations,
1832
+ forceSync
1833
+ );
1834
+ return result;
1259
1835
  }
1260
1836
 
1261
1837
  async addInternalListener(