document-drive 1.0.0-experimental.9 → 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,6 +850,7 @@ 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
  ) {
@@ -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,26 +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);
732
-
733
- if (documentStorage.state && (!options || options.checkHashes === false)) {
734
- return documentStorage as T;
735
- }
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
+ );
736
972
 
737
973
  return baseUtils.replayDocument(
738
974
  documentStorage.initialState,
@@ -741,12 +977,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
741
977
  undefined,
742
978
  documentStorage,
743
979
  undefined,
744
- { checkHashes: options?.checkHashes ?? true }
980
+ {
981
+ ...options,
982
+ checkHashes: options?.checkHashes ?? true,
983
+ reuseOperationResultingState: options?.checkHashes ?? true
984
+ }
745
985
  ) as T;
746
986
  }
747
987
 
748
988
  private async _performOperation<T extends Document>(
749
989
  drive: string,
990
+ id: string | undefined,
750
991
  document: T,
751
992
  operation: Operation,
752
993
  skipHashValidation = false
@@ -756,6 +997,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
756
997
  const signalResults: SignalResult[] = [];
757
998
  let newDocument = document;
758
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
+
759
1030
  const operationSignals: (() => Promise<SignalResult>)[] = [];
760
1031
  newDocument = documentModel.reducer(
761
1032
  newDocument,
@@ -792,7 +1063,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
792
1063
  );
793
1064
  }
794
1065
  },
795
- { skip: operation.skip }
1066
+ { skip: operation.skip, reuseOperationResultingState: true }
796
1067
  ) as T;
797
1068
 
798
1069
  const appliedOperation = newDocument.operations[operation.scope].filter(
@@ -803,16 +1074,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
803
1074
  throw new OperationError(
804
1075
  'ERROR',
805
1076
  operation,
806
- `Operation with index ${operation.index}:${operation.skip} was not applied.`
1077
+ `Operation with index ${operation.index}:${operation.skip || 0} was not applied.`
807
1078
  );
808
1079
  } else if (
809
1080
  appliedOperation[0]!.hash !== operation.hash &&
810
1081
  !skipHashValidation
811
1082
  ) {
812
- throw new ConflictOperationError(
813
- operation,
814
- appliedOperation[0]!
815
- );
1083
+ throw new ConflictOperationError(operation, appliedOperation[0]!);
816
1084
  }
817
1085
 
818
1086
  for (const signalHandler of operationSignals) {
@@ -827,7 +1095,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
827
1095
  };
828
1096
  }
829
1097
 
830
- 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> {
831
1104
  return this.addOperations(drive, id, [operation], forceSync);
832
1105
  }
833
1106
 
@@ -837,21 +1110,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
837
1110
  callback: (document: DocumentStorage) => Promise<{
838
1111
  operations: Operation[];
839
1112
  header: DocumentHeader;
840
- newState: State<any, any> | undefined;
841
1113
  }>
842
1114
  ) {
843
1115
  if (!this.storage.addDocumentOperationsWithTransaction) {
844
1116
  const documentStorage = await this.storage.getDocument(drive, id);
845
1117
  const result = await callback(documentStorage);
846
1118
  // saves the applied operations to storage
847
- if (
848
- result.operations.length > 0
849
- ) {
1119
+ if (result.operations.length > 0) {
850
1120
  await this.storage.addDocumentOperations(
851
1121
  drive,
852
1122
  id,
853
1123
  result.operations,
854
- result.header,
1124
+ result.header
855
1125
  );
856
1126
  }
857
1127
  } else {
@@ -863,47 +1133,213 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
863
1133
  }
864
1134
  }
865
1135
 
866
- 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> {
867
1142
  return this.queueOperations(drive, id, [operation], forceSync);
868
1143
  }
869
1144
 
870
- 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,
871
1181
  id: string,
872
1182
  operations: Operation[],
873
- 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
+ });
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
+ }
874
1239
 
1240
+ async queueActions(
1241
+ drive: string,
1242
+ id: string,
1243
+ actions: Action[],
1244
+ forceSync?: boolean | undefined
1245
+ ): Promise<IOperationResult> {
875
1246
  try {
876
- 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
+ });
877
1253
 
