document-drive 1.0.0-alpha.13 → 1.0.0-alpha.15

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": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.15",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -1,10 +1,12 @@
1
1
  import {
2
+ actions,
2
3
  AddListenerInput,
3
4
  DocumentDriveAction,
4
5
  DocumentDriveDocument,
5
6
  DocumentDriveState,
6
7
  FileNode,
7
8
  isFileNode,
9
+ ListenerFilter,
8
10
  RemoveListenerInput,
9
11
  Trigger,
10
12
  utils
@@ -32,12 +34,15 @@ import { OperationError } from './error';
32
34
  import { ListenerManager } from './listener/manager';
33
35
  import {
34
36
  CancelPullLoop,
37
+ InternalTransmitter,
38
+ IReceiver,
35
39
  ITransmitter,
36
40
  PullResponderTransmitter
37
41
  } from './listener/transmitter';
38
42
  import {
39
43
  BaseDocumentDriveServer,
40
44
  DriveEvents,
45
+ GetDocumentOptions,
41
46
  IOperationResult,
42
47
  ListenerState,
43
48
  RemoteDriveOptions,
@@ -49,6 +54,7 @@ import {
49
54
  type SignalResult,
50
55
  type SynchronizationUnit
51
56
  } from './types';
57
+ import { filterOperationsByRevision } from './utils';
52
58
 
53
59
  export * from './listener';
54
60
  export type * from './types';
@@ -196,11 +202,33 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
196
202
  async initialize() {
197
203
  await this.listenerStateManager.init();
198
204
  const drives = await this.getDrives();
199
- for (const id of drives) {
200
- const drive = await this.getDrive(id);
201
- if (this.shouldSyncRemoteDrive(drive)) {
202
- this.startSyncRemoteDrive(id);
203
- }
205
+ for (const drive of drives) {
206
+ await this._initializeDrive(drive);
207
+ }
208
+ }
209
+
210
+ private async _initializeDrive(driveId: string) {
211
+ const drive = await this.getDrive(driveId);
212
+
213
+ if (this.shouldSyncRemoteDrive(drive)) {
214
+ await this.startSyncRemoteDrive(driveId);
215
+ }
216
+
217
+ for (const listener of drive.state.local.listeners) {
218
+ await this.listenerStateManager.addListener({
219
+ driveId,
220
+ block: listener.block,
221
+ filter: {
222
+ branch: listener.filter.branch ?? [],
223
+ documentId: listener.filter.documentId ?? [],
224
+ documentType: listener.filter.documentType ?? [],
225
+ scope: listener.filter.scope ?? []
226
+ },
227
+ listenerId: listener.listenerId,
228
+ system: listener.system,
229
+ callInfo: listener.callInfo ?? undefined,
230
+ label: listener.label ?? ''
231
+ });
204
232
  }
205
233
  }
206
234
 
@@ -388,30 +416,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
388
416
  });
389
417
 
390
418
  await this.storage.createDrive(id, document);
391
-
392
- // add listeners to state manager
393
- for (const listener of drive.local.listeners) {
394
- await this.listenerStateManager.addListener({
395
- block: listener.block,
396
- driveId: id,
397
- filter: {
398
- branch: listener.filter.branch ?? [],
399
- documentId: listener.filter.documentId ?? [],
400
- documentType: listener.filter.documentType ?? [],
401
- scope: listener.filter.scope ?? []
402
- },
403
- listenerId: listener.listenerId,
404
- system: listener.system,
405
- callInfo: listener.callInfo ?? undefined,
406
- label: listener.label ?? ''
407
- });
408
- }
409
-
410
- // if it is a remote drive that should be available offline, starts
411
- // the sync process to pull changes from remote every 30 seconds
412
- if (this.shouldSyncRemoteDrive(document)) {
413
- await this.startSyncRemoteDrive(id);
414
- }
419
+ await this._initializeDrive(id);
415
420
  }
416
421
 
417
422
  async addRemoteDrive(url: string, options: RemoteDriveOptions) {
@@ -456,12 +461,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
456
461
  return this.storage.getDrives();
457
462
  }
458
463
 
