document-drive 0.0.30 → 1.0.0-alpha.2
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
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
57
|
"document-model": "^1.0.29",
|
|
53
|
-
"document-model-libs": "^1.1.
|
|
58
|
+
"document-model-libs": "^1.1.48",
|
|
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
|
@@ -23,8 +23,11 @@ import { generateUUID, isDocumentDrive, isNoopUpdate } from '../utils';
|
|
|
23
23
|
import { requestPublicDrive } from '../utils/graphql';
|
|
24
24
|
import { OperationError } from './error';
|
|
25
25
|
import { ListenerManager } from './listener/manager';
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
CancelPullLoop,
|
|
28
|
+
ITransmitter,
|
|
29
|
+
PullResponderTransmitter
|
|
30
|
+
} from './listener/transmitter';
|
|
28
31
|
import {
|
|
29
32
|
BaseDocumentDriveServer,
|
|
30
33
|
DriveEvents,
|
|
@@ -52,7 +55,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
52
55
|
private listenerStateManager: ListenerManager;
|
|
53
56
|
private triggerMap = new Map<
|
|
54
57
|
DocumentDriveState['id'],
|
|
55
|
-
Map<Trigger['id'],
|
|
58
|
+
Map<Trigger['id'], CancelPullLoop>
|
|
56
59
|
>();
|
|
57
60
|
private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
|
|
58
61
|
|
|
@@ -100,7 +103,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
100
103
|
operations
|
|
101
104
|
));
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
if (result.status === 'ERROR') {
|
|
107
|
+
this.updateSyncStatus(strand.driveId, result.status, result.error);
|
|
108
|
+
}
|
|
104
109
|
this.emit('strandUpdate', strand);
|
|
105
110
|
return result;
|
|
106
111
|
}
|
|
@@ -138,11 +143,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
138
143
|
|
|
139
144
|
if (!driveTriggers) {
|
|
140
145
|
driveTriggers = new Map();
|
|
141
|
-
this.updateSyncStatus(driveId, 'SYNCING');
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
this.updateSyncStatus(driveId, 'SYNCING');
|
|
144
149
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
145
|
-
const
|
|
150
|
+
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
146
151
|
driveId,
|
|
147
152
|
trigger,
|
|
148
153
|
this.saveStrand.bind(this),
|
|
@@ -151,12 +156,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
151
156
|
driveId,
|
|
152
157
|
error instanceof OperationError
|
|
153
158
|
? error.status
|
|
154
|
-
: 'ERROR'
|
|
159
|
+
: 'ERROR',
|
|
160
|
+
error
|
|
155
161
|
);
|
|
156
162
|
},
|
|
157
|
-
|
|
163
|
+
revisions => {
|
|
164
|
+
const errorRevision = revisions.find(
|
|
165
|
+
r => r.status !== 'SUCCESS'
|
|
166
|
+
);
|
|
167
|
+
if (!errorRevision) {
|
|
168
|
+
this.updateSyncStatus(driveId, 'SUCCESS');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
158
171
|
);
|
|
159
|
-
driveTriggers.set(trigger.id,
|
|
172
|
+
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
160
173
|
this.triggerMap.set(driveId, driveTriggers);
|
|
161
174
|
}
|
|
162
175
|
}
|
|
@@ -164,7 +177,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
164
177
|
|
|
165
178
|
private async stopSyncRemoteDrive(driveId: string) {
|
|
166
179
|
const triggers = this.triggerMap.get(driveId);
|
|
167
|
-
triggers?.forEach(
|
|
180
|
+
triggers?.forEach(cancel => cancel());
|
|
168
181
|
return this.triggerMap.delete(driveId);
|
|
169
182
|
}
|
|
170
183
|
|
|
@@ -607,9 +620,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
607
620
|
);
|
|
608
621
|
continue;
|
|
609
622
|
} else if (op.index < nextIndex) {
|
|
610
|
-
const existingOperation = scopeOperations
|
|
611
|
-
|
|
612
|
-
|
|
623
|
+
const existingOperation = scopeOperations
|
|
624
|
+
.concat(pastOperations)
|
|
625
|
+
.find(
|
|
626
|
+
existingOperation =>
|
|
627
|
+
existingOperation.index === op.index
|
|
628
|
+
);
|
|
613
629
|
if (existingOperation && existingOperation.hash !== op.hash) {
|
|
614
630
|
error = new OperationError(
|
|
615
631
|
'CONFLICT',
|
|
@@ -786,8 +802,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
786
802
|
syncUnit.syncId,
|
|
787
803
|
syncUnit.revision,
|
|
788
804
|
syncUnit.lastUpdated,
|
|
805
|
+
() => this.updateSyncStatus(drive, 'SYNCING'),
|
|
789
806
|
this.handleListenerError.bind(this)
|
|
790
807
|
)
|
|
808
|
+
.then(
|
|
809
|
+
updates =>
|
|
810
|
+
updates.length &&
|
|
811
|
+
this.updateSyncStatus(drive, 'SUCCESS')
|
|
812
|
+
)
|
|
791
813
|
.catch(error => {
|
|
792
814
|
console.error(
|
|
793
815
|
'Non handled error updating sync revision',
|
|
@@ -917,8 +939,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
917
939
|
'0',
|
|
918
940
|
lastOperation.index,
|
|
919
941
|
lastOperation.timestamp,
|
|
942
|
+
() => this.updateSyncStatus(drive, 'SYNCING'),
|
|
920
943
|
this.handleListenerError.bind(this)
|
|
921
944
|
)
|
|
945
|
+
.then(
|
|
946
|
+
updates =>
|
|
947
|
+
updates.length &&
|
|
948
|
+
this.updateSyncStatus(drive, 'SUCCESS')
|
|
949
|
+
)
|
|
922
950
|
.catch(error => {
|
|
923
951
|
console.error(
|
|
924
952
|
'Non handled error updating sync revision',
|
|
@@ -974,6 +1002,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
974
1002
|
return this.listenerStateManager.getTransmitter(driveId, listenerId);
|
|
975
1003
|
}
|
|
976
1004
|
|
|
1005
|
+
getListener(
|
|
1006
|
+
driveId: string,
|
|
1007
|
+
listenerId: string
|
|
1008
|
+
): Promise<ListenerState | undefined> {
|
|
1009
|
+
return this.listenerStateManager.getListener(driveId, listenerId);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
977
1012
|
getSyncStatus(drive: string): SyncStatus {
|
|
978
1013
|
const status = this.syncStatus.get(drive);
|
|
979
1014
|
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,102 @@ 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
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const listenerRevisions: ListenerRevisionWithError[] = [];
|
|
215
|
+
|
|
216
|
+
for (const strand of strands) {
|
|
217
|
+
const operations: Operation[] = strand.operations.map(
|
|
218
|
+
({ index, type, hash, input, skip, timestamp }) => ({
|
|
219
|
+
index,
|
|
220
|
+
type,
|
|
221
|
+
hash,
|
|
222
|
+
input,
|
|
223
|
+
skip,
|
|
224
|
+
timestamp,
|
|
225
|
+
scope: strand.scope,
|
|
226
|
+
branch: strand.branch
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
let error: Error | undefined = undefined;
|
|
231
|
+
try {
|
|
232
|
+
const result = await onStrandUpdate(strand);
|
|
233
|
+
if (result.error) {
|
|
234
|
+
throw result.error;
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
error = e as Error;
|
|
238
|
+
onError(error);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
listenerRevisions.push({
|
|
242
|
+
branch: strand.branch,
|
|
243
|
+
documentId: strand.documentId || '',
|
|
244
|
+
driveId: strand.driveId,
|
|
245
|
+
revision: operations.pop()?.index ?? -1,
|
|
246
|
+
scope: strand.scope as OperationScope,
|
|
247
|
+
status: error
|
|
248
|
+
? error instanceof OperationError
|
|
249
|
+
? error.status
|
|
250
|
+
: 'ERROR'
|
|
251
|
+
: 'SUCCESS',
|
|
252
|
+
error
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// TODO: Should try to parse remaining strands?
|
|
256
|
+
// if (error) {
|
|
257
|
+
// break;
|
|
258
|
+
// }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
onRevisions?.(listenerRevisions);
|
|
262
|
+
|
|
263
|
+
await PullResponderTransmitter.acknowledgeStrands(
|
|
264
|
+
driveId,
|
|
265
|
+
url,
|
|
266
|
+
listenerId,
|
|
267
|
+
listenerRevisions.map(revision => {
|
|
268
|
+
const { error, ...rest } = revision;
|
|
269
|
+
return rest;
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
.then(result => onAcknowledge?.(result))
|
|
273
|
+
.catch(error => console.error('ACK error', error));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
onError(error as Error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
221
279
|
static setupPull(
|
|
222
280
|
driveId: string,
|
|
223
281
|
trigger: PullResponderTrigger,
|
|
224
282
|
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
225
283
|
onError: (error: Error) => void,
|
|
284
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
226
285
|
onAcknowledge?: (success: boolean) => void
|
|
227
|
-
):
|
|
228
|
-
const {
|
|
286
|
+
): CancelPullLoop {
|
|
287
|
+
const { interval } = trigger.data;
|
|
229
288
|
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
230
289
|
if (interval) {
|
|
231
290
|
try {
|
|
@@ -238,80 +297,36 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
238
297
|
}
|
|
239
298
|
}
|
|
240
299
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
300
|
+
let isCancelled = false;
|
|
301
|
+
let timeout: number | undefined;
|
|
302
|
+
|
|
303
|
+
const executeLoop = async () => {
|
|
304
|
+
while (!isCancelled) {
|
|
305
|
+
await this.executePull(
|
|
244
306
|
driveId,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
307
|
+
trigger,
|
|
308
|
+
onStrandUpdate,
|
|
309
|
+
onError,
|
|
310
|
+
onRevisions,
|
|
311
|
+
onAcknowledge
|
|
248
312
|
);
|
|
313
|
+
await new Promise(resolve => {
|
|
314
|
+
timeout = setTimeout(
|
|
315
|
+
resolve,
|
|
316
|
+
loopInterval
|
|
317
|
+
) as unknown as number;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
249
321
|
|
|
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
|
-
});
|
|
322
|
+
executeLoop().catch(console.error);
|
|
295
323
|
|
|
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);
|
|
324
|
+
return () => {
|
|
325
|
+
isCancelled = true;
|
|
326
|
+
if (timeout !== undefined) {
|
|
327
|
+
clearTimeout(timeout);
|
|
312
328
|
}
|
|
313
|
-
}
|
|
314
|
-
return timeout as unknown as number;
|
|
329
|
+
};
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
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
|
@@ -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) {
|