document-drive 0.0.29 → 0.0.30

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.29",
3
+ "version": "0.0.30",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@prisma/client": "5.8.1",
32
- "document-model": "^1.0.28",
32
+ "document-model": "^1.0.29",
33
33
  "document-model-libs": "^1.1.44",
34
34
  "localforage": "^1.10.0",
35
35
  "sequelize": "^6.35.2",
@@ -39,6 +39,7 @@
39
39
  "graphql": "^16.8.1",
40
40
  "graphql-request": "^6.1.0",
41
41
  "json-stringify-deterministic": "^1.0.12",
42
+ "nanoevents": "^9.0.0",
42
43
  "sanitize-filename": "^1.6.3"
43
44
  },
44
45
  "devDependencies": {
@@ -48,7 +49,7 @@
48
49
  "@typescript-eslint/eslint-plugin": "^6.18.1",
49
50
  "@typescript-eslint/parser": "^6.18.1",
50
51
  "@vitest/coverage-v8": "^0.34.6",
51
- "document-model": "^1.0.28",
52
+ "document-model": "^1.0.29",
52
53
  "document-model-libs": "^1.1.44",
53
54
  "eslint": "^8.56.0",
54
55
  "eslint-config-prettier": "^9.1.0",
@@ -3,19 +3,20 @@ import {
3
3
  DocumentDriveDocument,
4
4
  DocumentDriveState,
5
5
  FileNode,
6
- Trigger,
7
6
  isFileNode,
7
+ Trigger,
8
8
  utils
9
9
  } from 'document-model-libs/document-drive';
10
10
  import {
11
11
  Action,
12
12
  BaseAction,
13
+ utils as baseUtils,
13
14
  Document,
14
15
  DocumentModel,
15
16
  Operation,
16
- OperationScope,
17
- utils as baseUtils
17
+ OperationScope
18
18
  } from 'document-model/document';
19
+ import { createNanoEvents, Unsubscribe } from 'nanoevents';
19
20
  import { MemoryStorage } from '../storage/memory';
20
21
  import type { DocumentStorage, IDriveStorage } from '../storage/types';
21
22
  import { generateUUID, isDocumentDrive, isNoopUpdate } from '../utils';
@@ -26,7 +27,9 @@ import { PullResponderTransmitter } from './listener/transmitter';
26
27
  import type { ITransmitter } from './listener/transmitter/types';
27
28
  import {
28
29
  BaseDocumentDriveServer,
30
+ DriveEvents,
29
31
  IOperationResult,
32
+ ListenerState,
30
33
  RemoteDriveOptions,
31
34
  StrandUpdate,
32
35
  SyncStatus,
@@ -43,6 +46,7 @@ export type * from './types';
43
46
  export const PULL_DRIVE_INTERVAL = 5000;
44
47
 
45
48
  export class DocumentDriveServer extends BaseDocumentDriveServer {
49
+ private emitter = createNanoEvents<DriveEvents>();
46
50
  private documentModels: DocumentModel[];
47
51
  private storage: IDriveStorage;
48
52
  private listenerStateManager: ListenerManager;
@@ -62,6 +66,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
62
66
  this.storage = storage;
63
67
  }
64
68
 
69
+ private updateSyncStatus(
70
+ driveId: string,
71
+ status: SyncStatus,
72
+ error?: Error
73
+ ) {
74
+ this.syncStatus.set(driveId, status);
75
+ this.emit('syncStatus', driveId, status, error);
76
+ }
77
+
65
78
  private async saveStrand(strand: StrandUpdate) {
66
79
  const operations: Operation[] = strand.operations.map(
67
80
  ({ index, type, hash, input, skip, timestamp }) => ({
@@ -86,10 +99,27 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
86
99
  strand.documentId,
87
100
  operations
88
101
  ));
89
- this.syncStatus.set(strand.driveId, result.status);
102
+
103
+ this.updateSyncStatus(strand.driveId, result.status);
104
+ this.emit('strandUpdate', strand);
90
105
  return result;
91
106
  }
92
107
 
108
+ private handleListenerError(
109
+ error: Error,
110
+ driveId: string,
111
+ listener: ListenerState
112
+ ) {
113
+ console.error(
114
+ `Listener ${listener.listener.label ?? listener.listener.listenerId} error: ${error.message}`
115
+ );
116
+ this.updateSyncStatus(
117
+ driveId,
118
+ error instanceof OperationError ? error.status : 'ERROR',
119
+ error
120
+ );
121
+ }
122
+
93
123
  private shouldSyncRemoteDrive(drive: DocumentDriveDocument) {
94
124
  return (
95
125
  drive.state.local.availableOffline &&
@@ -108,7 +138,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
108
138
 
109
139
  if (!driveTriggers) {
110
140
  driveTriggers = new Map();
111
- this.syncStatus.set(driveId, 'SYNCING');
141
+ this.updateSyncStatus(driveId, 'SYNCING');
112
142
  }
113
143
 
114
144
  if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
@@ -117,7 +147,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
117
147
  trigger,
118
148
  this.saveStrand.bind(this),
119
149
  error => {
120
- this.syncStatus.set(
150
+ this.updateSyncStatus(
121
151
  driveId,
122
152
  error instanceof OperationError
123
153
  ? error.status
@@ -265,7 +295,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
265
295
  (filter.since !== undefined &&
266
296
  filter.since <= operation.timestamp) ||
267
297
  (filter.fromRevision !== undefined &&
268
- operation.index >= filter.fromRevision)
298
+ operation.index > filter.fromRevision)
269
299
  );
270
300
 
271
301
  return filteredOperations.map(operation => ({
@@ -456,6 +486,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
456
486
  }
457
487
 
458
488
  async deleteDocument(driveId: string, id: string) {
489
+ try {
490
+ const syncUnits = await this.getSynchronizationUnits(driveId, [id]);
491
+ this.listenerStateManager.removeSyncUnits(syncUnits);
492
+ } catch {
493
+ /* empty */
494
+ }
459
495
  return this.storage.deleteDocument(driveId, id);
460
496
  }
461
497
 
@@ -664,10 +700,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
664
700
  );
665
701
  }
666
702
 
667
- const results = await Promise.all(
668
- operationSignals.map(handler => handler())
669
- );
670
- signalResults.push(...results);
703
+ for (const signalHandler of operationSignals) {
704
+ const result = await signalHandler();
705
+ signalResults.push(result);
706
+ }
671
707
 
672
708
  return {
673
709
  document: newDocument,
@@ -749,10 +785,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
749
785
  drive,
750
786
  syncUnit.syncId,
751
787
  syncUnit.revision,
752
- syncUnit.lastUpdated
788
+ syncUnit.lastUpdated,
789
+ this.handleListenerError.bind(this)
753
790
  )
754
791
  .catch(error => {
755
- console.error('Error updating sync revision', error);
792
+ console.error(
793
+ 'Non handled error updating sync revision',
794
+ error
795
+ );
796
+ this.updateSyncStatus(drive, 'ERROR', error as Error);
756
797
  });
757
798
  }
758
799
 
@@ -875,10 +916,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
875
916
  drive,
876
917
  '0',
877
918
  lastOperation.index,
878
- lastOperation.timestamp
919
+ lastOperation.timestamp,
920
+ this.handleListenerError.bind(this)
879
921
  )
880
922
  .catch(error => {
881
- console.error('Error updating sync revision', error);
923
+ console.error(
924
+ 'Non handled error updating sync revision',
925
+ error
926
+ );
927
+ this.updateSyncStatus(drive, 'ERROR', error as Error);
882
928
  });
883
929
  }
884
930
 
@@ -935,4 +981,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
935
981
  }
936
982
  return status;
937
983
  }
984
+
985
+ on<K extends keyof DriveEvents>(event: K, cb: DriveEvents[K]): Unsubscribe {
986
+ return this.emitter.on(event, cb);
987
+ }
988
+
989
+ protected emit<K extends keyof DriveEvents>(
990
+ event: K,
991
+ ...args: Parameters<DriveEvents[K]>
992
+ ): void {
993
+ return this.emitter.emit(event, ...args);
994
+ }
938
995
  }