459
- async getDrive(drive: string) {
464
+ async getDrive(drive: string, options?: GetDocumentOptions) {
460
465
  const driveStorage = await this.storage.getDrive(drive);
461
466
  const documentModel = this._getDocumentModel(driveStorage.documentType);
462
467
  const document = baseUtils.replayDocument(
463
468
  driveStorage.initialState,
464
- driveStorage.operations,
469
+ filterOperationsByRevision(
470
+ driveStorage.operations,
471
+ options?.revisions
472
+ ),
465
473
  documentModel.reducer,
466
474
  undefined,
467
475
  driveStorage
@@ -475,7 +483,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
475
483
  }
476
484
  }
477
485
 
478
- async getDocument(drive: string, id: string) {
486
+ async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
479
487
  const { initialState, operations, ...header } =
480
488
  await this.storage.getDocument(drive, id);
481
489
 
@@ -483,7 +491,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
483
491
 
484
492
  return baseUtils.replayDocument(
485
493
  initialState,
486
- operations,
494
+ filterOperationsByRevision(operations, options?.revisions),
487
495
  documentModel.reducer,
488
496
  undefined,
489
497
  header
@@ -1002,6 +1010,116 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1002
1010
  }
1003
1011
  }
1004
1012
 
1013
+ private _buildOperation<T extends Action>(
1014
+ documentStorage: DocumentStorage,
1015
+ action: T | BaseAction
1016
+ ): Operation<T | BaseAction> {
1017
+ const [operation] = this._buildOperations(documentStorage, [action]);
1018
+ if (!operation) {
1019
+ throw new Error('Error creating operation');
1020
+ }
1021
+ return operation;
1022
+ }
1023
+
1024
+ private _buildOperations<T extends Action>(
1025
+ documentStorage: DocumentStorage,
1026
+ actions: (T | BaseAction)[]
1027
+ ): Operation<T | BaseAction>[] {
1028
+ const operations: Operation<T | BaseAction>[] = [];
1029
+ const { reducer } = this._getDocumentModel(
1030
+ documentStorage.documentType
1031
+ );
1032
+ let document = baseUtils.replayDocument(
1033
+ documentStorage.initialState,
1034
+ documentStorage.operations,
1035
+ reducer,
1036
+ undefined,
1037
+ documentStorage
1038
+ );
1039
+ for (const action of actions) {
1040
+ document = reducer(document, action);
1041
+ const operation = document.operations[action.scope].slice().pop();
1042
+ if (!operation) {
1043
+ throw new Error('Error creating operations');
1044
+ }
1045
+ operations.push(operation);
1046
+ }
1047
+ return operations;
1048
+ }
1049
+
1050
+ async addAction(
1051
+ drive: string,
1052
+ id: string,
1053
+ action: Action
1054
+ ): Promise<IOperationResult> {
1055
+ const documentStorage = await this.storage.getDocument(drive, id);
1056
+ const operation = this._buildOperation(documentStorage, action);
1057
+ return this.addOperation(drive, id, operation);
1058
+ }
1059
+
1060
+ async addActions(
1061
+ drive: string,
1062
+ id: string,
1063
+ actions: Action[]
1064
+ ): Promise<IOperationResult> {
1065
+ const documentStorage = await this.storage.getDocument(drive, id);
1066
+ const operations = this._buildOperations(documentStorage, actions);
1067
+ return this.addOperations(drive, id, operations);
1068
+ }
1069
+
1070
+ async addDriveAction(
1071
+ drive: string,
1072
+ action: DocumentDriveAction | BaseAction
1073
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1074
+ const documentStorage = await this.storage.getDrive(drive);
1075
+ const operation = this._buildOperation(documentStorage, action);
1076
+ return this.addDriveOperation(drive, operation);
1077
+ }
1078
+
1079
+ async addDriveActions(
1080
+ drive: string,
1081
+ actions: (DocumentDriveAction | BaseAction)[]
1082
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1083
+ const documentStorage = await this.storage.getDrive(drive);
1084
+ const operations = this._buildOperations(documentStorage, actions);
1085
+ return this.addDriveOperations(drive, operations);
1086
+ }
1087
+
1088
+ async addInternalListener(
1089
+ driveId: string,
1090
+ receiver: IReceiver,
1091
+ options: {
1092
+ listenerId: string;
1093
+ label: string;
1094
+ block: boolean;
1095
+ filter: ListenerFilter;
1096
+ }
1097
+ ) {
1098
+ const listener: AddListenerInput['listener'] = {
1099
+ callInfo: {
1100
+ data: '',
1101
+ name: 'Interal',
1102
+ transmitterType: 'Internal'
1103
+ },
1104
+ system: true,
1105
+ ...options
1106
+ };
1107
+ await this.addDriveAction(driveId, actions.addListener({ listener }));
1108
+ const transmitter = await this.getTransmitter(
1109
+ driveId,
1110
+ options.listenerId
1111
+ );
1112
+ if (!transmitter) {
1113
+ throw new Error('Internal listener not found');
1114
+ }
1115
+ if (!(transmitter instanceof InternalTransmitter)) {
1116
+ throw new Error('Listener is not an internal transmitter');
1117
+ }
1118
+
1119
+ transmitter.setReceiver(receiver);
1120
+ return transmitter;
1121
+ }
1122
+
1005
1123
  private async addListener(
1006
1124
  driveId: string,
1007
1125
  operation: Operation<Action<'ADD_LISTENER', AddListenerInput>>
@@ -15,9 +15,9 @@ import {
15
15
  SynchronizationUnit
16
16
  } from '../types';
17
17
  import { PullResponderTransmitter } from './transmitter';
18
+ import { InternalTransmitter } from './transmitter/internal';
18
19
  import { SwitchboardPushTransmitter } from './transmitter/switchboard-push';
19
20
  import { ITransmitter } from './transmitter/types';
20
- import { InternalTransmitter } from './transmitter/internal';
21
21
 
22
22
  export class ListenerManager extends BaseListenerManager {
23
23
  async getTransmitter(
@@ -73,10 +73,11 @@ export class ListenerManager extends BaseListenerManager {
73
73
  this.drive,
74
74
  this
75
75
  );
76
+ break;
76
77
  }
77
-
78
78
  case 'Internal': {
79
79
  transmitter = new InternalTransmitter(listener, this.drive);
80
+ break;
80
81
  }
81
82
  }
82
83
 
@@ -1,15 +1,30 @@
1
+ import { Document, OperationScope } from 'document-model/document';
1
2
  import {
2
3
  BaseDocumentDriveServer,
3
4
  Listener,
4
5
  ListenerRevision,
6
+ OperationUpdate,
5
7
  StrandUpdate
6
8
  } from '../../types';
9
+ import { buildRevisionsFilter } from '../../utils';
7
10
  import { ITransmitter } from './types';
8
11
 
9
12
  export interface IReceiver {
10
- transmit: (strands: StrandUpdate[]) => Promise<ListenerRevision[]>;
13
+ transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
11
14
  }
12
15
 
16
+ export type InternalTransmitterUpdate<
17
+ T extends Document = Document,
18
+ S extends OperationScope = OperationScope
19
+ > = {
20
+ driveId: string;
21
+ documentId: string;
22
+ scope: S;
23
+ branch: string;
24
+ operations: OperationUpdate[];
25
+ state: T['state'][S];
26
+ };
27
+
13
28
  export class InternalTransmitter implements ITransmitter {
14
29
  private drive: BaseDocumentDriveServer;
15
30
  private listener: Listener;
@@ -24,10 +39,52 @@ export class InternalTransmitter implements ITransmitter {
24
39
  if (!this.receiver) {
25
40
  return [];
26
41
  }
27
- return this.receiver.transmit(strands);
42
+
43
+ const retrievedDocuments = new Map<string, Document>();
44
+ const updates: InternalTransmitterUpdate[] = [];
45
+ for (const strand of strands) {
46
+ let document = retrievedDocuments.get(
47
+ `${strand.driveId}:${strand.documentId}`
48
+ );
49
+ if (!document) {
50
+ const revisions = buildRevisionsFilter(
51
+ strands,
52
+ strand.driveId,
53
+ strand.documentId
54
+ );
55
+ document = await (strand.documentId
56
+ ? this.drive.getDocument(
57
+ strand.driveId,
58
+ strand.documentId,
59
+ { revisions }
60
+ )
61
+ : this.drive.getDrive(strand.driveId));
62
+ retrievedDocuments.set(
63
+ `${strand.driveId}:${strand.documentId}`,
64
+ document
65
+ );
66
+ }
67
+ updates.push({ ...strand, state: document.state[strand.scope] });
68
+ }
69
+ try {
70
+ await this.receiver.transmit(updates);
71
+ return strands.map(({ operations, ...s }) => ({
72
+ ...s,
73
+ status: 'SUCCESS',
74
+ revision: operations[operations.length - 1]?.index ?? -1
75
+ }));
76
+ } catch (error) {
77
+ console.error(error);
78
+ // TODO check which strand caused an error
79
+ return strands.map(({ operations, ...s }) => ({
80
+ ...s,
81
+ status: 'ERROR',
82
+ revision: (operations[0]?.index ?? 0) - 1
83
+ }));
84
+ }
28
85
  }
29
86
 
30
87
  setReceiver(receiver: IReceiver) {
31
- this.receiver = receiver
88
+ this.receiver = receiver;
32
89
  }
33
90
  }
@@ -7,6 +7,7 @@ import type {
7
7
  ListenerFilter
8
8
  } from 'document-model-libs/document-drive';
9
9
  import type {
10
+ Action,
10
11
  BaseAction,
11
12
  CreateChildDocumentInput,
12
13
  Document,
@@ -127,6 +128,17 @@ export interface DriveEvents {
127
128
  strandUpdate: (update: StrandUpdate) => void;
128
129
  }
129
130
 
131
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
+ export type PartialRecord<K extends keyof any, T> = {
133
+ [P in K]?: T;
134
+ };
135
+
136
+ export type RevisionsFilter = PartialRecord<OperationScope, number>;
137
+
138
+ export type GetDocumentOptions = {
139
+ revisions?: RevisionsFilter;
140
+ };
141
+
130
142
  export abstract class BaseDocumentDriveServer {
131
143
  /** Public methods **/
132
144
  abstract getDrives(): Promise<string[]>;
@@ -136,10 +148,17 @@ export abstract class BaseDocumentDriveServer {
136
148
  options: RemoteDriveOptions
137
149
  ): Promise<void>;
138
150
  abstract deleteDrive(id: string): Promise<void>;
139
- abstract getDrive(id: string): Promise<DocumentDriveDocument>;
151
+ abstract getDrive(
152
+ id: string,
153
+ options?: GetDocumentOptions
154
+ ): Promise<DocumentDriveDocument>;
140
155
 
141
156
  abstract getDocuments(drive: string): Promise<string[]>;
142
- abstract getDocument(drive: string, id: string): Promise<Document>;
157
+ abstract getDocument(
158
+ drive: string,
159
+ id: string,
160
+ options?: GetDocumentOptions
161
+ ): Promise<Document>;
143
162
 
144
163
  abstract addOperation(
145
164
  drive: string,
@@ -161,6 +180,26 @@ export abstract class BaseDocumentDriveServer {
161
180
  operations: Operation<DocumentDriveAction | BaseAction>[]
162
181
  ): Promise<IOperationResult<DocumentDriveDocument>>;
163
182
 
183
+ abstract addAction(
184
+ drive: string,
185
+ id: string,
186
+ action: Action
187
+ ): Promise<IOperationResult>;
188
+ abstract addActions(
189
+ drive: string,
190
+ id: string,
191
+ actions: Action[]
192
+ ): Promise<IOperationResult>;
193
+
194
+ abstract addDriveAction(
195
+ drive: string,
196
+ action: DocumentDriveAction | BaseAction
197
+ ): Promise<IOperationResult<DocumentDriveDocument>>;
198
+ abstract addDriveActions(
199
+ drive: string,
200
+ actions: (DocumentDriveAction | BaseAction)[]
201
+ ): Promise<IOperationResult<DocumentDriveDocument>>;
202
+
164
203
  abstract getSyncStatus(drive: string): SyncStatus;
165
204
 
166
205
  /** Synchronization methods */
@@ -0,0 +1,34 @@
1
+ import type { Document, OperationScope } from 'document-model/document';
2
+ import { RevisionsFilter, StrandUpdate } from './types';
3
+
4
+ export function buildRevisionsFilter(
5
+ strands: StrandUpdate[],
6
+ driveId: string,
7
+ documentId: string
8
+ ): RevisionsFilter {
9
+ return strands.reduce<RevisionsFilter>((acc, s) => {
10
+ if (!(s.driveId === driveId && s.documentId === documentId)) {
11
+ return acc;
12
+ }
13
+ acc[s.scope] = s.operations[s.operations.length - 1]?.index ?? -1;
14
+ return acc;
15
+ }, {});
16
+ }
17
+
18
+ export function filterOperationsByRevision(
19
+ operations: Document['operations'],
20
+ revisions?: RevisionsFilter
21
+ ): Document['operations'] {
22
+ if (!revisions) {
23
+ return operations;
24
+ }
25
+ return (Object.keys(operations) as OperationScope[]).reduce<
26
+ Document['operations']
27
+ >((acc, scope) => {
28
+ const revision = revisions[scope];
29
+ if (revision !== undefined) {
30
+ acc[scope] = operations[scope].filter(op => op.index < revision);
31
+ }
32
+ return acc;
33
+ }, operations);
34
+ }