document-drive 1.0.0-alpha.1 → 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": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -55,7 +55,7 @@
55
55
  "@typescript-eslint/parser": "^6.18.1",
56
56
  "@vitest/coverage-v8": "^0.34.6",
57
57
  "document-model": "^1.0.29",
58
- "document-model-libs": "^1.1.44",
58
+ "document-model-libs": "^1.1.48",
59
59
  "eslint": "^8.56.0",
60
60
  "eslint-config-prettier": "^9.1.0",
61
61
  "fake-indexeddb": "^5.0.1",
@@ -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 { PullResponderTransmitter } from './listener/transmitter';
27
- import type { ITransmitter } from './listener/transmitter/types';
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'], number>
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
- this.updateSyncStatus(strand.driveId, result.status);
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 intervalId = PullResponderTransmitter.setupPull(
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
- acknowledgeSuccess => {}
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, intervalId);
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(clearInterval);
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.find(
611
- existingOperation => existingOperation.index === op.index
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 class PullResponderTransmitter implements ITransmitter {
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
- async getStrands(
53
- listenerId: string,
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: ListenerRevision[] = [];
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
- break;
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
- ): number {
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
- this.executePull(
323
- driveId,
324
- trigger,
325
- onStrandUpdate,
326
- onError,
327
- onAcknowledge
328
- );
300
+ let isCancelled = false;
301
+ let timeout: number | undefined;
329
302
 
330
- const timeout = setInterval(
331
- async () =>
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
- loopInterval
340
- );
341
- return timeout as unknown as number;
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(
@@ -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,