document-drive 0.0.29 → 0.0.30
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 +4 -3
- package/src/server/index.ts +72 -15
- package/src/server/listener/manager.ts +42 -11
- package/src/server/types.ts +22 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@prisma/client": "5.8.1",
|
|
32
|
-
"document-model": "^1.0.
|
|
32
|
+
"document-model": "^1.0.29",
|
|
33
33
|
"document-model-libs": "^1.1.44",
|
|
34
34
|
"localforage": "^1.10.0",
|
|
35
35
|
"sequelize": "^6.35.2",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"graphql": "^16.8.1",
|
|
40
40
|
"graphql-request": "^6.1.0",
|
|
41
41
|
"json-stringify-deterministic": "^1.0.12",
|
|
42
|
+
"nanoevents": "^9.0.0",
|
|
42
43
|
"sanitize-filename": "^1.6.3"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
|
49
50
|
"@typescript-eslint/parser": "^6.18.1",
|
|
50
51
|
"@vitest/coverage-v8": "^0.34.6",
|
|
51
|
-
"document-model": "^1.0.
|
|
52
|
+
"document-model": "^1.0.29",
|
|
52
53
|
"document-model-libs": "^1.1.44",
|
|
53
54
|
"eslint": "^8.56.0",
|
|
54
55
|
"eslint-config-prettier": "^9.1.0",
|
package/src/server/index.ts
CHANGED
|
@@ -3,19 +3,20 @@ import {
|
|
|
3
3
|
DocumentDriveDocument,
|
|
4
4
|
DocumentDriveState,
|
|
5
5
|
FileNode,
|
|
6
|
-
Trigger,
|
|
7
6
|
isFileNode,
|
|
7
|
+
Trigger,
|
|
8
8
|
utils
|
|
9
9
|
} from 'document-model-libs/document-drive';
|
|
10
10
|
import {
|
|
11
11
|
Action,
|
|
12
12
|
BaseAction,
|
|
13
|
+
utils as baseUtils,
|
|
13
14
|
Document,
|
|
14
15
|
DocumentModel,
|
|
15
16
|
Operation,
|
|
16
|
-
OperationScope
|
|
17
|
-
utils as baseUtils
|
|
17
|
+
OperationScope
|
|
18
18
|
} from 'document-model/document';
|
|
19
|
+
import { createNanoEvents, Unsubscribe } from 'nanoevents';
|
|
19
20
|
import { MemoryStorage } from '../storage/memory';
|
|
20
21
|
import type { DocumentStorage, IDriveStorage } from '../storage/types';
|
|
21
22
|
import { generateUUID, isDocumentDrive, isNoopUpdate } from '../utils';
|
|
@@ -26,7 +27,9 @@ import { PullResponderTransmitter } from './listener/transmitter';
|
|
|
26
27
|
import type { ITransmitter } from './listener/transmitter/types';
|
|
27
28
|
import {
|
|
28
29
|
BaseDocumentDriveServer,
|
|
30
|
+
DriveEvents,
|
|
29
31
|
IOperationResult,
|
|
32
|
+
ListenerState,
|
|
30
33
|
RemoteDriveOptions,
|
|
31
34
|
StrandUpdate,
|
|
32
35
|
SyncStatus,
|
|
@@ -43,6 +46,7 @@ export type * from './types';
|
|
|
43
46
|
export const PULL_DRIVE_INTERVAL = 5000;
|
|
44
47
|
|
|
45
48
|
export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
49
|
+
private emitter = createNanoEvents<DriveEvents>();
|
|
46
50
|
private documentModels: DocumentModel[];
|
|
47
51
|
private storage: IDriveStorage;
|
|
48
52
|
private listenerStateManager: ListenerManager;
|
|
@@ -62,6 +66,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
62
66
|
this.storage = storage;
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
private updateSyncStatus(
|
|
70
|
+
driveId: string,
|
|
71
|
+
status: SyncStatus,
|
|
72
|
+
error?: Error
|
|
73
|
+
) {
|
|
74
|
+
this.syncStatus.set(driveId, status);
|
|
75
|
+
this.emit('syncStatus', driveId, status, error);
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
private async saveStrand(strand: StrandUpdate) {
|
|
66
79
|
const operations: Operation[] = strand.operations.map(
|
|
67
80
|
({ index, type, hash, input, skip, timestamp }) => ({
|
|
@@ -86,10 +99,27 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
86
99
|
strand.documentId,
|
|
87
100
|
operations
|
|
88
101
|
));
|
|
89
|
-
|
|
102
|
+
|
|
103
|
+
this.updateSyncStatus(strand.driveId, result.status);
|
|
104
|
+
this.emit('strandUpdate', strand);
|
|
90
105
|
return result;
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
private handleListenerError(
|
|
109
|
+
error: Error,
|
|
110
|
+
driveId: string,
|
|
111
|
+
listener: ListenerState
|
|
112
|
+
) {
|
|
113
|
+
console.error(
|
|
114
|
+
`Listener ${listener.listener.label ?? listener.listener.listenerId} error: ${error.message}`
|
|
115
|
+
);
|
|
116
|
+
this.updateSyncStatus(
|
|
117
|
+
driveId,
|
|
118
|
+
error instanceof OperationError ? error.status : 'ERROR',
|
|
119
|
+
error
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
private shouldSyncRemoteDrive(drive: DocumentDriveDocument) {
|
|
94
124
|
return (
|
|
95
125
|
drive.state.local.availableOffline &&
|
|
@@ -108,7 +138,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
108
138
|
|
|
109
139
|
if (!driveTriggers) {
|
|
110
140
|
driveTriggers = new Map();
|
|
111
|
-
this.
|
|
141
|
+
this.updateSyncStatus(driveId, 'SYNCING');
|
|
112
142
|
}
|
|
113
143
|
|
|
114
144
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
@@ -117,7 +147,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
117
147
|
trigger,
|
|
118
148
|
this.saveStrand.bind(this),
|
|
119
149
|
error => {
|
|
120
|
-
this.
|
|
150
|
+
this.updateSyncStatus(
|
|
121
151
|
driveId,
|
|
122
152
|
error instanceof OperationError
|
|
123
153
|
? error.status
|
|
@@ -265,7 +295,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
265
295
|
(filter.since !== undefined &&
|
|
266
296
|
filter.since <= operation.timestamp) ||
|
|
267
297
|
(filter.fromRevision !== undefined &&
|
|
268
|
-
operation.index
|
|
298
|
+
operation.index > filter.fromRevision)
|
|
269
299
|
);
|
|
270
300
|
|
|
271
301
|
return filteredOperations.map(operation => ({
|
|
@@ -456,6 +486,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
456
486
|
}
|
|
457
487
|
|
|
458
488
|
async deleteDocument(driveId: string, id: string) {
|
|
489
|
+
try {
|
|
490
|
+
const syncUnits = await this.getSynchronizationUnits(driveId, [id]);
|
|
491
|
+
this.listenerStateManager.removeSyncUnits(syncUnits);
|
|
492
|
+
} catch {
|
|
493
|
+
/* empty */
|
|
494
|
+
}
|
|
459
495
|
return this.storage.deleteDocument(driveId, id);
|
|
460
496
|
}
|
|
461
497
|
|
|
@@ -664,10 +700,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
664
700
|
);
|
|
665
701
|
}
|
|
666
702
|
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
703
|
+
for (const signalHandler of operationSignals) {
|
|
704
|
+
const result = await signalHandler();
|
|
705
|
+
signalResults.push(result);
|
|
706
|
+
}
|
|
671
707
|
|
|
672
708
|
return {
|
|
673
709
|
document: newDocument,
|
|
@@ -749,10 +785,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
749
785
|
drive,
|
|
750
786
|
syncUnit.syncId,
|
|
751
787
|
syncUnit.revision,
|
|
752
|
-
syncUnit.lastUpdated
|
|
788
|
+
syncUnit.lastUpdated,
|
|
789
|
+
this.handleListenerError.bind(this)
|
|
753
790
|
)
|
|
754
791
|
.catch(error => {
|
|
755
|
-
console.error(
|
|
792
|
+
console.error(
|
|
793
|
+
'Non handled error updating sync revision',
|
|
794
|
+
error
|
|
795
|
+
);
|
|
796
|
+
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
756
797
|
});
|
|
757
798
|
}
|
|
758
799
|
|
|
@@ -875,10 +916,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
875
916
|
drive,
|
|
876
917
|
'0',
|
|
877
918
|
lastOperation.index,
|
|
878
|
-
lastOperation.timestamp
|
|
919
|
+
lastOperation.timestamp,
|
|
920
|
+
this.handleListenerError.bind(this)
|
|
879
921
|
)
|
|
880
922
|
.catch(error => {
|
|
881
|
-
console.error(
|
|
923
|
+
console.error(
|
|
924
|
+
'Non handled error updating sync revision',
|
|
925
|
+
error
|
|
926
|
+
);
|
|
927
|
+
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
882
928
|
});
|
|
883
929
|
}
|
|
884
930
|
|
|
@@ -935,4 +981,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
935
981
|
}
|
|
936
982
|
return status;
|
|
937
983
|
}
|
|
984
|
+
|
|
985
|
+
on<K extends keyof DriveEvents>(event: K, cb: DriveEvents[K]): Unsubscribe {
|
|
986
|
+
return this.emitter.on(event, cb);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
protected emit<K extends keyof DriveEvents>(
|
|
990
|
+
event: K,
|
|
991
|
+
...args: Parameters<DriveEvents[K]>
|
|
992
|
+
): void {
|
|
993
|
+
return this.emitter.emit(event, ...args);
|
|
994
|
+
}
|
|
938
995
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ErrorStatus,
|
|
10
10
|
Listener,
|
|
11
11
|
ListenerState,
|
|
12
|
+
OperationUpdate,
|
|
12
13
|
StrandUpdate,
|
|
13
14
|
SynchronizationUnit
|
|
14
15
|
} from '../types';
|
|
@@ -120,7 +121,12 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
120
121
|
driveId: string,
|
|
121
122
|
syncId: string,
|
|
122
123
|
syncRev: number,
|
|
123
|
-
lastUpdated: string
|
|
124
|
+
lastUpdated: string,
|
|
125
|
+
onError?: (
|
|
126
|
+
error: Error,
|
|
127
|
+
driveId: string,
|
|
128
|
+
listener: ListenerState
|
|
129
|
+
) => void
|
|
124
130
|
) {
|
|
125
131
|
const drive = this.listenerState.get(driveId);
|
|
126
132
|
if (!drive) {
|
|
@@ -148,7 +154,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
if (newRevision) {
|
|
151
|
-
return this.triggerUpdate();
|
|
157
|
+
return this.triggerUpdate(onError);
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
|
|
@@ -202,6 +208,19 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
removeSyncUnits(syncUnits: SynchronizationUnit[]) {
|
|
212
|
+
for (const [driveId, drive] of this.listenerState) {
|
|
213
|
+
const syncIds = syncUnits
|
|
214
|
+
.filter(s => s.driveId === driveId)
|
|
215
|
+
.map(s => s.syncId);
|
|
216
|
+
for (const [, listenerState] of drive) {
|
|
217
|
+
listenerState.syncUnits = listenerState.syncUnits.filter(
|
|
218
|
+
s => !syncIds.includes(s.syncId)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
async updateListenerRevision(
|
|
206
225
|
listenerId: string,
|
|
207
226
|
driveId: string,
|
|
@@ -225,7 +244,13 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
|
|
228
|
-
async triggerUpdate(
|
|
247
|
+
async triggerUpdate(
|
|
248
|
+
onError?: (
|
|
249
|
+
error: Error,
|
|
250
|
+
driveId: string,
|
|
251
|
+
listener: ListenerState
|
|
252
|
+
) => void
|
|
253
|
+
) {
|
|
229
254
|
for (const [driveId, drive] of this.listenerState) {
|
|
230
255
|
for (const [id, listener] of drive) {
|
|
231
256
|
const transmitter = await this.getTransmitter(driveId, id);
|
|
@@ -248,13 +273,19 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
248
273
|
continue;
|
|
249
274
|
}
|
|
250
275
|
|
|
251
|
-
const opData =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
276
|
+
const opData: OperationUpdate[] = [];
|
|
277
|
+
try {
|
|
278
|
+
const data = await this.drive.getOperationData(
|
|
279
|
+
driveId,
|
|
280
|
+
syncId,
|
|
281
|
+
{
|
|
282
|
+
fromRevision: listenerRev
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
opData.push(...data);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error(e);
|
|
288
|
+
}
|
|
258
289
|
|
|
259
290
|
if (!opData.length) {
|
|
260
291
|
continue;
|
|
@@ -309,9 +340,9 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
309
340
|
listener.listenerStatus = 'SUCCESS';
|
|
310
341
|
} catch (e) {
|
|
311
342
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
343
|
+
onError?.(e as Error, driveId, listener);
|
|
312
344
|
listener.listenerStatus =
|
|
313
345
|
e instanceof OperationError ? e.status : 'ERROR';
|
|
314
|
-
throw e;
|
|
315
346
|
}
|
|
316
347
|
}
|
|
317
348
|
}
|
package/src/server/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
Signal,
|
|
16
16
|
State
|
|
17
17
|
} from 'document-model/document';
|
|
18
|
+
import { Unsubscribe } from 'nanoevents';
|
|
18
19
|
import { OperationError } from './error';
|
|
19
20
|
import { ITransmitter } from './listener/transmitter/types';
|
|
20
21
|
|
|
@@ -114,6 +115,11 @@ export type StrandUpdate = {
|
|
|
114
115
|
|
|
115
116
|
export type SyncStatus = 'SYNCING' | UpdateStatus;
|
|
116
117
|
|
|
118
|
+
export interface DriveEvents {
|
|
119
|
+
syncStatus: (driveId: string, status: SyncStatus, error?: Error) => void;
|
|
120
|
+
strandUpdate: (update: StrandUpdate) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
export abstract class BaseDocumentDriveServer {
|
|
118
124
|
/** Public methods **/
|
|
119
125
|
abstract getDrives(): Promise<string[]>;
|
|
@@ -132,12 +138,12 @@ export abstract class BaseDocumentDriveServer {
|
|
|
132
138
|
drive: string,
|
|
133
139
|
id: string,
|
|
134
140
|
operation: Operation
|
|
135
|
-
): Promise<IOperationResult
|
|
141
|
+
): Promise<IOperationResult>;
|
|
136
142
|
abstract addOperations(
|
|
137
143
|
drive: string,
|
|
138
144
|
id: string,
|
|
139
145
|
operations: Operation[]
|
|
140
|
-
): Promise<IOperationResult
|
|
146
|
+
): Promise<IOperationResult>;
|
|
141
147
|
|
|
142
148
|
abstract addDriveOperation(
|
|
143
149
|
drive: string,
|
|
@@ -183,12 +189,23 @@ export abstract class BaseDocumentDriveServer {
|
|
|
183
189
|
driveId: string,
|
|
184
190
|
listenerId: string
|
|
185
191
|
): Promise<ITransmitter | undefined>;
|
|
192
|
+
|
|
193
|
+
/** Event methods **/
|
|
194
|
+
protected abstract emit<K extends keyof DriveEvents>(
|
|
195
|
+
this: this,
|
|
196
|
+
event: K,
|
|
197
|
+
...args: Parameters<DriveEvents[K]>
|
|
198
|
+
): void;
|
|
199
|
+
abstract on<K extends keyof DriveEvents>(
|
|
200
|
+
this: this,
|
|
201
|
+
event: K,
|
|
202
|
+
cb: DriveEvents[K]
|
|
203
|
+
): Unsubscribe;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
export abstract class BaseListenerManager {
|
|
189
207
|
protected drive: BaseDocumentDriveServer;
|
|
190
|
-
protected listenerState
|
|
191
|
-
new Map();
|
|
208
|
+
protected listenerState = new Map<string, Map<string, ListenerState>>();
|
|
192
209
|
protected transmitters: Record<
|
|
193
210
|
DocumentDriveState['id'],
|
|
194
211
|
Record<Listener['listenerId'], ITransmitter>
|
|
@@ -196,7 +213,7 @@ export abstract class BaseListenerManager {
|
|
|
196
213
|
|
|
197
214
|
constructor(
|
|
198
215
|
drive: BaseDocumentDriveServer,
|
|
199
|
-
listenerState
|
|
216
|
+
listenerState = new Map<string, Map<string, ListenerState>>()
|
|
200
217
|
) {
|
|
201
218
|
this.drive = drive;
|
|
202
219
|
this.listenerState = listenerState;
|