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.30",
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.44",
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",
@@ -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
  }
@@ -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 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',
@@ -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
- let newRevision = false;
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
- newRevision = true;
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 (newRevision) {
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 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) {
@@ -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
- ): number {
228
- const { url, listenerId, interval } = trigger.data;
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
- const timeout = setInterval(async () => {
242
- try {
243
- const strands = await PullResponderTransmitter.pullStrands(
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
- url,
246
- listenerId
247
- // since ?
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
- // 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
- });
322
+ executeLoop().catch(console.error);
295
323
 
296
- // TODO: Should try to parse remaining strands?
297
- if (error) {
298
- break;
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
- }, loopInterval);
314
- return timeout as unknown as number;
329
+ };
315
330
  }
316
331
 
317
332
  static isPullResponderTrigger(
@@ -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
- ): Promise<void>;
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,
@@ -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) {