document-drive 0.0.29 → 1.0.0-alpha.1
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 +10 -3
- package/src/server/index.ts +84 -15
- package/src/server/listener/manager.ts +62 -15
- package/src/server/listener/transmitter/pull-responder.ts +99 -72
- package/src/server/types.ts +35 -7
- package/src/storage/prisma.ts +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
"lint": "eslint src --ext .js,.jsx,.ts,.tsx && yarn check-types",
|
|
25
25
|
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
|
26
26
|
"format": "prettier . --write",
|
|
27
|
+
"release": "semantic-release",
|
|
27
28
|
"test": "vitest run --coverage",
|
|
28
29
|
"test:watch": "vitest watch"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"@prisma/client": "5.8.1",
|
|
32
|
-
"document-model": "^1.0.
|
|
33
|
+
"document-model": "^1.0.29",
|
|
33
34
|
"document-model-libs": "^1.1.44",
|
|
34
35
|
"localforage": "^1.10.0",
|
|
35
36
|
"sequelize": "^6.35.2",
|
|
@@ -39,16 +40,21 @@
|
|
|
39
40
|
"graphql": "^16.8.1",
|
|
40
41
|
"graphql-request": "^6.1.0",
|
|
41
42
|
"json-stringify-deterministic": "^1.0.12",
|
|
43
|
+
"nanoevents": "^9.0.0",
|
|
42
44
|
"sanitize-filename": "^1.6.3"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
47
|
+
"@commitlint/cli": "^18.6.1",
|
|
48
|
+
"@commitlint/config-conventional": "^18.6.2",
|
|
45
49
|
"@prisma/client": "5.8.1",
|
|
50
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
51
|
+
"@semantic-release/git": "^10.0.1",
|
|
46
52
|
"@total-typescript/ts-reset": "^0.5.1",
|
|
47
53
|
"@types/node": "^20.11.16",
|
|
48
54
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
|
49
55
|
"@typescript-eslint/parser": "^6.18.1",
|
|
50
56
|
"@vitest/coverage-v8": "^0.34.6",
|
|
51
|
-
"document-model": "^1.0.
|
|
57
|
+
"document-model": "^1.0.29",
|
|
52
58
|
"document-model-libs": "^1.1.44",
|
|
53
59
|
"eslint": "^8.56.0",
|
|
54
60
|
"eslint-config-prettier": "^9.1.0",
|
|
@@ -57,6 +63,7 @@
|
|
|
57
63
|
"msw": "^2.1.2",
|
|
58
64
|
"prettier": "^3.1.1",
|
|
59
65
|
"prettier-plugin-organize-imports": "^3.2.4",
|
|
66
|
+
"semantic-release": "^23.0.2",
|
|
60
67
|
"sequelize": "^6.35.2",
|
|
61
68
|
"sqlite3": "^5.1.7",
|
|
62
69
|
"typescript": "^5.3.2",
|
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,16 +138,16 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
108
138
|
|
|
109
139
|
if (!driveTriggers) {
|
|
110
140
|
driveTriggers = new Map();
|
|
111
|
-
this.syncStatus.set(driveId, 'SYNCING');
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
this.updateSyncStatus(driveId, 'SYNCING');
|
|
114
144
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
115
145
|
const intervalId = PullResponderTransmitter.setupPull(
|
|
116
146
|
driveId,
|
|
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,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
749
785
|
drive,
|
|
750
786
|
syncUnit.syncId,
|
|
751
787
|
syncUnit.revision,
|
|
752
|
-
syncUnit.lastUpdated
|
|
788
|
+
syncUnit.lastUpdated,
|
|
789
|
+
() => this.updateSyncStatus(drive, 'SYNCING'),
|
|
790
|
+
this.handleListenerError.bind(this)
|
|
791
|
+
)
|
|
792
|
+
.then(
|
|
793
|
+
updates =>
|
|
794
|
+
updates.length &&
|
|
795
|
+
this.updateSyncStatus(drive, 'SUCCESS')
|
|
753
796
|
)
|
|
754
797
|
.catch(error => {
|
|
755
|
-
console.error(
|
|
798
|
+
console.error(
|
|
799
|
+
'Non handled error updating sync revision',
|
|
800
|
+
error
|
|
801
|
+
);
|
|
802
|
+
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
756
803
|
});
|
|
757
804
|
}
|
|
758
805
|
|
|
@@ -875,10 +922,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
875
922
|
drive,
|
|
876
923
|
'0',
|
|
877
924
|
lastOperation.index,
|
|
878
|
-
lastOperation.timestamp
|
|
925
|
+
lastOperation.timestamp,
|
|
926
|
+
() => this.updateSyncStatus(drive, 'SYNCING'),
|
|
927
|
+
this.handleListenerError.bind(this)
|
|
928
|
+
)
|
|
929
|
+
.then(
|
|
930
|
+
updates =>
|
|
931
|
+
updates.length &&
|
|
932
|
+
this.updateSyncStatus(drive, 'SUCCESS')
|
|
879
933
|
)
|
|
880
934
|
.catch(error => {
|
|
881
|
-
console.error(
|
|
935
|
+
console.error(
|
|
936
|
+
'Non handled error updating sync revision',
|
|
937
|
+
error
|
|
938
|
+
);
|
|
939
|
+
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
882
940
|
});
|
|
883
941
|
}
|
|
884
942
|
|
|
@@ -935,4 +993,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
935
993
|
}
|
|
936
994
|
return status;
|
|
937
995
|
}
|
|
996
|
+
|
|
997
|
+
on<K extends keyof DriveEvents>(event: K, cb: DriveEvents[K]): Unsubscribe {
|
|
998
|
+
return this.emitter.on(event, cb);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
protected emit<K extends keyof DriveEvents>(
|
|
1002
|
+
event: K,
|
|
1003
|
+
...args: Parameters<DriveEvents[K]>
|
|
1004
|
+
): void {
|
|
1005
|
+
return this.emitter.emit(event, ...args);
|
|
1006
|
+
}
|
|
938
1007
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
ErrorStatus,
|
|
10
10
|
Listener,
|
|
11
11
|
ListenerState,
|
|
12
|
+
ListenerUpdate,
|
|
13
|
+
OperationUpdate,
|
|
12
14
|
StrandUpdate,
|
|
13
15
|
SynchronizationUnit
|
|
14
16
|
} from '../types';
|
|
@@ -120,14 +122,20 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
120
122
|
driveId: string,
|
|
121
123
|
syncId: string,
|
|
122
124
|
syncRev: number,
|
|
123
|
-
lastUpdated: string
|
|
125
|
+
lastUpdated: string,
|
|
126
|
+
willUpdate?: (listeners: Listener[]) => void,
|
|
127
|
+
onError?: (
|
|
128
|
+
error: Error,
|
|
129
|
+
driveId: string,
|
|
130
|
+
listener: ListenerState
|
|
131
|
+
) => void
|
|
124
132
|
) {
|
|
125
133
|
const drive = this.listenerState.get(driveId);
|
|
126
134
|
if (!drive) {
|
|
127
|
-
return;
|
|
135
|
+
return [];
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
|
|
138
|
+
const outdatedListeners: Listener[] = [];
|
|
131
139
|
for (const [, listener] of drive) {
|
|
132
140
|
const syncUnits = listener.syncUnits.filter(
|
|
133
141
|
e => e.syncId === syncId
|
|
@@ -143,13 +151,21 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
143
151
|
|
|
144
152
|
syncUnit.syncRev = syncRev;
|
|
145
153
|
syncUnit.lastUpdated = lastUpdated;
|
|
146
|
-
|
|
154
|
+
if (
|
|
155
|
+
!outdatedListeners.find(
|
|
156
|
+
l => l.listenerId === listener.listener.listenerId
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
outdatedListeners.push(listener.listener);
|
|
160
|
+
}
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
164
|
+
if (outdatedListeners.length) {
|
|
165
|
+
willUpdate?.(outdatedListeners);
|
|
166
|
+
return this.triggerUpdate(onError);
|
|
152
167
|
}
|
|
168
|
+
return [];
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
async addSyncUnits(syncUnits: SynchronizationUnit[]) {
|
|
@@ -202,6 +218,19 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
|
|
221
|
+
removeSyncUnits(syncUnits: SynchronizationUnit[]) {
|
|
222
|
+
for (const [driveId, drive] of this.listenerState) {
|
|
223
|
+
const syncIds = syncUnits
|
|
224
|
+
.filter(s => s.driveId === driveId)
|
|
225
|
+
.map(s => s.syncId);
|
|
226
|
+
for (const [, listenerState] of drive) {
|
|
227
|
+
listenerState.syncUnits = listenerState.syncUnits.filter(
|
|
228
|
+
s => !syncIds.includes(s.syncId)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
205
234
|
async updateListenerRevision(
|
|
206
235
|
listenerId: string,
|
|
207
236
|
driveId: string,
|
|
@@ -225,7 +254,14 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
225
254
|
}
|
|
226
255
|
}
|
|
227
256
|
|
|
228
|
-
async triggerUpdate(
|
|
257
|
+
async triggerUpdate(
|
|
258
|
+
onError?: (
|
|
259
|
+
error: Error,
|
|
260
|
+
driveId: string,
|
|
261
|
+
listener: ListenerState
|
|
262
|
+
) => void
|
|
263
|
+
) {
|
|
264
|
+
const listenerUpdates: ListenerUpdate[] = [];
|
|
229
265
|
for (const [driveId, drive] of this.listenerState) {
|
|
230
266
|
for (const [id, listener] of drive) {
|
|
231
267
|
const transmitter = await this.getTransmitter(driveId, id);
|
|
@@ -248,13 +284,19 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
248
284
|
continue;
|
|
249
285
|
}
|
|
250
286
|
|
|
251
|
-
const opData =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
const opData: OperationUpdate[] = [];
|
|
288
|
+
try {
|
|
289
|
+
const data = await this.drive.getOperationData(
|
|
290
|
+
driveId,
|
|
291
|
+
syncId,
|
|
292
|
+
{
|
|
293
|
+
fromRevision: listenerRev
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
opData.push(...data);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.error(e);
|
|
299
|
+
}
|
|
258
300
|
|
|
259
301
|
if (!opData.length) {
|
|
260
302
|
continue;
|
|
@@ -307,14 +349,19 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
307
349
|
);
|
|
308
350
|
}
|
|
309
351
|
listener.listenerStatus = 'SUCCESS';
|
|
352
|
+
listenerUpdates.push({
|
|
353
|
+
listenerId: listener.listener.listenerId,
|
|
354
|
+
listenerRevisions
|
|
355
|
+
});
|
|
310
356
|
} catch (e) {
|
|
311
357
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
358
|
+
onError?.(e as Error, driveId, listener);
|
|
312
359
|
listener.listenerStatus =
|
|
313
360
|
e instanceof OperationError ? e.status : 'ERROR';
|
|
314
|
-
throw e;
|
|
315
361
|
}
|
|
316
362
|
}
|
|
317
363
|
}
|
|
364
|
+
return listenerUpdates;
|
|
318
365
|
}
|
|
319
366
|
|
|
320
367
|
private _checkFilter(
|
|
@@ -218,6 +218,87 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
218
218
|
return result.acknowledge;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
private static async executePull(
|
|
222
|
+
driveId: string,
|
|
223
|
+
trigger: PullResponderTrigger,
|
|
224
|
+
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
225
|
+
onError: (error: Error) => void,
|
|
226
|
+
onAcknowledge?: (success: boolean) => void
|
|
227
|
+
) {
|
|
228
|
+
try {
|
|
229
|
+
const { url, listenerId } = trigger.data;
|
|
230
|
+
const strands = await PullResponderTransmitter.pullStrands(
|
|
231
|
+
driveId,
|
|
232
|
+
url,
|
|
233
|
+
listenerId
|
|
234
|
+
// since ?
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// if there are no new strands then do nothing
|
|
238
|
+
if (!strands.length) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const listenerRevisions: ListenerRevision[] = [];
|
|
243
|
+
|
|
244
|
+
for (const strand of strands) {
|
|
245
|
+
const operations: Operation[] = strand.operations.map(
|
|
246
|
+
({ index, type, hash, input, skip, timestamp }) => ({
|
|
247
|
+
index,
|
|
248
|
+
type,
|
|
249
|
+
hash,
|
|
250
|
+
input,
|
|
251
|
+
skip,
|
|
252
|
+
timestamp,
|
|
253
|
+
scope: strand.scope,
|
|
254
|
+
branch: strand.branch
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
let error: Error | undefined = undefined;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const result = await onStrandUpdate(strand);
|
|
262
|
+
if (result.error) {
|
|
263
|
+
throw result.error;
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
error = e as Error;
|
|
267
|
+
onError(error);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
listenerRevisions.push({
|
|
271
|
+
branch: strand.branch,
|
|
272
|
+
documentId: strand.documentId || '',
|
|
273
|
+
driveId: strand.driveId,
|
|
274
|
+
revision: operations.pop()?.index ?? -1,
|
|
275
|
+
scope: strand.scope as OperationScope,
|
|
276
|
+
status: error
|
|
277
|
+
? error instanceof OperationError
|
|
278
|
+
? error.status
|
|
279
|
+
: 'ERROR'
|
|
280
|
+
: 'SUCCESS'
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// TODO: Should try to parse remaining strands?
|
|
284
|
+
if (error) {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await PullResponderTransmitter.acknowledgeStrands(
|
|
290
|
+
driveId,
|
|
291
|
+
url,
|
|
292
|
+
listenerId,
|
|
293
|
+
listenerRevisions
|
|
294
|
+
)
|
|
295
|
+
.then(result => onAcknowledge?.(result))
|
|
296
|
+
.catch(error => console.error('ACK error', error));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
onError(error as Error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
221
302
|
static setupPull(
|
|
222
303
|
driveId: string,
|
|
223
304
|
trigger: PullResponderTrigger,
|
|
@@ -225,7 +306,7 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
225
306
|
onError: (error: Error) => void,
|
|
226
307
|
onAcknowledge?: (success: boolean) => void
|
|
227
308
|
): number {
|
|
228
|
-
const {
|
|
309
|
+
const { interval } = trigger.data;
|
|
229
310
|
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
230
311
|
if (interval) {
|
|
231
312
|
try {
|
|
@@ -238,79 +319,25 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
238
319
|
}
|
|
239
320
|
}
|
|
240
321
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// if there are no new strands then do nothing
|
|
251
|
-
if (!strands.length) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const listenerRevisions: ListenerRevision[] = [];
|
|
256
|
-
|
|
257
|
-
for (const strand of strands) {
|
|
258
|
-
const operations: Operation[] = strand.operations.map(
|
|
259
|
-
({ index, type, hash, input, skip, timestamp }) => ({
|
|
260
|
-
index,
|
|
261
|
-
type,
|
|
262
|
-
hash,
|
|
263
|
-
input,
|
|
264
|
-
skip,
|
|
265
|
-
timestamp,
|
|
266
|
-
scope: strand.scope,
|
|
267
|
-
branch: strand.branch
|
|
268
|
-
})
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
let error: Error | undefined = undefined;
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const result = await onStrandUpdate(strand);
|
|
275
|
-
if (result.error) {
|
|
276
|
-
throw result.error;
|
|
277
|
-
}
|
|
278
|
-
} catch (e) {
|
|
279
|
-
error = e as Error;
|
|
280
|
-
onError?.(error);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
listenerRevisions.push({
|
|
284
|
-
branch: strand.branch,
|
|
285
|
-
documentId: strand.documentId ?? '',
|
|
286
|
-
driveId: strand.driveId,
|
|
287
|
-
revision: operations.pop()?.index ?? -1,
|
|
288
|
-
scope: strand.scope as OperationScope,
|
|
289
|
-
status: error
|
|
290
|
-
? error instanceof OperationError
|
|
291
|
-
? error.status
|
|
292
|
-
: 'ERROR'
|
|
293
|
-
: 'SUCCESS'
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// TODO: Should try to parse remaining strands?
|
|
297
|
-
if (error) {
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
322
|
+
this.executePull(
|
|
323
|
+
driveId,
|
|
324
|
+
trigger,
|
|
325
|
+
onStrandUpdate,
|
|
326
|
+
onError,
|
|
327
|
+
onAcknowledge
|
|
328
|
+
);
|
|
301
329
|
|
|
302
|
-
|
|
330
|
+
const timeout = setInterval(
|
|
331
|
+
async () =>
|
|
332
|
+
this.executePull(
|
|
303
333
|
driveId,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
onError(error as Error);
|
|
312
|
-
}
|
|
313
|
-
}, loopInterval);
|
|
334
|
+
trigger,
|
|
335
|
+
onStrandUpdate,
|
|
336
|
+
onError,
|
|
337
|
+
onAcknowledge
|
|
338
|
+
),
|
|
339
|
+
loopInterval
|
|
340
|
+
);
|
|
314
341
|
return timeout as unknown as number;
|
|
315
342
|
}
|
|
316
343
|
|
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
|
|
|
@@ -92,6 +93,11 @@ export type ListenerRevision = {
|
|
|
92
93
|
revision: number;
|
|
93
94
|
};
|
|
94
95
|
|
|
96
|
+
export type ListenerUpdate = {
|
|
97
|
+
listenerId: string;
|
|
98
|
+
listenerRevisions: ListenerRevision[];
|
|
99
|
+
};
|
|
100
|
+
|
|
95
101
|
export type UpdateStatus = 'SUCCESS' | 'CONFLICT' | 'MISSING' | 'ERROR';
|
|
96
102
|
export type ErrorStatus = Exclude<UpdateStatus, 'SUCCESS'>;
|
|
97
103
|
|
|
@@ -114,6 +120,11 @@ export type StrandUpdate = {
|
|
|
114
120
|
|
|
115
121
|
export type SyncStatus = 'SYNCING' | UpdateStatus;
|
|
116
122
|
|
|
123
|
+
export interface DriveEvents {
|
|
124
|
+
syncStatus: (driveId: string, status: SyncStatus, error?: Error) => void;
|
|
125
|
+
strandUpdate: (update: StrandUpdate) => void;
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
export abstract class BaseDocumentDriveServer {
|
|
118
129
|
/** Public methods **/
|
|
119
130
|
abstract getDrives(): Promise<string[]>;
|
|
@@ -132,12 +143,12 @@ export abstract class BaseDocumentDriveServer {
|
|
|
132
143
|
drive: string,
|
|
133
144
|
id: string,
|
|
134
145
|
operation: Operation
|
|
135
|
-
): Promise<IOperationResult
|
|
146
|
+
): Promise<IOperationResult>;
|
|
136
147
|
abstract addOperations(
|
|
137
148
|
drive: string,
|
|
138
149
|
id: string,
|
|
139
150
|
operations: Operation[]
|
|
140
|
-
): Promise<IOperationResult
|
|
151
|
+
): Promise<IOperationResult>;
|
|
141
152
|
|
|
142
153
|
abstract addDriveOperation(
|
|
143
154
|
drive: string,
|
|
@@ -183,12 +194,23 @@ export abstract class BaseDocumentDriveServer {
|
|
|
183
194
|
driveId: string,
|
|
184
195
|
listenerId: string
|
|
185
196
|
): Promise<ITransmitter | undefined>;
|
|
197
|
+
|
|
198
|
+
/** Event methods **/
|
|
199
|
+
protected abstract emit<K extends keyof DriveEvents>(
|
|
200
|
+
this: this,
|
|
201
|
+
event: K,
|
|
202
|
+
...args: Parameters<DriveEvents[K]>
|
|
203
|
+
): void;
|
|
204
|
+
abstract on<K extends keyof DriveEvents>(
|
|
205
|
+
this: this,
|
|
206
|
+
event: K,
|
|
207
|
+
cb: DriveEvents[K]
|
|
208
|
+
): Unsubscribe;
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
export abstract class BaseListenerManager {
|
|
189
212
|
protected drive: BaseDocumentDriveServer;
|
|
190
|
-
protected listenerState
|
|
191
|
-
new Map();
|
|
213
|
+
protected listenerState = new Map<string, Map<string, ListenerState>>();
|
|
192
214
|
protected transmitters: Record<
|
|
193
215
|
DocumentDriveState['id'],
|
|
194
216
|
Record<Listener['listenerId'], ITransmitter>
|
|
@@ -196,7 +218,7 @@ export abstract class BaseListenerManager {
|
|
|
196
218
|
|
|
197
219
|
constructor(
|
|
198
220
|
drive: BaseDocumentDriveServer,
|
|
199
|
-
listenerState
|
|
221
|
+
listenerState = new Map<string, Map<string, ListenerState>>()
|
|
200
222
|
) {
|
|
201
223
|
this.drive = drive;
|
|
202
224
|
this.listenerState = listenerState;
|
|
@@ -216,8 +238,14 @@ export abstract class BaseListenerManager {
|
|
|
216
238
|
driveId: string,
|
|
217
239
|
syncId: string,
|
|
218
240
|
syncRev: number,
|
|
219
|
-
lastUpdated: string
|
|
220
|
-
|
|
241
|
+
lastUpdated: string,
|
|
242
|
+
willUpdate?: (listeners: Listener[]) => void,
|
|
243
|
+
onError?: (
|
|
244
|
+
error: Error,
|
|
245
|
+
driveId: string,
|
|
246
|
+
listener: ListenerState
|
|
247
|
+
) => void
|
|
248
|
+
): Promise<ListenerUpdate[]>;
|
|
221
249
|
|
|
222
250
|
abstract updateListenerRevision(
|
|
223
251
|
listenerId: string,
|
package/src/storage/prisma.ts
CHANGED
|
@@ -119,28 +119,6 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
119
119
|
} catch (e) {
|
|
120
120
|
console.log(e);
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
await this.db.document.upsert({
|
|
124
|
-
where: {
|
|
125
|
-
id_driveId: {
|
|
126
|
-
id: 'drives',
|
|
127
|
-
driveId: id
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
create: {
|
|
131
|
-
id: 'drives',
|
|
132
|
-
driveId: id,
|
|
133
|
-
documentType: header.documentType,
|
|
134
|
-
initialState: document.initialState,
|
|
135
|
-
lastModified: header.lastModified,
|
|
136
|
-
revision: header.revision,
|
|
137
|
-
created: header.created
|
|
138
|
-
},
|
|
139
|
-
update: {
|
|
140
|
-
lastModified: header.lastModified,
|
|
141
|
-
revision: header.revision
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
122
|
}
|
|
145
123
|
|
|
146
124
|
async getDocuments(drive: string) {
|