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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "0.0.30",
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.29",
53
- "document-model-libs": "^1.1.44",
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",
@@ -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 { generateUUID, isDocumentDrive, isNoopUpdate } from '../utils';
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 { PullResponderTransmitter } from './listener/transmitter';
27
- import type { ITransmitter } from './listener/transmitter/types';
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'], number>
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
- this.updateSyncStatus(strand.driveId, result.status);
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 intervalId = PullResponderTransmitter.setupPull(
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
- acknowledgeSuccess => {}
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, intervalId);
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(clearInterval);
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 !== undefined &&
296
- filter.since <= operation.timestamp) ||
297
- (filter.fromRevision !== undefined &&
298
- operation.index > filter.fromRevision)
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.find(
611
- existingOperation => existingOperation.index === op.index
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
- 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,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
- ): number {
228
- const { url, listenerId, interval } = trigger.data;
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
- const timeout = setInterval(async () => {
242
- try {
243
- const strands = await PullResponderTransmitter.pullStrands(
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
- url,
246
- listenerId
247
- // since ?
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
- // 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
- });
323
+ executeLoop().catch(console.error);
295
324
 
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);
325
+ return () => {
326
+ isCancelled = true;
327
+ if (timeout !== undefined) {
328
+ clearTimeout(timeout);
312
329
  }
313
- }, loopInterval);
314
- return timeout as unknown as number;
330
+ };
315
331
  }
316
332
 
317
333
  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,
@@ -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 Promise.all(
70
- operations.map(async op => {
71
- return this.db.operation.upsert({
72
- where: {
73
- driveId_documentId_scope_branch_index: {
74
- driveId: drive,
75
- documentId: id,
76
- scope: op.scope,
77
- branch: 'main',
78
- index: op.index
79
- }
80
- },
81
- create: {
82
- driveId: drive,
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
  }
@@ -75,3 +75,8 @@ export function isNoopUpdate(
75
75
  isSkipOpGreaterThanLatestOp
76
76
  );
77
77
  }
78
+
79
+ // return true if dateA is before dateB
80
+ export function isBefore(dateA: Date | string, dateB: Date | string) {
81
+ return new Date(dateA) < new Date(dateB);
82
+ }