document-drive 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -18,7 +18,7 @@ import {
18
18
  } from 'document-model/document';
19
19
  import { MemoryStorage } from '../storage/memory';
20
20
  import type { DocumentStorage, IDriveStorage } from '../storage/types';
21
- import { generateUUID, isDocumentDrive } from '../utils';
21
+ import { generateUUID, isDocumentDrive, isNoopUpdate } from '../utils';
22
22
  import { requestPublicDrive } from '../utils/graphql';
23
23
  import { OperationError } from './error';
24
24
  import { ListenerManager } from './listener/manager';
@@ -127,7 +127,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
127
127
  acknowledgeSuccess => {}
128
128
  );
129
129
  driveTriggers.set(trigger.id, intervalId);
130
- this.triggerMap.set(trigger.id, driveTriggers);
130
+ this.triggerMap.set(driveId, driveTriggers);
131
131
  }
132
132
  }
133
133
  }
@@ -465,18 +465,27 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
465
465
  operations: Operation<A | BaseAction>[]
466
466
  ) {
467
467
  const operationsApplied: Operation<A | BaseAction>[] = [];
468
+ const operationsUpdated: Operation<A | BaseAction>[] = [];
468
469
  let document: T | undefined;
469
470
  const signals: SignalResult[] = [];
470
471
 
471
472
  // eslint-disable-next-line prefer-const
472
- let [operationsToApply, error] = this._validateOperations(
473
- operations,
474
- documentStorage
475
- );
473
+ let [operationsToApply, error, updatedOperations] =
474
+ this._validateOperations(operations, documentStorage);
475
+
476
+ const unregisteredOps = [
477
+ ...operationsToApply.map(operation => ({ operation, type: 'new' })),
478
+ ...updatedOperations.map(operation => ({
479
+ operation,
480
+ type: 'update'
481
+ }))
482
+ ].sort((a, b) => a.operation.index - b.operation.index);
476
483
 
477
484
  // retrieves the document's document model and
478
485
  // applies the operations using its reducer
479
- for (const operation of operationsToApply) {
486
+ for (const unregisteredOp of unregisteredOps) {
487
+ const { operation, type } = unregisteredOp;
488
+
480
489
  try {
481
490
  const {
482
491
  document: newDocument,
@@ -488,7 +497,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
488
497
  operation
489
498
  );
490
499
  document = newDocument;
491
- operationsApplied.push(appliedOperation);
500
+
501
+ if (type === 'new') {
502
+ operationsApplied.push(appliedOperation);
503
+ } else {
504
+ operationsUpdated.push(appliedOperation);
505
+ }
506
+
492
507
  signals.push(...signals);
493
508
  } catch (e) {
494
509
  if (!error) {
@@ -505,7 +520,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
505
520
  break;
506
521
  }
507
522
  }
508
- return { document, operationsApplied, signals, error } as const;
523
+
524
+ if (!document) {
525
+ document = this._buildDocument(documentStorage);
526
+ }
527
+
528
+ return {
529
+ document,
530
+ operationsApplied,
531
+ signals,
532
+ error,
533
+ operationsUpdated
534
+ } as const;
509
535
  }
510
536
 
511
537
  private _validateOperations<T extends Document, A extends Action>(
@@ -513,6 +539,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
513
539
  documentStorage: DocumentStorage<T>
514
540
  ) {
515
541
  const operationsToApply: Operation<A | BaseAction>[] = [];
542
+ const updatedOperations: Operation<A | BaseAction>[] = [];
516
543
  let error: OperationError | undefined;
517
544
 
518
545
  // sort operations so from smaller index to biggest
@@ -525,7 +552,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
525
552
  .slice(0, i);
526
553
  const scopeOperations = documentStorage.operations[op.scope];
527
554
 
528
- const nextIndex = scopeOperations.length + pastOperations.length;
555
+ // get latest operation
556
+ const ops = [...scopeOperations, ...pastOperations];
557
+ const latestOperation = ops.slice().pop();
558
+
559
+ const noopUpdate = isNoopUpdate(op, latestOperation);
560
+
561
+ let nextIndex = scopeOperations.length + pastOperations.length;
562
+ if (noopUpdate) {
563
+ nextIndex = nextIndex - 1;
564
+ }
565
+
529
566
  if (op.index > nextIndex) {
530
567
  error = new OperationError(
531
568
  'MISSING',
@@ -534,39 +571,54 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
534
571
  );
535
572
  continue;
536
573
  } else if (op.index < nextIndex) {
537
- const existingOperation =
538
- scopeOperations.concat(pastOperations)[op.index];
574
+ const existingOperation = scopeOperations.find(
575
+ existingOperation => existingOperation.index === op.index
576
+ );
539
577
  if (existingOperation && existingOperation.hash !== op.hash) {
540
578
  error = new OperationError(
541
579
  'CONFLICT',
542
580
  op,
543
- `Conflicting operation on index ${op.index}`
581
+ `Conflicting operation on index ${op.index}`,
582
+ { existingOperation, newOperation: op }
544
583
  );
545
584
  continue;
546
585
  }
547
586
  } else {
548
- operationsToApply.push(op);
587
+ if (noopUpdate) {
588
+ updatedOperations.push(op);
589
+ } else {
590
+ operationsToApply.push(op);
591
+ }
549
592
  }
550
593
  }
551
594
 
552
- return [operationsToApply, error] as const;
595
+ return [operationsToApply, error, updatedOperations] as const;
553
596
  }
554
597
 
555
- private async _performOperation<T extends Document, A extends Action>(
556
- drive: string,
557
- documentStorage: DocumentStorage<T>,
558
- operation: Operation<A | BaseAction>
559
- ) {
598
+ private _buildDocument<T extends Document>(
599
+ documentStorage: DocumentStorage<T>
600
+ ): T {
560
601
  const documentModel = this._getDocumentModel(
561
602
  documentStorage.documentType
562
603
  );
563
- const document = baseUtils.replayDocument(
604
+ return baseUtils.replayDocument(
564
605
  documentStorage.initialState,
565
606
  documentStorage.operations,
566
607
  documentModel.reducer,
567
608
  undefined,
568
609
  documentStorage
569
610
  ) as T;
611
+ }
612
+
613
+ private async _performOperation<T extends Document, A extends Action>(
614
+ drive: string,
615
+ documentStorage: DocumentStorage<T>,
616
+ operation: Operation<A | BaseAction>
617
+ ) {
618
+ const documentModel = this._getDocumentModel(
619
+ documentStorage.documentType
620
+ );
621
+ const document = this._buildDocument(documentStorage);
570
622
 
571
623
  const signalResults: SignalResult[] = [];
572
624
  let newDocument = document;
@@ -634,6 +686,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
634
686
 
635
687
  let document: Document | undefined;
636
688
  const operationsApplied: Operation[] = [];
689
+ const updatedOperations: Operation[] = [];
637
690
  const signals: SignalResult[] = [];
638
691
  let error: Error | undefined;
639
692
 
@@ -647,7 +700,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
647
700
  );
648
701
 
649
702
  document = result.document;
703
+
650
704
  operationsApplied.push(...result.operationsApplied);
705
+ updatedOperations.push(...result.operationsUpdated);
706
+
651
707
  signals.push(...result.signals);
652
708
  error = result.error;
653
709
 
@@ -656,15 +712,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
656
712
  }
657
713
 
658
714
  // saves the applied operations to storage
659
- await this.storage.addDocumentOperations(
660
- drive,
661
- id,
662
- operationsApplied,
663
- document
664
- );
715
+ if (operationsApplied.length > 0 || updatedOperations.length > 0) {
716
+ await this.storage.addDocumentOperations(
717
+ drive,
718
+ id,
719
+ operationsApplied,
720
+ document,
721
+ updatedOperations
722
+ );
723
+ }
665
724
 
666
725
  // gets all the different scopes and branches combinations from the operations
667
- const { scopes, branches } = operationsApplied.reduce(
726
+ const { scopes, branches } = [
727
+ ...operationsApplied,
728
+ ...updatedOperations
729
+ ].reduce(
668
730
  (acc, operation) => {
669
731
  if (!acc.scopes.includes(operation.scope)) {
670
732
  acc.scopes.push(operation.scope);
@@ -682,12 +744,16 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
682
744
  );
683
745
  // update listener cache
684
746
  for (const syncUnit of syncUnits) {
685
- await this.listenerStateManager.updateSynchronizationRevision(
686
- drive,
687
- syncUnit.syncId,
688
- syncUnit.revision,
689
- syncUnit.lastUpdated
690
- );
747
+ this.listenerStateManager
748
+ .updateSynchronizationRevision(
749
+ drive,
750
+ syncUnit.syncId,
751
+ syncUnit.revision,
752
+ syncUnit.lastUpdated
753
+ )
754
+ .catch(error => {
755
+ console.error('Error updating sync revision', error);
756
+ });
691
757
  }
692
758
 
693
759
  // after applying all the valid operations,throws
@@ -759,11 +825,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
759
825
  }
760
826
 
761
827
  // saves the applied operations to storage
762
- await this.storage.addDriveOperations(
763
- drive,
764
- operationsApplied,
765
- document
766
- );
828
+ if (operationsApplied.length > 0) {
829
+ await this.storage.addDriveOperations(
830
+ drive,
831
+ operationsApplied,
832
+ document
833
+ );
834
+ }
767
835
 
768
836
  for (const operation of operationsApplied) {
769
837
  if (operation.type === 'ADD_LISTENER') {
@@ -802,12 +870,16 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
802
870
  .slice()
803
871
  .pop();
804
872
  if (lastOperation) {
805
- await this.listenerStateManager.updateSynchronizationRevision(
806
- drive,
807
- '0',
808
- lastOperation.index,
809
- lastOperation.timestamp
810
- );
873
+ this.listenerStateManager
874
+ .updateSynchronizationRevision(
875
+ drive,
876
+ '0',
877
+ lastOperation.index,
878
+ lastOperation.timestamp
879
+ )
880
+ .catch(error => {
881
+ console.error('Error updating sync revision', error);
882
+ });
811
883
  }
812
884
 
813
885
  if (this.shouldSyncRemoteDrive(document)) {
@@ -76,6 +76,11 @@ export class PullResponderTransmitter implements ITransmitter {
76
76
  fromRevision: entry.listenerRev
77
77
  }
78
78
  );
79
+
80
+ if (!operations.length) {
81
+ continue;
82
+ }
83
+
79
84
  strands.push({
80
85
  driveId,
81
86
  documentId,
@@ -94,12 +99,18 @@ export class PullResponderTransmitter implements ITransmitter {
94
99
  revisions: ListenerRevision[]
95
100
  ): Promise<boolean> {
96
101
  const listener = this.manager.getListener(driveId, listenerId);
102
+
97
103
  let success = true;
98
104
  for (const revision of revisions) {
99
- const syncId = listener.syncUnits.find(
100
- s => s.scope === revision.scope && s.branch === revision.branch && s.documentId === revision.documentId && s.driveId === driveId
101
- )?.syncId;
102
- if (!syncId) {
105
+ const syncUnit = listener.syncUnits.find(
106
+ s =>
107
+ s.scope === revision.scope &&
108
+ s.branch === revision.branch &&
109
+ s.driveId === revision.driveId &&
110
+ s.documentId == revision.documentId
111
+ );
112
+ if (!syncUnit) {
113
+ console.log('Sync unit not found', revision);
103
114
  success = false;
104
115
  continue;
105
116
  }
@@ -107,7 +118,7 @@ export class PullResponderTransmitter implements ITransmitter {
107
118
  await this.manager.updateListenerRevision(
108
119
  listenerId,
109
120
  driveId,
110
- syncId,
121
+ syncUnit.syncId,
111
122
  revision.revision
112
123
  );
113
124
  }
@@ -235,6 +246,12 @@ export class PullResponderTransmitter implements ITransmitter {
235
246
  listenerId
236
247
  // since ?
237
248
  );
249
+
250
+ // if there are no new strands then do nothing
251
+ if (!strands.length) {
252
+ return;
253
+ }
254
+
238
255
  const listenerRevisions: ListenerRevision[] = [];
239
256
 
240
257
  for (const strand of strands) {
@@ -5,7 +5,7 @@ import {
5
5
  DocumentHeader,
6
6
  Operation
7
7
  } from 'document-model/document';
8
- import { mergeOperations } from '..';
8
+ import { applyUpdatedOperations, mergeOperations } from '..';
9
9
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
10
10
 
11
11
  export class BrowserStorage implements IDriveStorage {
@@ -57,7 +57,8 @@ export class BrowserStorage implements IDriveStorage {
57
57
  drive: string,
58
58
  id: string,
59
59
  operations: Operation[],
60
- header: DocumentHeader
60
+ header: DocumentHeader,
61
+ updatedOperations: Operation[] = []
61
62
  ): Promise<void> {
62
63
  const document = await this.getDocument(drive, id);
63
64
  if (!document) {
@@ -69,12 +70,17 @@ export class BrowserStorage implements IDriveStorage {
69
70
  operations
70
71
  );
71
72
 
73
+ const mergedUpdatedOperations = applyUpdatedOperations(
74
+ mergedOperations,
75
+ updatedOperations
76
+ );
77
+
72
78
  await (
73
79
  await this.db
74
80
  ).setItem(this.buildKey(drive, id), {
75
81
  ...document,
76
82
  ...header,
77
- operations: mergedOperations
83
+ operations: mergedUpdatedOperations
78
84
  });
79
85
  }
80
86
 
@@ -12,7 +12,7 @@ import fs from 'fs/promises';
12
12
  import stringify from 'json-stringify-deterministic';
13
13
  import path from 'path';
14
14
  import sanitize from 'sanitize-filename';
15
- import { mergeOperations } from '..';
15
+ import { applyUpdatedOperations, mergeOperations } from '..';
16
16
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
17
17
 
18
18
  type FSError = {
@@ -104,7 +104,8 @@ export class FilesystemStorage implements IDriveStorage {
104
104
  drive: string,
105
105
  id: string,
106
106
  operations: Operation[],
107
- header: DocumentHeader
107
+ header: DocumentHeader,
108
+ updatedOperations: Operation[] = []
108
109
  ) {
109
110
  const document = await this.getDocument(drive, id);
110
111
  if (!document) {
@@ -116,10 +117,15 @@ export class FilesystemStorage implements IDriveStorage {
116
117
  operations
117
118
  );
118
119
 
120
+ const mergedUpdatedOperations = applyUpdatedOperations(
121
+ mergedOperations,
122
+ updatedOperations
123
+ );
124
+
119
125
  this.createDocument(drive, id, {
120
126
  ...document,
121
127
  ...header,
122
- operations: mergedOperations
128
+ operations: mergedUpdatedOperations
123
129
  });
124
130
  }
125
131
 
@@ -5,7 +5,7 @@ import {
5
5
  DocumentHeader,
6
6
  Operation
7
7
  } from 'document-model/document';
8
- import { mergeOperations } from '..';
8
+ import { applyUpdatedOperations, mergeOperations } from '..';
9
9
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
10
10
 
11
11
  export class MemoryStorage implements IDriveStorage {
@@ -67,7 +67,8 @@ export class MemoryStorage implements IDriveStorage {
67
67
  drive: string,
68
68
  id: string,
69
69
  operations: Operation[],
70
- header: DocumentHeader
70
+ header: DocumentHeader,
71
+ updatedOperations: Operation[] = []
71
72
  ): Promise<void> {
72
73
  const document = await this.getDocument(drive, id);
73
74
  if (!document) {
@@ -79,10 +80,15 @@ export class MemoryStorage implements IDriveStorage {
79
80
  operations
80
81
  );
81
82
 
83
+ const mergedUpdatedOperations = applyUpdatedOperations(
84
+ mergedOperations,
85
+ updatedOperations
86
+ );
87
+
82
88
  this.documents[drive]![id] = {
83
89
  ...document,
84
90
  ...header,
85
- operations: mergedOperations
91
+ operations: mergedUpdatedOperations
86
92
  };
87
93
  }
88
94
 
@@ -1,9 +1,9 @@
1
- import { type Prisma } from '@prisma/client';
1
+ import { PrismaClient, type Prisma } from '@prisma/client';
2
2
  import {
3
3
  DocumentDriveLocalState,
4
4
  DocumentDriveState
5
5
  } from 'document-model-libs/document-drive';
6
- import {
6
+ import type {
7
7
  DocumentHeader,
8
8
  ExtendedState,
9
9
  Operation,
@@ -12,16 +12,16 @@ import {
12
12
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
13
13
 
14
14
  export class PrismaStorage implements IDriveStorage {
15
- private db: Prisma.TransactionClient;
15
+ private db: PrismaClient;
16
16
 
17
- constructor(db: Prisma.TransactionClient) {
17
+ constructor(db: PrismaClient) {
18
18
  this.db = db;
19
19
  }
20
20
 
21
21
  async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
22
+ // drive for all drive documents
22
23
  await this.createDocument('drives', id, drive as DocumentStorage);
23
24
  }
24
-
25
25
  async addDriveOperations(
26
26
  id: string,
27
27
  operations: Operation[],
@@ -166,6 +166,9 @@ export class PrismaStorage implements IDriveStorage {
166
166
  },
167
167
  include: {
168
168
  operations: {
169
+ orderBy: {
170
+ index: 'asc'
171
+ },
169
172
  include: {
170
173
  attachments: true
171
174
  }
@@ -225,7 +228,7 @@ export class PrismaStorage implements IDriveStorage {
225
228
  scope: op.scope as OperationScope
226
229
  // attachments: fileRegistry
227
230
  })),
228
- revision: dbDoc.revision as Required<Record<OperationScope, number>>
231
+ revision: dbDoc.revision as Record<OperationScope, number>
229
232
  };
230
233
 
231
234
  return doc;
@@ -281,7 +284,12 @@ export class PrismaStorage implements IDriveStorage {
281
284
  }
282
285
 
283
286
  async getDrive(id: string) {
284
- return this.getDocument('drives', id) as Promise<DocumentDriveStorage>;
287
+ try {
288
+ const doc = await this.getDocument('drives', id);
289
+ return doc as DocumentDriveStorage;
290
+ } catch (e) {
291
+ throw new Error(`Drive with id ${id} not found`);
292
+ }
285
293
  }
286
294
 
287
295
  async deleteDrive(id: string) {
@@ -27,7 +27,8 @@ export interface IStorage {
27
27
  drive: string,
28
28
  id: string,
29
29
  operations: Operation[],
30
- header: DocumentHeader
30
+ header: DocumentHeader,
31
+ updatedOperations?: Operation[]
31
32
  ): Promise<void>;
32
33
  deleteDocument(drive: string, id: string): Promise<void>;
33
34
  }
@@ -38,3 +38,40 @@ export function generateUUID(): string {
38
38
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
39
39
  return crypto.randomUUID() as string;
40
40
  }
41
+
42
+ export function applyUpdatedOperations<A extends Action = Action>(
43
+ currentOperations: DocumentOperations<A>,
44
+ updatedOperations: Operation<A | BaseAction>[]
45
+ ): DocumentOperations<A> {
46
+ return updatedOperations.reduce(
47
+ (acc, curr) => {
48
+ const operations = acc[curr.scope] ?? [];
49
+ acc[curr.scope] = operations.map(op => {
50
+ return op.index === curr.index ? curr : op;
51
+ });
52
+ return acc;
53
+ },
54
+ { ...currentOperations }
55
+ );
56
+ }
57
+
58
+ export function isNoopUpdate(
59
+ operation: Operation,
60
+ latestOperation?: Operation
61
+ ) {
62
+ if (!latestOperation) {
63
+ return false;
64
+ }
65
+
66
+ const isNoopOp = operation.type === 'NOOP';
67
+ const isNoopLatestOp = latestOperation.type === 'NOOP';
68
+ const isSameIndexOp = operation.index === latestOperation.index;
69
+ const isSkipOpGreaterThanLatestOp = operation.skip > latestOperation.skip;
70
+
71
+ return (
72
+ isNoopOp &&
73
+ isNoopLatestOp &&
74
+ isSameIndexOp &&
75
+ isSkipOpGreaterThanLatestOp
76
+ );
77
+ }