@@ -9,6 +9,7 @@ import {
9
9
  ErrorStatus,
10
10
  Listener,
11
11
  ListenerState,
12
+ OperationUpdate,
12
13
  StrandUpdate,
13
14
  SynchronizationUnit
14
15
  } from '../types';
@@ -120,7 +121,12 @@ export class ListenerManager extends BaseListenerManager {
120
121
  driveId: string,
121
122
  syncId: string,
122
123
  syncRev: number,
123
- lastUpdated: string
124
+ lastUpdated: string,
125
+ onError?: (
126
+ error: Error,
127
+ driveId: string,
128
+ listener: ListenerState
129
+ ) => void
124
130
  ) {
125
131
  const drive = this.listenerState.get(driveId);
126
132
  if (!drive) {
@@ -148,7 +154,7 @@ export class ListenerManager extends BaseListenerManager {
148
154
  }
149
155
 
150
156
  if (newRevision) {
151
- return this.triggerUpdate();
157
+ return this.triggerUpdate(onError);
152
158
  }
153
159
  }
154
160
 
@@ -202,6 +208,19 @@ export class ListenerManager extends BaseListenerManager {
202
208
  }
203
209
  }
204
210
 
211
+ removeSyncUnits(syncUnits: SynchronizationUnit[]) {
212
+ for (const [driveId, drive] of this.listenerState) {
213
+ const syncIds = syncUnits
214
+ .filter(s => s.driveId === driveId)
215
+ .map(s => s.syncId);
216
+ for (const [, listenerState] of drive) {
217
+ listenerState.syncUnits = listenerState.syncUnits.filter(
218
+ s => !syncIds.includes(s.syncId)
219
+ );
220
+ }
221
+ }
222
+ }
223
+
205
224
  async updateListenerRevision(
206
225
  listenerId: string,
207
226
  driveId: string,
@@ -225,7 +244,13 @@ export class ListenerManager extends BaseListenerManager {
225
244
  }
226
245
  }
