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 +1 -1
- package/src/server/index.ts +123 -4
- package/src/server/listener/transmitter/internal.ts +60 -3
- package/src/server/types.ts +41 -2
- package/src/server/utils.ts +34 -0
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';
|
|
@@ -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
|
-
|
|
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:
|
|
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
|
+
}
|