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
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|
package/src/server/types.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
+
}
|