227
246
 
228
- async triggerUpdate() {
247
+ async triggerUpdate(
248
+ onError?: (
249
+ error: Error,
250
+ driveId: string,
251
+ listener: ListenerState
252
+ ) => void
253
+ ) {
229
254
  for (const [driveId, drive] of this.listenerState) {
230
255
  for (const [id, listener] of drive) {
231
256
  const transmitter = await this.getTransmitter(driveId, id);
@@ -248,13 +273,19 @@ export class ListenerManager extends BaseListenerManager {
248
273
  continue;
249
274
  }
250
275
 
251
- const opData = await this.drive.getOperationData(
252
- driveId,
253
- syncId,
254
- {
255
- fromRevision: listenerRev
256
- }
257
- );
276
+ const opData: OperationUpdate[] = [];
277
+ try {
278
+ const data = await this.drive.getOperationData(
279
+ driveId,
280
+ syncId,
281
+ {
282
+ fromRevision: listenerRev
283
+ }
284
+ );
285
+ opData.push(...data);
286
+ } catch (e) {
287
+ console.error(e);
288
+ }
258
289
 
259
290
  if (!opData.length) {
260
291
  continue;
@@ -309,9 +340,9 @@ export class ListenerManager extends BaseListenerManager {
309
340
  listener.listenerStatus = 'SUCCESS';
310
341
  } catch (e) {
311
342
  // TODO: Handle error based on listener params (blocking, retry, etc)
343
+ onError?.(e as Error, driveId, listener);
312
344
  listener.listenerStatus =
313
345
  e instanceof OperationError ? e.status : 'ERROR';
314
- throw e;
315
346
  }
316
347
  }
317
348
  }
@@ -15,6 +15,7 @@ import type {
15
15
  Signal,
16
16
  State
17
17
  } from 'document-model/document';
18
+ import { Unsubscribe } from 'nanoevents';
18
19
  import { OperationError } from './error';
19
20
  import { ITransmitter } from './listener/transmitter/types';
20
21
 
@@ -114,6 +115,11 @@ export type StrandUpdate = {
114
115
 
115
116
  export type SyncStatus = 'SYNCING' | UpdateStatus;
116
117
 
118
+ export interface DriveEvents {
119
+ syncStatus: (driveId: string, status: SyncStatus, error?: Error) => void;
120
+ strandUpdate: (update: StrandUpdate) => void;
121
+ }
122
+
117
123
  export abstract class BaseDocumentDriveServer {
118
124
  /** Public methods **/
119
125
  abstract getDrives(): Promise<string[]>;
@@ -132,12 +138,12 @@ export abstract class BaseDocumentDriveServer {
132
138
  drive: string,
133
139
  id: string,
134
140
  operation: Operation
135
- ): Promise<IOperationResult<Document>>;
141
+ ): Promise<IOperationResult>;
136
142
  abstract addOperations(
137
143
  drive: string,
138
144
  id: string,
139
145
  operations: Operation[]
140
- ): Promise<IOperationResult<Document>>;
146
+ ): Promise<IOperationResult>;
141
147
 
142
148
  abstract addDriveOperation(
143
149
  drive: string,
@@ -183,12 +189,23 @@ export abstract class BaseDocumentDriveServer {
183
189
  driveId: string,
184
190
  listenerId: string
185
191
  ): Promise<ITransmitter | undefined>;
192
+
193
+ /** Event methods **/
194
+ protected abstract emit<K extends keyof DriveEvents>(
195
+ this: this,
196
+ event: K,
197
+ ...args: Parameters<DriveEvents[K]>
198
+ ): void;
199
+ abstract on<K extends keyof DriveEvents>(
200
+ this: this,
201
+ event: K,
202
+ cb: DriveEvents[K]
203
+ ): Unsubscribe;
186
204
  }
187
205
 
188
206
  export abstract class BaseListenerManager {
189
207
  protected drive: BaseDocumentDriveServer;
190
- protected listenerState: Map<string, Map<string, ListenerState>> =
191
- new Map();
208
+ protected listenerState = new Map<string, Map<string, ListenerState>>();
192
209
  protected transmitters: Record<
193
210
  DocumentDriveState['id'],
194
211
  Record<Listener['listenerId'], ITransmitter>
@@ -196,7 +213,7 @@ export abstract class BaseListenerManager {
196
213
 
197
214
  constructor(
198
215
  drive: BaseDocumentDriveServer,
199
- listenerState: Map<string, Map<string, ListenerState>> = new Map()
216
+ listenerState = new Map<string, Map<string, ListenerState>>()
200
217
  ) {
201
218
  this.drive = drive;
202
219
  this.listenerState = listenerState;