878
1254
  return new Promise<IOperationResult>((resolve, reject) => {
879
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
880
- if (job.jobId === jobId) {
881
- unsubscribe();
882
- unsubscribeError();
883
- 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
+ }
884
1263
  }
885
- });
886
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
887
- console.log("test")
888
- if (job.jobId === jobId) {
889
- unsubscribe();
890
- unsubscribeError();
891
- 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
+ }
892
1273
  }
893
- });
894
- })
1274
+ );
1275
+ });
895
1276
  } catch (error) {
896
1277
  logger.error('Error adding job', error);
897
1278
  throw error;
898
1279
  }
899
1280
  }
900
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
+
901
1328
  async addOperations(
902
1329
  drive: string,
903
1330
  id: string,
904
1331
  operations: Operation[],
905
1332
  forceSync = true
906
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
+ }
907
1343
  let document: Document | undefined;
908
1344
  const operationsApplied: Operation[] = [];
909
1345
  const signals: SignalResult[] = [];
@@ -913,6 +1349,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
913
1349
  await this._addOperations(drive, id, async documentStorage => {
914
1350
  const result = await this._processOperations(
915
1351
  drive,
1352
+ id,
916
1353
  documentStorage,
917
1354
  operations
918
1355
  );
@@ -960,21 +1397,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
960
1397
  .updateSynchronizationRevisions(
961
1398
  drive,
962
1399
  syncUnits,
963
- () => 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
+ },
964
1407
  this.handleListenerError.bind(this),
965
1408
  forceSync
966
1409
  )
967
- .then(
968
- updates =>
969
- updates.length &&
970
- this.updateSyncStatus(drive, 'SUCCESS')
971
- )
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
+ })
972
1417
  .catch(error => {
973
1418
  logger.error(
974
1419
  'Non handled error updating sync revision',
975
1420
  error
976
1421
  );
977
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
+ }
978
1431
  });
979
1432
 
980
1433
  // after applying all the valid operations,throws
@@ -994,11 +1447,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
994
1447
  error instanceof OperationError
995
1448
  ? error
996
1449
  : new OperationError(
997
- 'ERROR',
998
- undefined,
999
- (error as Error).message,
1000
- (error as Error).cause
1001
- );
1450
+ 'ERROR',
1451
+ undefined,
1452
+ (error as Error).message,
1453
+ (error as Error).cause
1454
+ );
1002
1455
 
1003
1456
  return {
1004
1457
  status: operationError.status,
@@ -1053,33 +1506,92 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1053
1506
  }
1054
1507
  }
1055
1508
 
1056
- 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>> {
1057
1514
  return this.queueDriveOperations(drive, [operation], forceSync);
1058
1515
  }
1059
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
+
1060
1550
  async queueDriveOperations(
1061
1551
  drive: string,
1062
1552
  operations: Operation<DocumentDriveAction | BaseAction>[],
1063
1553
  forceSync = true
1064
1554
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1065
- const jobId = await this.queueManager.addJob({ driveId: drive, operations, forceSync });
1066
- return new Promise<IOperationResult<DocumentDriveDocument>>((resolve, reject) => {
1067
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1068
- if (job.jobId === jobId) {
1069
- unsubscribe();
1070
- unsubscribeError();
1071
- resolve(result as IOperationResult<DocumentDriveDocument>);
1072
- }
1073
- });
1074
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1075
- if (job.jobId === jobId) {
1076
- unsubscribe();
1077
- unsubscribeError();
1078
- reject(error);
1079
- }
1080
- });
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
+ }
1081
1563
 
1082
- })
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
+ );
1083
1595
  }
1084
1596
 
