document-drive 0.0.30 → 1.0.0-alpha.10
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 +9 -3
- package/src/server/index.ts +58 -18
- package/src/server/listener/decorator.ts +28 -0
- package/src/server/listener/manager.ts +64 -6
- package/src/server/listener/transmitter/pull-responder.ts +129 -113
- package/src/server/types.ts +32 -7
- package/src/storage/prisma.ts +33 -96
- package/src/utils/index.ts +5 -0
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.10",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -24,6 +24,7 @@
|
|
|
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
|
},
|
|
@@ -43,14 +44,18 @@
|
|
|
43
44
|
"sanitize-filename": "^1.6.3"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
47
|
+
"@commitlint/cli": "^18.6.1",
|
|
48
|
+
"@commitlint/config-conventional": "^18.6.2",
|
|
46
49
|
"@prisma/client": "5.8.1",
|
|
50
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
51
|
+
"@semantic-release/git": "^10.0.1",
|
|
47
52
|
"@total-typescript/ts-reset": "^0.5.1",
|
|
48
53
|
"@types/node": "^20.11.16",
|
|
49
54
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
|
50
55
|
"@typescript-eslint/parser": "^6.18.1",
|
|
51
56
|
"@vitest/coverage-v8": "^0.34.6",
|
|
52
|
-
"document-model": "^1.0.
|
|
53
|
-
"document-model-libs": "^1.1.
|
|
57
|
+
"document-model": "^1.0.30",
|
|
58
|
+
"document-model-libs": "^1.1.51",
|
|
54
59
|
"eslint": "^8.56.0",
|
|
55
60
|
"eslint-config-prettier": "^9.1.0",
|
|
56
61
|
"fake-indexeddb": "^5.0.1",
|
|
@@ -58,6 +63,7 @@
|
|
|
58
63
|
"msw": "^2.1.2",
|
|
59
64
|
"prettier": "^3.1.1",
|
|
60
65
|
"prettier-plugin-organize-imports": "^3.2.4",
|
|
66
|
+
"semantic-release": "^23.0.2",
|
|
61
67
|
"sequelize": "^6.35.2",
|
|
62
68
|
"sqlite3": "^5.1.7",
|
|
63
69
|
"typescript": "^5.3.2",
|
package/src/server/index.ts
CHANGED
|
@@ -19,12 +19,20 @@ import {
|
|
|
19
19
|
import { createNanoEvents, Unsubscribe } from 'nanoevents';
|
|
20
20
|
import { MemoryStorage } from '../storage/memory';
|
|
21
21
|
import type { DocumentStorage, IDriveStorage } from '../storage/types';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
generateUUID,
|
|
24
|
+
isBefore,
|
|
25
|
+
isDocumentDrive,
|
|
26
|
+
isNoopUpdate
|
|
27
|
+
} from '../utils';
|
|
23
28
|
import { requestPublicDrive } from '../utils/graphql';
|
|
24
29
|
import { OperationError } from './error';
|
|
25
30
|
import { ListenerManager } from './listener/manager';
|
|
26
|
-
import {
|
|
27
|
-
|
|
31
|
+
import {
|
|
32
|
+
CancelPullLoop,
|
|
33
|
+
ITransmitter,
|
|
34
|
+
PullResponderTransmitter
|
|
35
|
+
} from './listener/transmitter';
|
|
28
36
|
import {
|
|
29
37
|
BaseDocumentDriveServer,
|
|
30
38
|
DriveEvents,
|
|
@@ -52,7 +60,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
52
60
|
private listenerStateManager: ListenerManager;
|
|
53
61
|
private triggerMap = new Map<
|
|
54
62
|
DocumentDriveState['id'],
|
|
55
|
-
Map<Trigger['id'],
|
|
63
|
+
Map<Trigger['id'], CancelPullLoop>
|
|
56
64
|
>();
|
|
57
65
|
private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
|
|
58
66
|
|
|
@@ -100,7 +108,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
100
108
|
operations
|
|
101
109
|
));
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
if (result.status === 'ERROR') {
|
|
112
|
+
this.updateSyncStatus(strand.driveId, result.status, result.error);
|
|
113
|
+
}
|
|
104
114
|
this.emit('strandUpdate', strand);
|
|
105
115
|
return result;
|
|
106
116
|
}
|
|
@@ -138,11 +148,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
138
148
|
|
|
139
149
|
if (!driveTriggers) {
|
|
140
150
|
driveTriggers = new Map();
|
|
141
|
-
this.updateSyncStatus(driveId, 'SYNCING');
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
this.updateSyncStatus(driveId, 'SYNCING');
|
|
144
154
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
145
|
-
const
|
|
155
|
+
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
146
156
|
driveId,
|
|
147
157
|
trigger,
|
|
148
158
|
this.saveStrand.bind(this),
|
|
@@ -151,12 +161,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
151
161
|
driveId,
|
|
152
162
|
error instanceof OperationError
|
|
153
163
|
? error.status
|
|
154
|
-
: 'ERROR'
|
|
164
|
+
: 'ERROR',
|
|
165
|
+
error
|
|
155
166
|
);
|
|
156
167
|
},
|
|
157
|
-
|
|
168
|
+
revisions => {
|
|
169
|
+
const errorRevision = revisions.find(
|
|
170
|
+
r => r.status !== 'SUCCESS'
|
|
171
|
+
);
|
|
172
|
+
if (!errorRevision) {
|
|
173
|
+
this.updateSyncStatus(driveId, 'SUCCESS');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
158
176
|
);
|
|
159
|
-
driveTriggers.set(trigger.id,
|
|
177
|
+
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
160
178
|
this.triggerMap.set(driveId, driveTriggers);
|
|
161
179
|
}
|
|
162
180
|
}
|
|
@@ -164,7 +182,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
164
182
|
|
|
165
183
|
private async stopSyncRemoteDrive(driveId: string) {
|
|
166
184
|
const triggers = this.triggerMap.get(driveId);
|
|
167
|
-
triggers?.forEach(
|
|
185
|
+
triggers?.forEach(cancel => cancel());
|
|
168
186
|
return this.triggerMap.delete(driveId);
|
|
169
187
|
}
|
|
170
188
|
|
|
@@ -292,10 +310,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
292
310
|
const filteredOperations = operations.filter(
|
|
293
311
|
operation =>
|
|
294
312
|
Object.keys(filter).length === 0 ||
|
|
295
|
-
(filter.since
|
|
296
|
-
filter.since
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
((filter.since === undefined ||
|
|
314
|
+
isBefore(filter.since, operation.timestamp)) &&
|
|
315
|
+
(filter.fromRevision === undefined ||
|
|
316
|
+
operation.index > filter.fromRevision))
|
|
299
317
|
);
|
|
300
318
|
|
|
301
319
|
return filteredOperations.map(operation => ({
|
|
@@ -607,9 +625,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
607
625
|
);
|
|
608
626
|
continue;
|
|
609
627
|
} else if (op.index < nextIndex) {
|
|
610
|
-
const existingOperation = scopeOperations
|
|
611
|
-
|
|
612
|
-
|
|
628
|
+
const existingOperation = scopeOperations
|
|
629
|
+
.concat(pastOperations)
|
|
630
|
+
.find(
|
|
631
|
+
existingOperation =>
|
|
632
|
+
existingOperation.index === op.index
|
|
633
|
+
);
|
|
613
634
|
if (existingOperation && existingOperation.hash !== op.hash) {
|
|
614
635
|
error = new OperationError(
|
|
615
636
|
'CONFLICT',
|
|
@@ -786,8 +807,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
786
807
|
syncUnit.syncId,
|
|
787
808
|
syncUnit.revision,
|
|
788
809
|
syncUnit.lastUpdated,
|
|
810
|
+
() => this.updateSyncStatus(drive, 'SYNCING'),
|
|
789
811
|
this.handleListenerError.bind(this)
|
|
790
812
|
)
|
|
813
|
+
.then(
|
|
814
|
+
updates =>
|
|
815
|
+
updates.length &&
|
|
816
|
+
this.updateSyncStatus(drive, 'SUCCESS')
|
|
817
|
+
)
|
|
791
818
|
.catch(error => {
|
|
792
819
|
console.error(
|
|
793
820
|
'Non handled error updating sync revision',
|
|
@@ -917,8 +944,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
917
944
|
'0',
|
|
918
945
|
lastOperation.index,
|
|
919
946
|
lastOperation.timestamp,
|
|
947
|
+
() => this.updateSyncStatus(drive, 'SYNCING'),
|
|
920
948
|
this.handleListenerError.bind(this)
|
|
921
949
|
)
|
|
950
|
+
.then(
|
|
951
|
+
updates =>
|
|
952
|
+
updates.length &&
|
|
953
|
+
this.updateSyncStatus(drive, 'SUCCESS')
|
|
954
|
+
)
|
|
922
955
|
.catch(error => {
|
|
923
956
|
console.error(
|
|
924
957
|
'Non handled error updating sync revision',
|
|
@@ -974,6 +1007,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
974
1007
|
return this.listenerStateManager.getTransmitter(driveId, listenerId);
|
|
975
1008
|
}
|
|
976
1009
|
|
|
1010
|
+
getListener(
|
|
1011
|
+
driveId: string,
|
|
1012
|
+
listenerId: string
|
|
1013
|
+
): Promise<ListenerState | undefined> {
|
|
1014
|
+
return this.listenerStateManager.getListener(driveId, listenerId);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
977
1017
|
getSyncStatus(drive: string): SyncStatus {
|
|
978
1018
|
const status = this.syncStatus.get(drive);
|
|
979
1019
|
if (!status) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DocumentDriveServer } from '..';
|
|
2
|
+
|
|
3
|
+
function ListenerManagerDecorator(constructor: new () => DocumentDriveServer) {
|
|
4
|
+
return class extends constructor {
|
|
5
|
+
// Define extra methods here
|
|
6
|
+
extraMethod(): void {
|
|
7
|
+
// Access private variables of the original class
|
|
8
|
+
console.log('Accessing private variable:', this.getLi);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Define your original class
|
|
14
|
+
class OriginalClass {
|
|
15
|
+
private privateVariable: string;
|
|
16
|
+
|
|
17
|
+
constructor(privateVariable: string) {
|
|
18
|
+
this.privateVariable = privateVariable;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Define other methods and properties here
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use the decorator to augment the original class with extra methods
|
|
25
|
+
const AugmentedClass = ExtraMethodsDecorator(OriginalClass);
|
|
26
|
+
|
|
27
|
+
// Create an instance of the augmented class
|
|
28
|
+
const instance = new AugmentedClass('private data');
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ErrorStatus,
|
|
10
10
|
Listener,
|
|
11
11
|
ListenerState,
|
|
12
|
+
ListenerUpdate,
|
|
12
13
|
OperationUpdate,
|
|
13
14
|
StrandUpdate,
|
|
14
15
|
SynchronizationUnit
|
|
@@ -122,6 +123,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
122
123
|
syncId: string,
|
|
123
124
|
syncRev: number,
|
|
124
125
|
lastUpdated: string,
|
|
126
|
+
willUpdate?: (listeners: Listener[]) => void,
|
|
125
127
|
onError?: (
|
|
126
128
|
error: Error,
|
|
127
129
|
driveId: string,
|
|
@@ -130,10 +132,10 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
130
132
|
) {
|
|
131
133
|
const drive = this.listenerState.get(driveId);
|
|
132
134
|
if (!drive) {
|
|
133
|
-
return;
|
|
135
|
+
return [];
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
|
|
138
|
+
const outdatedListeners: Listener[] = [];
|
|
137
139
|
for (const [, listener] of drive) {
|
|
138
140
|
const syncUnits = listener.syncUnits.filter(
|
|
139
141
|
e => e.syncId === syncId
|
|
@@ -149,13 +151,21 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
149
151
|
|
|
150
152
|
syncUnit.syncRev = syncRev;
|
|
151
153
|
syncUnit.lastUpdated = lastUpdated;
|
|
152
|
-
|
|
154
|
+
if (
|
|
155
|
+
!outdatedListeners.find(
|
|
156
|
+
l => l.listenerId === listener.listener.listenerId
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
outdatedListeners.push(listener.listener);
|
|
160
|
+
}
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
if (
|
|
164
|
+
if (outdatedListeners.length) {
|
|
165
|
+
willUpdate?.(outdatedListeners);
|
|
157
166
|
return this.triggerUpdate(onError);
|
|
158
167
|
}
|
|
168
|
+
return [];
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
async addSyncUnits(syncUnits: SynchronizationUnit[]) {
|
|
@@ -251,6 +261,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
251
261
|
listener: ListenerState
|
|
252
262
|
) => void
|
|
253
263
|
) {
|
|
264
|
+
const listenerUpdates: ListenerUpdate[] = [];
|
|
254
265
|
for (const [driveId, drive] of this.listenerState) {
|
|
255
266
|
for (const [id, listener] of drive) {
|
|
256
267
|
const transmitter = await this.getTransmitter(driveId, id);
|
|
@@ -338,6 +349,10 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
338
349
|
);
|
|
339
350
|
}
|
|
340
351
|
listener.listenerStatus = 'SUCCESS';
|
|
352
|
+
listenerUpdates.push({
|
|
353
|
+
listenerId: listener.listener.listenerId,
|
|
354
|
+
listenerRevisions
|
|
355
|
+
});
|
|
341
356
|
} catch (e) {
|
|
342
357
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
343
358
|
onError?.(e as Error, driveId, listener);
|
|
@@ -346,6 +361,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
346
361
|
}
|
|
347
362
|
}
|
|
348
363
|
}
|
|
364
|
+
return listenerUpdates;
|
|
349
365
|
}
|
|
350
366
|
|
|
351
367
|
private _checkFilter(
|
|
@@ -403,11 +419,53 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
403
419
|
}
|
|
404
420
|
}
|
|
405
421
|
|
|
406
|
-
getListener(driveId: string, listenerId: string): ListenerState {
|
|
422
|
+
getListener(driveId: string, listenerId: string): Promise<ListenerState> {
|
|
407
423
|
const drive = this.listenerState.get(driveId);
|
|
408
424
|
if (!drive) throw new Error('Drive not found');
|
|
409
425
|
const listener = drive.get(listenerId);
|
|
410
426
|
if (!listener) throw new Error('Listener not found');
|
|
411
|
-
return listener;
|
|
427
|
+
return Promise.resolve(listener);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async getStrands(
|
|
431
|
+
driveId: string,
|
|
432
|
+
listenerId: string,
|
|
433
|
+
since?: string
|
|
434
|
+
): Promise<StrandUpdate[]> {
|
|
435
|
+
// fetch listenerState from listenerManager
|
|
436
|
+
const entries = await this.getListener(driveId, listenerId);
|
|
437
|
+
|
|
438
|
+
// fetch operations from drive and prepare strands
|
|
439
|
+
const strands: StrandUpdate[] = [];
|
|
440
|
+
|
|
441
|
+
for (const entry of entries.syncUnits) {
|
|
442
|
+
if (entry.listenerRev >= entry.syncRev) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const { documentId, driveId, scope, branch } = entry;
|
|
447
|
+
const operations = await this.drive.getOperationData(
|
|
448
|
+
entry.driveId,
|
|
449
|
+
entry.syncId,
|
|
450
|
+
{
|
|
451
|
+
since,
|
|
452
|
+
fromRevision: entry.listenerRev
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (!operations.length) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
strands.push({
|
|
461
|
+
driveId,
|
|
462
|
+
documentId,
|
|
463
|
+
scope: scope as OperationScope,
|
|
464
|
+
branch,
|
|
465
|
+
operations
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return strands;
|
|
412
470
|
}
|
|
413
471
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
IOperationResult,
|
|
9
9
|
Listener,
|
|
10
10
|
ListenerRevision,
|
|
11
|
+
ListenerRevisionWithError,
|
|
11
12
|
OperationUpdate,
|
|
12
13
|
StrandUpdate
|
|
13
14
|
} from '../../types';
|
|
@@ -26,11 +27,17 @@ export type PullStrandsGraphQL = {
|
|
|
26
27
|
};
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
export type CancelPullLoop = () => void;
|
|
31
|
+
|
|
29
32
|
export type StrandUpdateGraphQL = Omit<StrandUpdate, 'operations'> & {
|
|
30
33
|
operations: OperationUpdateGraphQL[];
|
|
31
34
|
};
|
|
32
35
|
|
|
33
|
-
export
|
|
36
|
+
export interface IPullResponderTransmitter extends ITransmitter {
|
|
37
|
+
getStrands(since?: string): Promise<StrandUpdate[]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
34
41
|
private drive: BaseDocumentDriveServer;
|
|
35
42
|
private listener: Listener;
|
|
36
43
|
private manager: ListenerManager;
|
|
@@ -49,48 +56,12 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
49
56
|
return [];
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
since?: string
|
|
55
|
-
): Promise<StrandUpdate[]> {
|
|
56
|
-
// fetch listenerState from listenerManager
|
|
57
|
-
const entries = this.manager.getListener(
|
|
59
|
+
getStrands(since?: string | undefined): Promise<StrandUpdate[]> {
|
|
60
|
+
return this.manager.getStrands(
|
|
58
61
|
this.listener.driveId,
|
|
59
|
-
listenerId
|
|
62
|
+
this.listener.listenerId,
|
|
63
|
+
since
|
|
60
64
|
);
|
|
61
|
-
|
|
62
|
-
// fetch operations from drive and prepare strands
|
|
63
|
-
const strands: StrandUpdate[] = [];
|
|
64
|
-
|
|
65
|
-
for (const entry of entries.syncUnits) {
|
|
66
|
-
if (entry.listenerRev >= entry.syncRev) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const { documentId, driveId, scope, branch } = entry;
|
|
71
|
-
const operations = await this.drive.getOperationData(
|
|
72
|
-
entry.driveId,
|
|
73
|
-
entry.syncId,
|
|
74
|
-
{
|
|
75
|
-
since,
|
|
76
|
-
fromRevision: entry.listenerRev
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
if (!operations.length) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
strands.push({
|
|
85
|
-
driveId,
|
|
86
|
-
documentId,
|
|
87
|
-
scope: scope as OperationScope,
|
|
88
|
-
branch,
|
|
89
|
-
operations
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return strands;
|
|
94
65
|
}
|
|
95
66
|
|
|
96
67
|
async processAcknowledge(
|
|
@@ -98,7 +69,7 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
98
69
|
listenerId: string,
|
|
99
70
|
revisions: ListenerRevision[]
|
|
100
71
|
): Promise<boolean> {
|
|
101
|
-
const listener = this.manager.getListener(driveId, listenerId);
|
|
72
|
+
const listener = await this.manager.getListener(driveId, listenerId);
|
|
102
73
|
|
|
103
74
|
let success = true;
|
|
104
75
|
for (const revision of revisions) {
|
|
@@ -218,14 +189,103 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
218
189
|
return result.acknowledge;
|
|
219
190
|
}
|
|
220
191
|
|
|
192
|
+
private static async executePull(
|
|
193
|
+
driveId: string,
|
|
194
|
+
trigger: PullResponderTrigger,
|
|
195
|
+
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
196
|
+
onError: (error: Error) => void,
|
|
197
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
198
|
+
onAcknowledge?: (success: boolean) => void
|
|
199
|
+
) {
|
|
200
|
+
try {
|
|
201
|
+
const { url, listenerId } = trigger.data;
|
|
202
|
+
const strands = await PullResponderTransmitter.pullStrands(
|
|
203
|
+
driveId,
|
|
204
|
+
url,
|
|
205
|
+
listenerId
|
|
206
|
+
// since ?
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// if there are no new strands then do nothing
|
|
210
|
+
if (!strands.length) {
|
|
211
|
+
onRevisions?.([]);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const listenerRevisions: ListenerRevisionWithError[] = [];
|
|
216
|
+
|
|
217
|
+
for (const strand of strands) {
|
|
218
|
+
const operations: Operation[] = strand.operations.map(
|
|
219
|
+
({ index, type, hash, input, skip, timestamp }) => ({
|
|
220
|
+
index,
|
|
221
|
+
type,
|
|
222
|
+
hash,
|
|
223
|
+
input,
|
|
224
|
+
skip,
|
|
225
|
+
timestamp,
|
|
226
|
+
scope: strand.scope,
|
|
227
|
+
branch: strand.branch
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
let error: Error | undefined = undefined;
|
|
232
|
+
try {
|
|
233
|
+
const result = await onStrandUpdate(strand);
|
|
234
|
+
if (result.error) {
|
|
235
|
+
throw result.error;
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
error = e as Error;
|
|
239
|
+
onError(error);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
listenerRevisions.push({
|
|
243
|
+
branch: strand.branch,
|
|
244
|
+
documentId: strand.documentId || '',
|
|
245
|
+
driveId: strand.driveId,
|
|
246
|
+
revision: operations.pop()?.index ?? -1,
|
|
247
|
+
scope: strand.scope as OperationScope,
|
|
248
|
+
status: error
|
|
249
|
+
? error instanceof OperationError
|
|
250
|
+
? error.status
|
|
251
|
+
: 'ERROR'
|
|
252
|
+
: 'SUCCESS',
|
|
253
|
+
error
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// TODO: Should try to parse remaining strands?
|
|
257
|
+
// if (error) {
|
|
258
|
+
// break;
|
|
259
|
+
// }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onRevisions?.(listenerRevisions);
|
|
263
|
+
|
|
264
|
+
await PullResponderTransmitter.acknowledgeStrands(
|
|
265
|
+
driveId,
|
|
266
|
+
url,
|
|
267
|
+
listenerId,
|
|
268
|
+
listenerRevisions.map(revision => {
|
|
269
|
+
const { error, ...rest } = revision;
|
|
270
|
+
return rest;
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
.then(result => onAcknowledge?.(result))
|
|
274
|
+
.catch(error => console.error('ACK error', error));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
onError(error as Error);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
221
280
|
static setupPull(
|
|
222
281
|
driveId: string,
|
|
223
282
|
trigger: PullResponderTrigger,
|
|
224
283
|
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
225
284
|
onError: (error: Error) => void,
|
|
285
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
226
286
|
onAcknowledge?: (success: boolean) => void
|
|
227
|
-
):
|
|
228
|
-
const {
|
|
287
|
+
): CancelPullLoop {
|
|
288
|
+
const { interval } = trigger.data;
|
|
229
289
|
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
230
290
|
if (interval) {
|
|
231
291
|
try {
|
|
@@ -238,80 +298,36 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
238
298
|
}
|
|
239
299
|
}
|
|
240
300
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
301
|
+
let isCancelled = false;
|
|
302
|
+
let timeout: number | undefined;
|
|
303
|
+
|
|
304
|
+
const executeLoop = async () => {
|
|
305
|
+
while (!isCancelled) {
|
|
306
|
+
await this.executePull(
|
|
244
307
|
driveId,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
308
|
+
trigger,
|
|
309
|
+
onStrandUpdate,
|
|
310
|
+
onError,
|
|
311
|
+
onRevisions,
|
|
312
|
+
onAcknowledge
|
|
248
313
|
);
|
|
314
|
+
await new Promise(resolve => {
|
|
315
|
+
timeout = setTimeout(
|
|
316
|
+
resolve,
|
|
317
|
+
loopInterval
|
|
318
|
+
) as unknown as number;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
};
|
|
249
322
|
|
|
250
|
-
|
|
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
|
-
});
|
|
323
|
+
executeLoop().catch(console.error);
|
|
295
324
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
await PullResponderTransmitter.acknowledgeStrands(
|
|
303
|
-
driveId,
|
|
304
|
-
url,
|
|
305
|
-
listenerId,
|
|
306
|
-
listenerRevisions
|
|
307
|
-
)
|
|
308
|
-
.then(result => onAcknowledge?.(result))
|
|
309
|
-
.catch(error => console.error('ACK error', error));
|
|
310
|
-
} catch (error) {
|
|
311
|
-
onError(error as Error);
|
|
325
|
+
return () => {
|
|
326
|
+
isCancelled = true;
|
|
327
|
+
if (timeout !== undefined) {
|
|
328
|
+
clearTimeout(timeout);
|
|
312
329
|
}
|
|
313
|
-
}
|
|
314
|
-
return timeout as unknown as number;
|
|
330
|
+
};
|
|
315
331
|
}
|
|
316
332
|
|
|
317
333
|
static isPullResponderTrigger(
|
package/src/server/types.ts
CHANGED
|
@@ -93,6 +93,13 @@ export type ListenerRevision = {
|
|
|
93
93
|
revision: number;
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
export type ListenerRevisionWithError = ListenerRevision & { error?: Error };
|
|
97
|
+
|
|
98
|
+
export type ListenerUpdate = {
|
|
99
|
+
listenerId: string;
|
|
100
|
+
listenerRevisions: ListenerRevision[];
|
|
101
|
+
};
|
|
102
|
+
|
|
96
103
|
export type UpdateStatus = 'SUCCESS' | 'CONFLICT' | 'MISSING' | 'ERROR';
|
|
97
104
|
export type ErrorStatus = Exclude<UpdateStatus, 'SUCCESS'>;
|
|
98
105
|
|
|
@@ -185,11 +192,6 @@ export abstract class BaseDocumentDriveServer {
|
|
|
185
192
|
): Promise<Document>;
|
|
186
193
|
protected abstract deleteDocument(drive: string, id: string): Promise<void>;
|
|
187
194
|
|
|
188
|
-
abstract getTransmitter(
|
|
189
|
-
driveId: string,
|
|
190
|
-
listenerId: string
|
|
191
|
-
): Promise<ITransmitter | undefined>;
|
|
192
|
-
|
|
193
195
|
/** Event methods **/
|
|
194
196
|
protected abstract emit<K extends keyof DriveEvents>(
|
|
195
197
|
this: this,
|
|
@@ -201,6 +203,11 @@ export abstract class BaseDocumentDriveServer {
|
|
|
201
203
|
event: K,
|
|
202
204
|
cb: DriveEvents[K]
|
|
203
205
|
): Unsubscribe;
|
|
206
|
+
|
|
207
|
+
abstract getTransmitter(
|
|
208
|
+
driveId: string,
|
|
209
|
+
listenerId: string
|
|
210
|
+
): Promise<ITransmitter | undefined>;
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
export abstract class BaseListenerManager {
|
|
@@ -220,21 +227,39 @@ export abstract class BaseListenerManager {
|
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
abstract init(): Promise<void>;
|
|
230
|
+
|
|
223
231
|
abstract addListener(listener: Listener): Promise<ITransmitter>;
|
|
224
232
|
abstract removeListener(
|
|
225
233
|
driveId: string,
|
|
226
234
|
listenerId: string
|
|
227
235
|
): Promise<boolean>;
|
|
236
|
+
abstract getListener(
|
|
237
|
+
driveId: string,
|
|
238
|
+
listenerId: string
|
|
239
|
+
): Promise<ListenerState | undefined>;
|
|
240
|
+
|
|
228
241
|
abstract getTransmitter(
|
|
229
242
|
driveId: string,
|
|
230
243
|
listenerId: string
|
|
231
244
|
): Promise<ITransmitter | undefined>;
|
|
245
|
+
|
|
246
|
+
abstract getStrands(
|
|
247
|
+
listenerId: string,
|
|
248
|
+
since?: string
|
|
249
|
+
): Promise<StrandUpdate[]>;
|
|
250
|
+
|
|
232
251
|
abstract updateSynchronizationRevision(
|
|
233
252
|
driveId: string,
|
|
234
253
|
syncId: string,
|
|
235
254
|
syncRev: number,
|
|
236
|
-
lastUpdated: string
|
|
237
|
-
|
|
255
|
+
lastUpdated: string,
|
|
256
|
+
willUpdate?: (listeners: Listener[]) => void,
|
|
257
|
+
onError?: (
|
|
258
|
+
error: Error,
|
|
259
|
+
driveId: string,
|
|
260
|
+
listener: ListenerState
|
|
261
|
+
) => void
|
|
262
|
+
): Promise<ListenerUpdate[]>;
|
|
238
263
|
|
|
239
264
|
abstract updateListenerRevision(
|
|
240
265
|
listenerId: string,
|
package/src/storage/prisma.ts
CHANGED
|
@@ -58,53 +58,33 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
58
58
|
drive: string,
|
|
59
59
|
id: string,
|
|
60
60
|
operations: Operation[],
|
|
61
|
-
header: DocumentHeader
|
|
61
|
+
header: DocumentHeader,
|
|
62
|
+
updatedOperations: Operation[] = []
|
|
62
63
|
): Promise<void> {
|
|
63
64
|
const document = await this.getDocument(drive, id);
|
|
64
65
|
if (!document) {
|
|
65
66
|
throw new Error(`Document with id ${id} not found`);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
const mergedOperations = [...operations, ...updatedOperations].sort(
|
|
70
|
+
(a, b) => a.index - b.index
|
|
71
|
+
);
|
|
72
|
+
|
|
68
73
|
try {
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
documentId: id,
|
|
84
|
-
hash: op.hash,
|
|
85
|
-
index: op.index,
|
|
86
|
-
input: op.input as Prisma.InputJsonObject,
|
|
87
|
-
timestamp: op.timestamp,
|
|
88
|
-
type: op.type,
|
|
89
|
-
scope: op.scope,
|
|
90
|
-
branch: 'main',
|
|
91
|
-
skip: op.skip
|
|
92
|
-
},
|
|
93
|
-
update: {
|
|
94
|
-
driveId: drive,
|
|
95
|
-
documentId: id,
|
|
96
|
-
hash: op.hash,
|
|
97
|
-
index: op.index,
|
|
98
|
-
input: op.input as Prisma.InputJsonObject,
|
|
99
|
-
timestamp: op.timestamp,
|
|
100
|
-
type: op.type,
|
|
101
|
-
scope: op.scope,
|
|
102
|
-
branch: 'main',
|
|
103
|
-
skip: op.skip
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
})
|
|
107
|
-
);
|
|
74
|
+
await this.db.operation.createMany({
|
|
75
|
+
data: mergedOperations.map(op => ({
|
|
76
|
+
driveId: drive,
|
|
77
|
+
documentId: id,
|
|
78
|
+
hash: op.hash,
|
|
79
|
+
index: op.index,
|
|
80
|
+
input: op.input as Prisma.InputJsonObject,
|
|
81
|
+
timestamp: op.timestamp,
|
|
82
|
+
type: op.type,
|
|
83
|
+
scope: op.scope,
|
|
84
|
+
branch: 'main',
|
|
85
|
+
skip: op.skip
|
|
86
|
+
}))
|
|
87
|
+
});
|
|
108
88
|
|
|
109
89
|
await this.db.document.updateMany({
|
|
110
90
|
where: {
|
|
@@ -119,28 +99,6 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
119
99
|
} catch (e) {
|
|
120
100
|
console.log(e);
|
|
121
101
|
}
|
|
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
102
|
}
|
|
145
103
|
|
|
146
104
|
async getDocuments(drive: string) {
|
|
@@ -235,48 +193,21 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
235
193
|
}
|
|
236
194
|
|
|
237
195
|
async deleteDocument(drive: string, id: string) {
|
|
238
|
-
await this.db.attachment.deleteMany({
|
|
239
|
-
where: {
|
|
240
|
-
driveId: drive,
|
|
241
|
-
documentId: id
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
await this.db.operation.deleteMany({
|
|
246
|
-
where: {
|
|
247
|
-
driveId: drive,
|
|
248
|
-
documentId: id
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
196
|
await this.db.document.delete({
|
|
253
197
|
where: {
|
|
254
198
|
id_driveId: {
|
|
255
199
|
driveId: drive,
|
|
256
200
|
id: id
|
|
257
201
|
}
|
|
202
|
+
},
|
|
203
|
+
include: {
|
|
204
|
+
operations: {
|
|
205
|
+
include: {
|
|
206
|
+
attachments: true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
258
209
|
}
|
|
259
210
|
});
|
|
260
|
-
|
|
261
|
-
if (drive === 'drives') {
|
|
262
|
-
await this.db.attachment.deleteMany({
|
|
263
|
-
where: {
|
|
264
|
-
driveId: id
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
await this.db.operation.deleteMany({
|
|
269
|
-
where: {
|
|
270
|
-
driveId: id
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
await this.db.document.deleteMany({
|
|
275
|
-
where: {
|
|
276
|
-
driveId: id
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
211
|
}
|
|
281
212
|
|
|
282
213
|
async getDrives() {
|
|
@@ -293,6 +224,12 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
293
224
|
}
|
|
294
225
|
|
|
295
226
|
async deleteDrive(id: string) {
|
|
227
|
+
const docs = await this.getDocuments(id);
|
|
228
|
+
await Promise.all(
|
|
229
|
+
docs.map(async doc => {
|
|
230
|
+
return this.deleteDocument(id, doc);
|
|
231
|
+
})
|
|
232
|
+
);
|
|
296
233
|
await this.deleteDocument('drives', id);
|
|
297
234
|
}
|
|
298
235
|
}
|
package/src/utils/index.ts
CHANGED