document-drive 0.0.26 → 0.0.28

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.
@@ -1,34 +1,283 @@
1
- import { DocumentDriveAction, utils } from 'document-model-libs/document-drive';
2
1
  import {
2
+ DocumentDriveAction,
3
+ DocumentDriveDocument,
4
+ DocumentDriveState,
5
+ FileNode,
6
+ Trigger,
7
+ isFileNode,
8
+ utils
9
+ } from 'document-model-libs/document-drive';
10
+ import {
11
+ Action,
3
12
  BaseAction,
13
+ Document,
4
14
  DocumentModel,
5
15
  Operation,
16
+ OperationScope,
6
17
  utils as baseUtils
7
18
  } from 'document-model/document';
8
- import { DocumentStorage, IDriveStorage } from '../storage';
9
19
  import { MemoryStorage } from '../storage/memory';
10
- import { isDocumentDrive } from '../utils';
20
+ import type { DocumentStorage, IDriveStorage } from '../storage/types';
21
+ import { generateUUID, isDocumentDrive } from '../utils';
22
+ import { requestPublicDrive } from '../utils/graphql';
23
+ import { OperationError } from './error';
24
+ import { ListenerManager } from './listener/manager';
25
+ import { PullResponderTransmitter } from './listener/transmitter';
26
+ import type { ITransmitter } from './listener/transmitter/types';
11
27
  import {
12
- CreateDocumentInput,
13
- DriveInput,
14
- IDocumentDriveServer,
15
- SignalResult
28
+ BaseDocumentDriveServer,
29
+ IOperationResult,
30
+ RemoteDriveOptions,
31
+ StrandUpdate,
32
+ SyncStatus,
33
+ type CreateDocumentInput,
34
+ type DriveInput,
35
+ type OperationUpdate,
36
+ type SignalResult,
37
+ type SynchronizationUnit
16
38
  } from './types';
17
39
 
40
+ export * from './listener';
18
41
  export type * from './types';
19
42
 
