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

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.14",
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';
@@ -456,12 +462,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
456
462
  return this.storage.getDrives();
457
463
  }
458
464
 
459
- async getDrive(drive: string) {
465
+ async getDrive(drive: string, options?: GetDocumentOptions) {
460
466
  const driveStorage = await this.storage.getDrive(drive);
461
467
  const documentModel = this._getDocumentModel(driveStorage.documentType);
462
468
  const document = baseUtils.replayDocument(
463
469
  driveStorage.initialState,
464
- driveStorage.operations,
470
+ filterOperationsByRevision(
471
+ driveStorage.operations,
472
+ options?.revisions
473
+ ),
465
474
  documentModel.reducer,
466
475
  undefined,
467
476
  driveStorage
@@ -475,7 +484,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
475
484
  }
476
485
  }
477
486
 
478
- async getDocument(drive: string, id: string) {
487
+ async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
479
488
  const { initialState, operations, ...header } =
480
489
  await this.storage.getDocument(drive, id);
481
490
 
@@ -483,7 +492,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
483
492
 
484
493
  return baseUtils.replayDocument(
485
494
  initialState,
486
- operations,
495
+ filterOperationsByRevision(operations, options?.revisions),
487
496
  documentModel.reducer,
488
497
  undefined,
489
498
  header
@@ -1002,6 +1011,116 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1002
1011
  }
1003
1012
  }
1004
1013
 
1014
+ private _buildOperation<T extends Action>(
1015
+ documentStorage: DocumentStorage,
1016
+ action: T | BaseAction
1017
+ ): Operation<T | BaseAction> {
1018
+ const [operation] = this._buildOperations(documentStorage, [action]);
1019
+ if (!operation) {
1020
+ throw new Error('Error creating operation');
1021
+ }
1022
+ return operation;
1023
+ }
1024
+
1025
+ private _buildOperations<T extends Action>(
1026
+ documentStorage: DocumentStorage,
1027
+ actions: (T | BaseAction)[]
1028
+ ): Operation<T | BaseAction>[] {
1029
+ const operations: Operation<T | BaseAction>[] = [];
1030
+ const { reducer } = this._getDocumentModel(
1031
+ documentStorage.documentType
1032
+ );
1033
+ let document = baseUtils.replayDocument(
1034
+ documentStorage.initialState,
1035
+ documentStorage.operations,
1036
+ reducer,
1037
+ undefined,
1038
+ documentStorage
1039
+ );
1040
+ for (const action of actions) {
1041
+ document = reducer(document, action);
1042
+ const operation = document.operations[action.scope].slice().pop();
1043
+ if (!operation) {
1044
+ throw new Error('Error creating operations');
1045
+ }
1046
+ operations.push(operation);
1047
+ }
1048
+ return operations;
1049
+ }
1050
+
1051
+ async addAction(
1052
+ drive: string,
1053
+ id: string,
1054
+ action: Action
1055
+ ): Promise<IOperationResult> {
1056
+ const documentStorage = await this.storage.getDocument(drive, id);
1057
+ const operation = this._buildOperation(documentStorage, action);
1058
+ return this.addOperation(drive, id, operation);
1059
+ }
1060
+
1061
+ async addActions(
1062
+ drive: string,
1063
+ id: string,
1064
+ actions: Action[]
1065
+ ): Promise<IOperationResult> {
1066
+ const documentStorage = await this.storage.getDocument(drive, id);
1067
+ const operations = this._buildOperations(documentStorage, actions);
1068
+ return this.addOperations(drive, id, operations);
1069
+ }
1070
+
1071
+ async addDriveAction(
1072
+ drive: string,
1073
+ action: DocumentDriveAction | BaseAction
1074
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1075
+ const documentStorage = await this.storage.getDrive(drive);
1076
+ const operation = this._buildOperation(documentStorage, action);
1077
+ return this.addDriveOperation(drive, operation);
1078
+ }
1079
+
1080
+ async addDriveActions(
1081
+ drive: string,
1082
+ actions: (DocumentDriveAction | BaseAction)[]
1083
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1084
+ const documentStorage = await this.storage.getDrive(drive);
1085
+ const operations = this._buildOperations(documentStorage, actions);
1086
+ return this.addDriveOperations(drive, operations);
1087
+ }
1088
+
1089
+ async addInternalListener(
1090
+ driveId: string,
1091
+ receiver: IReceiver,
1092
+ options: {
1093
+ listenerId: string;
1094
+ label: string;
1095
+ block: boolean;
1096
+ filter: ListenerFilter;
1097
+ }
1098
+ ) {
1099
+ const listener: AddListenerInput['listener'] = {
1100
+ callInfo: {
1101
+ data: '',
1102
+ name: 'Interal',
1103
+ transmitterType: 'Internal'
1104
+ },
1105
+ system: true,
1106
+ ...options
1107
+ };
1108
+ await this.addDriveAction(driveId, actions.addListener({ listener }));
1109
+ const transmitter = await this.getTransmitter(
1110
+ driveId,
1111
+ options.listenerId
1112
+ );
1113
+ if (!transmitter) {
1114
+ throw new Error('Internal listener not found');
1115
+ }
1116
+ if (!(transmitter instanceof InternalTransmitter)) {
1117
+ throw new Error('Listener is not an internal transmitter');
1118
+ }
1119
+
1120
+ transmitter.setReceiver(receiver);
1121
+ return transmitter;
1122
+ }
1123
+
1005
1124
  private async addListener(
1006
1125
  driveId: string,
1007
1126
  operation: Operation<Action<'ADD_LISTENER', AddListenerInput>>
@@ -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
+ }