document-drive 1.0.0-alpha.1 → 1.0.0-alpha.3
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": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.3",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
|
55
55
|
"@typescript-eslint/parser": "^6.18.1",
|
|
56
56
|
"@vitest/coverage-v8": "^0.34.6",
|
|
57
|
-
"document-model": "^1.0.
|
|
58
|
-
"document-model-libs": "^1.1.
|
|
57
|
+
"document-model": "^1.0.30",
|
|
58
|
+
"document-model-libs": "^1.1.51",
|
|
59
59
|
"eslint": "^8.56.0",
|
|
60
60
|
"eslint-config-prettier": "^9.1.0",
|
|
61
61
|
"fake-indexeddb": "^5.0.1",
|
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
|
}
|
|
@@ -142,7 +147,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
142
147
|
|
|
143
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',
|
|
@@ -986,6 +1002,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
986
1002
|
return this.listenerStateManager.getTransmitter(driveId, listenerId);
|
|
987
1003
|
}
|
|
988
1004
|
|
|
1005
|
+
getListener(
|
|
1006
|
+
driveId: string,
|
|
1007
|
+
listenerId: string
|
|
1008
|
+
): Promise<ListenerState | undefined> {
|
|
1009
|
+
return this.listenerStateManager.getListener(driveId, listenerId);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
989
1012
|
getSyncStatus(drive: string): SyncStatus {
|
|
990
1013
|
const status = this.syncStatus.get(drive);
|
|
991
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');
|
|
@@ -419,11 +419,53 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
getListener(driveId: string, listenerId: string): ListenerState {
|
|
422
|
+
getListener(driveId: string, listenerId: string): Promise<ListenerState> {
|
|
423
423
|
const drive = this.listenerState.get(driveId);
|
|
424
424
|
if (!drive) throw new Error('Drive not found');
|
|
425
425
|
const listener = drive.get(listenerId);
|
|
426
426
|
if (!listener) throw new Error('Listener not found');
|
|
427
|
-
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;
|
|
428
470
|
}
|
|
429
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) {
|
|
@@ -223,6 +194,7 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
223
194
|
trigger: PullResponderTrigger,
|
|
224
195
|
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
225
196
|
onError: (error: Error) => void,
|
|
197
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
226
198
|
onAcknowledge?: (success: boolean) => void
|
|
227
199
|
) {
|
|
228
200
|
try {
|
|
@@ -239,7 +211,7 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
239
211
|
return;
|
|
240
212
|
}
|
|
241
213
|
|
|
242
|
-
const listenerRevisions:
|
|
214
|
+
const listenerRevisions: ListenerRevisionWithError[] = [];
|
|
243
215
|
|
|
244
216
|
for (const strand of strands) {
|
|
245
217
|
const operations: Operation[] = strand.operations.map(
|
|
@@ -256,7 +228,6 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
256
228
|
);
|
|
257
229
|
|
|
258
230
|
let error: Error | undefined = undefined;
|
|
259
|
-
|
|
260
231
|
try {
|
|
261
232
|
const result = await onStrandUpdate(strand);
|
|
262
233
|
if (result.error) {
|
|
@@ -277,20 +248,26 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
277
248
|
? error instanceof OperationError
|
|
278
249
|
? error.status
|
|
279
250
|
: 'ERROR'
|
|
280
|
-
: 'SUCCESS'
|
|
251
|
+
: 'SUCCESS',
|
|
252
|
+
error
|
|
281
253
|
});
|
|
282
254
|
|
|
283
255
|
// TODO: Should try to parse remaining strands?
|
|
284
|
-
if (error) {
|
|
285
|
-
|
|
286
|
-
}
|
|
256
|
+
// if (error) {
|
|
257
|
+
// break;
|
|
258
|
+
// }
|
|
287
259
|
}
|
|
288
260
|
|
|
261
|
+
onRevisions?.(listenerRevisions);
|
|
262
|
+
|
|
289
263
|
await PullResponderTransmitter.acknowledgeStrands(
|
|
290
264
|
driveId,
|
|
291
265
|
url,
|
|
292
266
|
listenerId,
|
|
293
|
-
listenerRevisions
|
|
267
|
+
listenerRevisions.map(revision => {
|
|
268
|
+
const { error, ...rest } = revision;
|
|
269
|
+
return rest;
|
|
270
|
+
})
|
|
294
271
|
)
|
|
295
272
|
.then(result => onAcknowledge?.(result))
|
|
296
273
|
.catch(error => console.error('ACK error', error));
|
|
@@ -304,8 +281,9 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
304
281
|
trigger: PullResponderTrigger,
|
|
305
282
|
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
306
283
|
onError: (error: Error) => void,
|
|
284
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
307
285
|
onAcknowledge?: (success: boolean) => void
|
|
308
|
-
):
|
|
286
|
+
): CancelPullLoop {
|
|
309
287
|
const { interval } = trigger.data;
|
|
310
288
|
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
311
289
|
if (interval) {
|
|
@@ -319,26 +297,36 @@ export class PullResponderTransmitter implements ITransmitter {
|
|
|
319
297
|
}
|
|
320
298
|
}
|
|
321
299
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
trigger,
|
|
325
|
-
onStrandUpdate,
|
|
326
|
-
onError,
|
|
327
|
-
onAcknowledge
|
|
328
|
-
);
|
|
300
|
+
let isCancelled = false;
|
|
301
|
+
let timeout: number | undefined;
|
|
329
302
|
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
this.executePull(
|
|
303
|
+
const executeLoop = async () => {
|
|
304
|
+
while (!isCancelled) {
|
|
305
|
+
await this.executePull(
|
|
333
306
|
driveId,
|
|
334
307
|
trigger,
|
|
335
308
|
onStrandUpdate,
|
|
336
309
|
onError,
|
|
310
|
+
onRevisions,
|
|
337
311
|
onAcknowledge
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
312
|
+
);
|
|
313
|
+
await new Promise(resolve => {
|
|
314
|
+
timeout = setTimeout(
|
|
315
|
+
resolve,
|
|
316
|
+
loopInterval
|
|
317
|
+
) as unknown as number;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
executeLoop().catch(console.error);
|
|
323
|
+
|
|
324
|
+
return () => {
|
|
325
|
+
isCancelled = true;
|
|
326
|
+
if (timeout !== undefined) {
|
|
327
|
+
clearTimeout(timeout);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
342
330
|
}
|
|
343
331
|
|
|
344
332
|
static isPullResponderTrigger(
|
package/src/server/types.ts
CHANGED
|
@@ -93,6 +93,8 @@ export type ListenerRevision = {
|
|
|
93
93
|
revision: number;
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
export type ListenerRevisionWithError = ListenerRevision & { error?: Error };
|
|
97
|
+
|
|
96
98
|
export type ListenerUpdate = {
|
|
97
99
|
listenerId: string;
|
|
98
100
|
listenerRevisions: ListenerRevision[];
|
|
@@ -190,11 +192,6 @@ export abstract class BaseDocumentDriveServer {
|
|
|
190
192
|
): Promise<Document>;
|
|
191
193
|
protected abstract deleteDocument(drive: string, id: string): Promise<void>;
|
|
192
194
|
|
|
193
|
-
abstract getTransmitter(
|
|
194
|
-
driveId: string,
|
|
195
|
-
listenerId: string
|
|
196
|
-
): Promise<ITransmitter | undefined>;
|
|
197
|
-
|
|
198
195
|
/** Event methods **/
|
|
199
196
|
protected abstract emit<K extends keyof DriveEvents>(
|
|
200
197
|
this: this,
|
|
@@ -206,6 +203,11 @@ export abstract class BaseDocumentDriveServer {
|
|
|
206
203
|
event: K,
|
|
207
204
|
cb: DriveEvents[K]
|
|
208
205
|
): Unsubscribe;
|
|
206
|
+
|
|
207
|
+
abstract getTransmitter(
|
|
208
|
+
driveId: string,
|
|
209
|
+
listenerId: string
|
|
210
|
+
): Promise<ITransmitter | undefined>;
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
export abstract class BaseListenerManager {
|
|
@@ -225,15 +227,27 @@ export abstract class BaseListenerManager {
|
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
abstract init(): Promise<void>;
|
|
230
|
+
|
|
228
231
|
abstract addListener(listener: Listener): Promise<ITransmitter>;
|
|
229
232
|
abstract removeListener(
|
|
230
233
|
driveId: string,
|
|
231
234
|
listenerId: string
|
|
232
235
|
): Promise<boolean>;
|
|
236
|
+
abstract getListener(
|
|
237
|
+
driveId: string,
|
|
238
|
+
listenerId: string
|
|
239
|
+
): Promise<ListenerState | undefined>;
|
|
240
|
+
|
|
233
241
|
abstract getTransmitter(
|
|
234
242
|
driveId: string,
|
|
235
243
|
listenerId: string
|
|
236
244
|
): Promise<ITransmitter | undefined>;
|
|
245
|
+
|
|
246
|
+
abstract getStrands(
|
|
247
|
+
listenerId: string,
|
|
248
|
+
since?: string
|
|
249
|
+
): Promise<StrandUpdate[]>;
|
|
250
|
+
|
|
237
251
|
abstract updateSynchronizationRevision(
|
|
238
252
|
driveId: string,
|
|
239
253
|
syncId: string,
|