1085
1597
  async addDriveOperations(
@@ -1093,12 +1605,23 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1093
1605
  const signals: SignalResult[] = [];
1094
1606
  let error: Error | undefined;
1095
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
+
1096
1619
  try {
1097
1620
  await this._addDriveOperations(drive, async documentStorage => {
1098
1621
  const result = await this._processOperations<
1099
1622
  DocumentDriveDocument,
1100
1623
  DocumentDriveAction
1101
- >(drive, documentStorage, operations.slice());
1624
+ >(drive, undefined, documentStorage, operations.slice());
1102
1625
 
1103
1626
  document = result.document;
1104
1627
  operationsApplied.push(...result.operationsApplied);
@@ -1107,7 +1630,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1107
1630
 
1108
1631
  return {
1109
1632
  operations: result.operationsApplied,
1110
- header: result.document,
1633
+ header: result.document
1111
1634
  };
1112
1635
  });
1113
1636
 
@@ -1132,6 +1655,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1132
1655
  }
1133
1656
  }
1134
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
+
1135
1671
  // update listener cache
1136
1672
  const lastOperation = operationsApplied
1137
1673
  .filter(op => op.scope === 'global')
@@ -1153,21 +1689,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1153
1689
  revision: lastOperation.index
1154
1690
  }
1155
1691
  ],
1156
- () => 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
+ },
1157
1702
  this.handleListenerError.bind(this),
1158
1703
  forceSync
1159
1704
  )
1160
- .then(
1161
- updates =>
1162
- updates.length &&
1163
- this.updateSyncStatus(drive, 'SUCCESS')
1164
- )
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
+ })
1165
1718
  .catch(error => {
1166
1719
  logger.error(
1167
1720
  'Non handled error updating sync revision',
1168
1721
  error
1169
1722
  );
1170
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
+ }
1171
1735
  });
1172
1736
  }
1173
1737
 
@@ -1194,11 +1758,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1194
1758
  error instanceof OperationError
1195
1759
  ? error
1196
1760
  : new OperationError(
1197
- 'ERROR',
1198
- undefined,
1199
- (error as Error).message,
1200
- (error as Error).cause
1201
- );
1761
+ 'ERROR',
1762
+ undefined,
1763
+ (error as Error).message,
1764
+ (error as Error).cause
1765
+ );
1202
1766
 
1203
1767
  return {
1204
1768
  status: operationError.status,
@@ -1230,36 +1794,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1230
1794
  async addAction(
1231
1795
  drive: string,
1232
1796
  id: string,
1233
- action: Action
1797
+ action: Action,
1798
+ forceSync = true
1234
1799
  ): Promise<IOperationResult> {
1235
- return this.addActions(drive, id, [action]);
1800
+ return this.addActions(drive, id, [action], forceSync);
1236
1801
  }
1237
1802
 
1238
1803
  async addActions(
1239
1804
  drive: string,
1240
1805
  id: string,
1241
- actions: Action[]
1806
+ actions: Action[],
1807
+ forceSync = true
1242
1808
  ): Promise<IOperationResult> {
1243
1809
  const document = await this.getDocument(drive, id);
1244
1810
  const operations = this._buildOperations(document, actions);
1245
- return this.queueOperations(drive, id, operations);
1811
+ return this.addOperations(drive, id, operations, forceSync);
1246
1812
  }
1247
1813
 
1248
1814
  async addDriveAction(
1249
1815
  drive: string,
1250
- action: DocumentDriveAction | BaseAction
1816
+ action: DocumentDriveAction | BaseAction,
1817
+ forceSync = true
1251
1818
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1252
- return this.addDriveActions(drive, [action]);
1819
+ return this.addDriveActions(drive, [action], forceSync);
1253
1820
  }
1254
1821
 
1255
1822
  async addDriveActions(
1256
1823
  drive: string,
1257
- actions: (DocumentDriveAction | BaseAction)[]
1824
+ actions: (DocumentDriveAction | BaseAction)[],
1825
+ forceSync = true
1258
1826
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1259
1827
  const document = await this.getDrive(drive);
1260
1828
  const operations = this._buildOperations(document, actions);
1261
- const result = await this.queueDriveOperations(drive, operations);
1262
- return result as IOperationResult<DocumentDriveDocument>;
1829
+ const result = await this.addDriveOperations(
1830
+ drive,
1831
+ operations,
1832
+ forceSync
1833
+ );
1834
+ return result;
1263
1835
  }
1264
1836
 
1265
1837
  async addInternalListener(