20
- export class DocumentDriveServer implements IDocumentDriveServer {
43
+ export const PULL_DRIVE_INTERVAL = 5000;
44
+
45
+ export class DocumentDriveServer extends BaseDocumentDriveServer {
21
46
  private documentModels: DocumentModel[];
22
47
  private storage: IDriveStorage;
48
+ private listenerStateManager: ListenerManager;
49
+ private triggerMap = new Map<
50
+ DocumentDriveState['id'],
51
+ Map<Trigger['id'], number>
52
+ >();
53
+ private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
23
54
 
24
55
  constructor(
25
56
  documentModels: DocumentModel[],
26
57
  storage: IDriveStorage = new MemoryStorage()
27
58
  ) {
59
+ super();
60
+ this.listenerStateManager = new ListenerManager(this);
28
61
  this.documentModels = documentModels;
29
62
  this.storage = storage;
30
63
  }
31
64
 
65
+ private async saveStrand(strand: StrandUpdate) {
66
+ const operations: Operation[] = strand.operations.map(
67
+ ({ index, type, hash, input, skip, timestamp }) => ({
68
+ index,
69
+ type,
70
+ hash,
71
+ input,
72
+ skip,
73
+ timestamp,
74
+ scope: strand.scope,
75
+ branch: strand.branch
76
+ })
77
+ );
78
+
79
+ const result = await (!strand.documentId
80
+ ? this.addDriveOperations(
81
+ strand.driveId,
82
+ operations as Operation<DocumentDriveAction | BaseAction>[]
83
+ )
84
+ : this.addOperations(
85
+ strand.driveId,
86
+ strand.documentId,
87
+ operations
88
+ ));
89
+ this.syncStatus.set(strand.driveId, result.status);
90
+ return result;
91
+ }
92
+
93
+ private shouldSyncRemoteDrive(drive: DocumentDriveDocument) {
94
+ return (
95
+ drive.state.local.availableOffline &&
96
+ drive.state.local.triggers.length > 0
97
+ );
98
+ }
99
+
100
+ private async startSyncRemoteDrive(driveId: string) {
101
+ const drive = await this.getDrive(driveId);
102
+ let driveTriggers = this.triggerMap.get(driveId);
103
+
104
+ for (const trigger of drive.state.local.triggers) {
105
+ if (driveTriggers?.get(trigger.id)) {
106
+ continue;
107
+ }
108
+
109
+ if (!driveTriggers) {
110
+ driveTriggers = new Map();
111
+ this.syncStatus.set(driveId, 'SYNCING');
112
+ }
113
+
114
+ if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
115
+ const intervalId = PullResponderTransmitter.setupPull(
116
+ driveId,
117
+ trigger,
118
+ this.saveStrand.bind(this),
119
+ error => {
120
+ this.syncStatus.set(
121
+ driveId,
122
+ error instanceof OperationError
123
+ ? error.status
124
+ : 'ERROR'
125
+ );
126
+ },
127
+ acknowledgeSuccess => {}
128
+ );
129
+ driveTriggers.set(trigger.id, intervalId);
130
+ this.triggerMap.set(trigger.id, driveTriggers);
131
+ }
132
+ }
133
+ }
134
+
135
+ private async stopSyncRemoteDrive(driveId: string) {
136
+ const triggers = this.triggerMap.get(driveId);
137
+ triggers?.forEach(clearInterval);
138
+ return this.triggerMap.delete(driveId);
139
+ }
140
+
141
+ async initialize() {
142
+ await this.listenerStateManager.init();
143
+ const drives = await this.getDrives();
144
+ for (const id of drives) {
145
+ const drive = await this.getDrive(id);
146
+ if (this.shouldSyncRemoteDrive(drive)) {
147
+ this.startSyncRemoteDrive(id);
148
+ }
149
+ }
150
+ }
151
+
152
+ public async getSynchronizationUnits(
153
+ driveId: string,
154
+ documentId?: string[],
155
+ scope?: string[],
156
+ branch?: string[]
157
+ ) {
158
+ const drive = await this.getDrive(driveId);
159
+
160
+ const nodes = drive.state.global.nodes.filter(
161
+ node =>
162
+ isFileNode(node) &&
163
+ (!documentId?.length || documentId.includes(node.id)) // TODO support * as documentId
164
+ ) as FileNode[];
165
+
166
+ if (documentId && !nodes.length) {
167
+ throw new Error('File node not found');
168
+ }
169
+
170
+ const synchronizationUnits: SynchronizationUnit[] = [];
171
+
172
+ for (const node of nodes) {
173
+ const nodeUnits =
174
+ scope?.length || branch?.length
175
+ ? node.synchronizationUnits.filter(
176
+ unit =>
177
+ (!scope?.length || scope.includes(unit.scope)) &&
178
+ (!branch?.length || branch.includes(unit.branch))
179
+ )
180
+ : node.synchronizationUnits;
181
+ if (!nodeUnits.length) {
182
+ continue;
183
+ }
184
+
185
+ const document = await this.getDocument(driveId, node.id);
186
+
187
+ for (const { syncId, scope, branch } of nodeUnits) {
188
+ const operations =
189
+ document.operations[scope as OperationScope] ?? [];
190
+ const lastOperation = operations.pop();
191
+ synchronizationUnits.push({
192
+ syncId,
193
+ scope,
194
+ branch,
195
+ driveId,
196
+ documentId: node.id,
197
+ documentType: node.documentType,
198
+ lastUpdated:
199
+ lastOperation?.timestamp ?? document.lastModified,
200
+ revision: lastOperation?.index ?? 0
201
+ });
202
+ }
203
+ }
204
+ return synchronizationUnits;
205
+ }
206
+
207
+ public async getSynchronizationUnit(
208
+ driveId: string,
209
+ syncId: string
210
+ ): Promise<SynchronizationUnit> {
211
+ const drive = await this.getDrive(driveId);
212
+ const node = drive.state.global.nodes.find(
213
+ node =>
214
+ isFileNode(node) &&
215
+ node.synchronizationUnits.find(unit => unit.syncId === syncId)
216
+ );
217
+
218
+ if (!node || !isFileNode(node)) {
219
+ throw new Error('Synchronization unit not found');
220
+ }
221
+
222
+ const { scope, branch } = node.synchronizationUnits.find(
223
+ unit => unit.syncId === syncId
224
+ )!;
225
+
226
+ const documentId = node.id;
227
+ const document = await this.getDocument(driveId, documentId);
228
+ const operations = document.operations[scope as OperationScope] ?? [];
229
+ const lastOperation = operations.pop();
230
+
231
+ return {
232
+ syncId,
233
+ scope,
234
+ branch,
235
+ driveId,
236
+ documentId,
237
+ documentType: node.documentType,
238
+ lastUpdated: lastOperation?.timestamp ?? document.lastModified,
239
+ revision: lastOperation?.index ?? 0
240
+ };
241
+ }
242
+
243
+ async getOperationData(
244
+ driveId: string,
245
+ syncId: string,
246
+ filter: {
247
+ since?: string | undefined;
248
+ fromRevision?: number | undefined;
249
+ }
250
+ ): Promise<OperationUpdate[]> {
251
+ const { documentId, scope } =
252
+ syncId === '0'
253
+ ? { documentId: '', scope: 'global' }
254
+ : await this.getSynchronizationUnit(driveId, syncId);
255
+
256
+ const document =
257
+ syncId === '0'
258
+ ? await this.getDrive(driveId)
259
+ : await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
260
+
261
+ const operations = document.operations[scope as OperationScope] ?? []; // TODO filter by branch also
262
+ const filteredOperations = operations.filter(
263
+ operation =>
264
+ Object.keys(filter).length === 0 ||
265
+ (filter.since !== undefined &&
266
+ filter.since <= operation.timestamp) ||
267
+ (filter.fromRevision !== undefined &&
268
+ operation.index >= filter.fromRevision)
269
+ );
270
+
271
+ return filteredOperations.map(operation => ({
272
+ hash: operation.hash,
273
+ index: operation.index,
274
+ timestamp: operation.timestamp,
275
+ type: operation.type,
276
+ input: operation.input as object,
277
+ skip: operation.skip
278
+ }));
279
+ }
280
+
32
281
  private _getDocumentModel(documentType: string) {
33
282
  const documentModel = this.documentModels.find(
34
283
  model => model.documentModel.id === documentType
@@ -40,7 +289,7 @@ export class DocumentDriveServer implements IDocumentDriveServer {
40
289
  }
41
290
 
42
291
  async addDrive(drive: DriveInput) {
43
- const id = drive.global.id;
292
+ const id = drive.global.id ?? generateUUID();
44
293
  if (!id) {
45
294
  throw new Error('Invalid Drive Id');
46
295
  }
@@ -55,10 +304,83 @@ export class DocumentDriveServer implements IDocumentDriveServer {
55
304
  const document = utils.createDocument({
56
305
  state: drive
57
306
  });
58
- return this.storage.createDrive(id, document);
307
+
308
+ await this.storage.createDrive(id, document);
309
+
310
+ // add listeners to state manager
311
+ for (const listener of drive.local.listeners) {
312
+ await this.listenerStateManager.addListener({
313
+ block: listener.block,
314
+ driveId: id,
315
+ filter: {
316
+ branch: listener.filter.branch ?? [],
317
+ documentId: listener.filter.documentId ?? [],
318
+ documentType: listener.filter.documentType ?? [],
319
+ scope: listener.filter.scope ?? []
320
+ },
321
+ listenerId: listener.listenerId,
322
+ system: listener.system,
323
+ callInfo: listener.callInfo ?? undefined,
324
+ label: listener.label ?? ''
325
+ });
326
+ }
327
+
328
+ // if it is a remote drive that should be available offline, starts
329
+ // the sync process to pull changes from remote every 30 seconds
330
+ if (this.shouldSyncRemoteDrive(document)) {
331
+ await this.startSyncRemoteDrive(id);
332
+ }
333
+ }
334
+
335
+ async addRemoteDrive(url: string, options: RemoteDriveOptions) {
336
+ const { id, name, slug, icon } = await requestPublicDrive(url);
337
+ const {
338
+ pullFilter,
339
+ pullInterval,
340
+ availableOffline,
341
+ sharingType,
342
+ listeners,
343
+ triggers
344
+ } = options;
345
+ const listenerId = await PullResponderTransmitter.registerPullResponder(
346
+ id,
347
+ url,
348
+ pullFilter ?? {
349
+ documentId: ['*'],
350
+ documentType: ['*'],
351
+ branch: ['*'],
352
+ scope: ['*']
353
+ }
354
+ );
355
+
356
+ const pullTrigger: Trigger = {
357
+ id: generateUUID(),
358
+ type: 'PullResponder',
359
+ data: {
360
+ url,
361
+ listenerId,
362
+ interval: pullInterval?.toString() ?? ''
363
+ }
364
+ };
365
+
366
+ return await this.addDrive({
367
+ global: {
368
+ id: id,
369
+ name,
370
+ slug,
371
+ icon: icon ?? null
372
+ },
373
+ local: {
374
+ triggers: [...triggers, pullTrigger],
375
+ listeners: listeners,
376
+ availableOffline,
377
+ sharingType
378
+ }
379
+ });
59
380
  }
60
381
 
61
382
  deleteDrive(id: string) {
383
+ this.stopSyncRemoteDrive(id);
62
384
  return this.storage.deleteDrive(id);
63
385
  }
64
386
 
@@ -104,23 +426,136 @@ export class DocumentDriveServer implements IDocumentDriveServer {
104
426
  return this.storage.getDocuments(drive);
105
427
  }
106
428
 
107
- async createDocument(driveId: string, input: CreateDocumentInput) {
429
+ protected async createDocument(
430
+ driveId: string,
431
+ input: CreateDocumentInput
432
+ ) {
108
433
  const documentModel = this._getDocumentModel(input.documentType);
109
-
110
434
  // TODO validate input.document is of documentType
111
435
  const document = input.document ?? documentModel.utils.createDocument();
112
436
 
113
- return this.storage.createDocument(driveId, input.id, document);
437
+ await this.storage.createDocument(driveId, input.id, document);
438
+
439
+ await this.listenerStateManager.addSyncUnits(
440
+ input.synchronizationUnits.map(({ syncId, scope, branch }) => {
441
+ const lastOperation = document.operations[scope].slice().pop();
442
+ return {
443
+ syncId,
444
+ scope,
445
+ branch,
446
+ driveId,
447
+ documentId: input.id,
448
+ documentType: document.documentType,
449
+ lastUpdated:
450
+ lastOperation?.timestamp ?? document.lastModified,
451
+ revision: lastOperation?.index ?? 0
452
+ };
453
+ })
454
+ );
455
+ return document;
114
456
  }
115
457
 
116
458
  async deleteDocument(driveId: string, id: string) {
117
459
  return this.storage.deleteDocument(driveId, id);
118
460
  }
119
461
 
120
- private async _performOperations(
462
+ async _processOperations<T extends Document, A extends Action>(
121
463
  drive: string,
122
- documentStorage: DocumentStorage,
123
- operations: Operation[]
464
+ documentStorage: DocumentStorage<T>,
465
+ operations: Operation<A | BaseAction>[]
466
+ ) {
467
+ const operationsApplied: Operation<A | BaseAction>[] = [];
468
+ let document: T | undefined;
469
+ const signals: SignalResult[] = [];
470
+
471
+ // eslint-disable-next-line prefer-const
472
+ let [operationsToApply, error] = this._validateOperations(
473
+ operations,
474
+ documentStorage
475
+ );
476
+
477
+ // retrieves the document's document model and
478
+ // applies the operations using its reducer
479
+ for (const operation of operationsToApply) {
480
+ try {
481
+ const {
482
+ document: newDocument,
483
+ signals,
484
+ operation: appliedOperation
485
+ } = await this._performOperation(
486
+ drive,
487
+ document ?? documentStorage,
488
+ operation
489
+ );
490
+ document = newDocument;
491
+ operationsApplied.push(appliedOperation);
492
+ signals.push(...signals);
493
+ } catch (e) {
494
+ if (!error) {
495
+ error =
496
+ e instanceof OperationError
497
+ ? e
498
+ : new OperationError(
499
+ 'ERROR',
500
+ operation,
501
+ (e as Error).message,
502
+ (e as Error).cause
503
+ );
504
+ }
505
+ break;
506
+ }
507
+ }
508
+ return { document, operationsApplied, signals, error } as const;
509
+ }
510
+
511
+ private _validateOperations<T extends Document, A extends Action>(
512
+ operations: Operation<A | BaseAction>[],
513
+ documentStorage: DocumentStorage<T>
514
+ ) {
515
+ const operationsToApply: Operation<A | BaseAction>[] = [];
516
+ let error: OperationError | undefined;
517
+
518
+ // sort operations so from smaller index to biggest
519
+ operations = operations.sort((a, b) => a.index - b.index);
520
+
521
+ for (let i = 0; i < operations.length; i++) {
522
+ const op = operations[i]!;
523
+ const pastOperations = operationsToApply
524
+ .filter(appliedOperation => appliedOperation.scope === op.scope)
525
+ .slice(0, i);
526
+ const scopeOperations = documentStorage.operations[op.scope];
527
+
528
+ const nextIndex = scopeOperations.length + pastOperations.length;
529
+ if (op.index > nextIndex) {
530
+ error = new OperationError(
531
+ 'MISSING',
532
+ op,
533
+ `Missing operation on index ${nextIndex}`
534
+ );
535
+ continue;
536
+ } else if (op.index < nextIndex) {
537
+ const existingOperation =
538
+ scopeOperations.concat(pastOperations)[op.index];
539
+ if (existingOperation && existingOperation.hash !== op.hash) {
540
+ error = new OperationError(
541
+ 'CONFLICT',
542
+ op,
543
+ `Conflicting operation on index ${op.index}`
544
+ );
545
+ continue;
546
+ }
547
+ } else {
548
+ operationsToApply.push(op);
549
+ }
550
+ }
551
+
552
+ return [operationsToApply, error] as const;
553
+ }
554
+
555
+ private async _performOperation<T extends Document, A extends Action>(
556
+ drive: string,
557
+ documentStorage: DocumentStorage<T>,
558
+ operation: Operation<A | BaseAction>
124
559
  ) {
125
560
  const documentModel = this._getDocumentModel(
126
561
  documentStorage.documentType
@@ -131,51 +566,62 @@ export class DocumentDriveServer implements IDocumentDriveServer {
131
566
  documentModel.reducer,
132
567
  undefined,
133
568
  documentStorage
134
- );
569
+ ) as T;
135
570
 
136
571
  const signalResults: SignalResult[] = [];
137
572
  let newDocument = document;
138
- for (const operation of operations) {
139
- const operationSignals: Promise<SignalResult>[] = [];
140
- newDocument = documentModel.reducer(
141
- newDocument,
142
- operation,
143
- signal => {
144
- let handler: Promise<unknown> | undefined = undefined;
145
- switch (signal.type) {
146
- case 'CREATE_CHILD_DOCUMENT':
147
- handler = this.createDocument(drive, signal.input);
148
- break;
149
- case 'DELETE_CHILD_DOCUMENT':
150
- handler = this.deleteDocument(
151
- drive,
152
- signal.input.id
153
- );
154
- break;
155
- case 'COPY_CHILD_DOCUMENT':
156
- handler = this.getDocument(
157
- drive,
158
- signal.input.id
159
- ).then(documentToCopy =>
573
+
574
+ const operationSignals: (() => Promise<SignalResult>)[] = [];
575
+ newDocument = documentModel.reducer(newDocument, operation, signal => {
576
+ let handler: (() => Promise<unknown>) | undefined = undefined;
577
+ switch (signal.type) {
578
+ case 'CREATE_CHILD_DOCUMENT':
579
+ handler = () => this.createDocument(drive, signal.input);
580
+ break;
581
+ case 'DELETE_CHILD_DOCUMENT':
582
+ handler = () => this.deleteDocument(drive, signal.input.id);
583
+ break;
584
+ case 'COPY_CHILD_DOCUMENT':
585
+ handler = () =>
586
+ this.getDocument(drive, signal.input.id).then(
587
+ documentToCopy =>
160
588
  this.createDocument(drive, {
161
589
  id: signal.input.newId,
162
590
  documentType: documentToCopy.documentType,
163
- document: documentToCopy
591
+ document: documentToCopy,
592
+ synchronizationUnits:
593
+ signal.input.synchronizationUnits
164
594
  })
165
- );
166
- break;
167
- }
168
- if (handler) {
169
- operationSignals.push(
170
- handler.then(result => ({ signal, result }))
171
595
  );
172
- }
173
- }
596
+ break;
597
+ }
598
+ if (handler) {
599
+ operationSignals.push(() =>
600
+ handler().then(result => ({ signal, result }))
601
+ );
602
+ }
603
+ }) as T;
604
+
605
+ const appliedOperation =
606
+ newDocument.operations[operation.scope][operation.index];
607
+ if (!appliedOperation || appliedOperation.hash !== operation.hash) {
608
+ throw new OperationError(
609
+ 'CONFLICT',
610
+ operation,
611
+ `Operation with index ${operation.index} had different result`
174
612
  );
175
- const results = await Promise.all(operationSignals);
176
- signalResults.push(...results);
177
613
  }
178
- return { document: newDocument, signals: signalResults };
614
+
615
+ const results = await Promise.all(
616
+ operationSignals.map(handler => handler())
617
+ );
618
+ signalResults.push(...results);
619
+
620
+ return {
621
+ document: newDocument,
622
+ signals: signalResults,
623
+ operation: appliedOperation
624
+ };
179
625
  }
180
626
 
181
627
  addOperation(drive: string, id: string, operation: Operation) {
@@ -185,37 +631,95 @@ export class DocumentDriveServer implements IDocumentDriveServer {
185
631
  async addOperations(drive: string, id: string, operations: Operation[]) {
186
632
  // retrieves document from storage
187
633
  const documentStorage = await this.storage.getDocument(drive, id);
634
+
635
+ let document: Document | undefined;
636
+ const operationsApplied: Operation[] = [];
637
+ const signals: SignalResult[] = [];
638
+ let error: Error | undefined;
639
+
188
640
  try {
189
641
  // retrieves the document's document model and
190
642
  // applies the operations using its reducer
191
- const { document, signals } = await this._performOperations(
643
+ const result = await this._processOperations(
192
644
  drive,
193
645
  documentStorage,
194
646
  operations
195
647
  );
196
648
 
197
- // saves the updated state of the document and returns it
649
+ document = result.document;
650
+ operationsApplied.push(...result.operationsApplied);
651
+ signals.push(...result.signals);
652
+ error = result.error;
653
+
654
+ if (!document) {
655
+ throw error ?? new Error('Invalid document');
656
+ }
657
+
658
+ // saves the applied operations to storage
198
659
  await this.storage.addDocumentOperations(
199
660
  drive,
200
661
  id,
201
- operations,
662
+ operationsApplied,
202
663
  document
203
664
  );
204
665
 
666
+ // gets all the different scopes and branches combinations from the operations
667
+ const { scopes, branches } = operationsApplied.reduce(
668
+ (acc, operation) => {
669
+ if (!acc.scopes.includes(operation.scope)) {
670
+ acc.scopes.push(operation.scope);
671
+ }
672
+ return acc;
673
+ },
674
+ { scopes: [] as string[], branches: ['main'] }
675
+ );
676
+
677
+ const syncUnits = await this.getSynchronizationUnits(
678
+ drive,
679
+ [id],
680
+ scopes,
681
+ branches
682
+ );
683
+ // update listener cache
684
+ for (const syncUnit of syncUnits) {
685
+ await this.listenerStateManager.updateSynchronizationRevision(
686
+ drive,
687
+ syncUnit.syncId,
688
+ syncUnit.revision,
689
+ syncUnit.lastUpdated
690
+ );
691
+ }
692
+
693
+ // after applying all the valid operations,throws
694
+ // an error if there was an invalid operation
695
+ if (error) {
696
+ throw error;
697
+ }
698
+
205
699
  return {
206
- success: true,
700
+ status: 'SUCCESS',
207
701
  document,
208
- operations,
702
+ operations: operationsApplied,
209
703
  signals
210
- };
704
+ } satisfies IOperationResult;
211
705
  } catch (error) {
706
+ const operationError =
707
+ error instanceof OperationError
708
+ ? error
709
+ : new OperationError(
710
+ 'ERROR',
711
+ undefined,
712
+ (error as Error).message,
713
+ (error as Error).cause
714
+ );
715
+
212
716
  return {
213
- success: false,
214
- error: error as Error,
215
- document: undefined,
216
- operations,
217
- signals: []
218
- };
717
+ status: operationError.status,
718
+ error: operationError,
719
+ document,
720
+ operations: operationsApplied,
721
+ signals
722
+ } satisfies IOperationResult;
219
723
  }
220
724
  }
221
725
 
@@ -232,39 +736,131 @@ export class DocumentDriveServer implements IDocumentDriveServer {
232
736
  ) {
233
737
  // retrieves document from storage
234
738
  const documentStorage = await this.storage.getDrive(drive);
739
+
740
+ let document: DocumentDriveDocument | undefined;
741
+ const operationsApplied: Operation<DocumentDriveAction | BaseAction>[] =
742
+ [];
743
+ const signals: SignalResult[] = [];
744
+ let error: Error | undefined;
745
+
235
746
  try {
236
- // retrieves the document's document model and
237
- // applies the operations using its reducer
238
- const { document, signals } = await this._performOperations(
747
+ const result = await this._processOperations<
748
+ DocumentDriveDocument,
749
+ DocumentDriveAction
750
+ >(drive, documentStorage, operations.slice());
751
+
752
+ document = result.document;
753
+ operationsApplied.push(...result.operationsApplied);
754
+ signals.push(...result.signals);
755
+ error = result.error;
756
+
757
+ if (!document || !isDocumentDrive(document)) {
758
+ throw error ?? new Error('Invalid Document Drive document');
759
+ }
760
+
761
+ // saves the applied operations to storage
762
+ await this.storage.addDriveOperations(
239
763
  drive,
240
- documentStorage,
241
- operations
764
+ operationsApplied,
765
+ document
242
766
  );
243
767
 
244
- if (isDocumentDrive(document)) {
245
- await this.storage.addDriveOperations(
768
+ for (const operation of operationsApplied) {
769
+ if (operation.type === 'ADD_LISTENER') {
770
+ const { listener } = operation.input;
771
+ await this.listenerStateManager.addListener({
772
+ ...listener,
773
+ driveId: drive,
774
+ label: listener.label ?? '',
775
+ system: listener.system ?? false,
776
+ filter: {
777
+ branch: listener.filter.branch ?? [],
778
+ documentId: listener.filter.documentId ?? [],
779
+ documentType: listener.filter.documentType ?? [],
780
+ scope: listener.filter.scope ?? []
781
+ },
782
+ callInfo: {
783
+ data: listener.callInfo?.data ?? '',
784
+ name: listener.callInfo?.name ?? 'PullResponder',
785
+ transmitterType:
786
+ listener.callInfo?.transmitterType ??
787
+ 'PullResponder'
788
+ }
789
+ });
790
+ } else if (operation.type === 'REMOVE_LISTENER') {
791
+ const { listenerId } = operation.input;
792
+ await this.listenerStateManager.removeListener(
793
+ drive,
794
+ listenerId
795
+ );
796
+ }
797
+ }
798
+
799
+ // update listener cache
800
+ const lastOperation = operationsApplied
801
+ .filter(op => op.scope === 'global')
802
+ .slice()
803
+ .pop();
804
+ if (lastOperation) {
805
+ await this.listenerStateManager.updateSynchronizationRevision(
246
806
  drive,
247
- operations, // TODO check?
248
- document
807
+ '0',
808
+ lastOperation.index,
809
+ lastOperation.timestamp
249
810
  );
811
+ }
812
+
813
+ if (this.shouldSyncRemoteDrive(document)) {
814
+ this.startSyncRemoteDrive(document.state.global.id);
250
815
  } else {
251
- throw new Error('Invalid Document Drive document');
816
+ this.stopSyncRemoteDrive(document.state.global.id);
817
+ }
818
+
819
+ // after applying all the valid operations,throws
820
+ // an error if there was an invalid operation
821
+ if (error) {
822
+ throw error;
252
823
  }
253
824
 
254
825
  return {
255
- success: true,
826
+ status: 'SUCCESS',
256
827
  document,
257
- operations,
828
+ operations: operationsApplied,
258
829
  signals
259
- };
830
+ } satisfies IOperationResult;
260
831
  } catch (error) {
832
+ const operationError =
833
+ error instanceof OperationError
834
+ ? error
835
+ : new OperationError(
836
+ 'ERROR',
837
+ undefined,
838
+ (error as Error).message,
839
+ (error as Error).cause
840
+ );
841
+
261
842
  return {
262
- success: false,
263
- error: error as Error,
264
- document: undefined,
265
- operations,
266
- signals: []
267
- };
843
+ status: operationError.status,
844
+ error: operationError,
845
+ document,
846
+ operations: operationsApplied,
847
+ signals
848
+ } satisfies IOperationResult;
849
+ }
850
+ }
851
+
852
+ getTransmitter(
853
+ driveId: string,
854
+ listenerId: string
855
+ ): Promise<ITransmitter | undefined> {
856
+ return this.listenerStateManager.getTransmitter(driveId, listenerId);
857
+ }
858
+
859
+ getSyncStatus(drive: string): SyncStatus {
860
+ const status = this.syncStatus.get(drive);
861
+ if (!status) {
862
+ throw new Error(`Sync status not found for drive ${drive}`);
268
863
  }
864
+ return status;
269
865
  }
270
866
  }