document-drive 1.0.0-alpha.87 → 1.0.0-alpha.89

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.87",
3
+ "version": "1.0.0-alpha.89",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -19,6 +19,7 @@
19
19
  "./queue/base": "./src/queue/base.ts",
20
20
  "./utils": "./src/utils/index.ts",
21
21
  "./utils/graphql": "./src/utils/graphql.ts",
22
+ "./utils/migrations": "./src/utils/migrations.ts",
22
23
  "./logger": "./src/utils/logger.ts"
23
24
  },
24
25
  "files": [
@@ -30,7 +31,7 @@
30
31
  "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
31
32
  "format": "prettier . --write",
32
33
  "release": "semantic-release",
33
- "test": "vitest run --coverage",
34
+ "test": "vitest run --coverage --exclude \"test/flaky/**\"",
34
35
  "test:watch": "vitest watch"
35
36
  },
36
37
  "peerDependencies": {
@@ -38,7 +39,7 @@
38
39
  "document-model-libs": "^1.57.0"
39
40
  },
40
41
  "optionalDependencies": {
41
- "@prisma/client": "5.17.0",
42
+ "@prisma/client": "^5.18.0",
42
43
  "localforage": "^1.10.0",
43
44
  "redis": "^4.6.15",
44
45
  "sequelize": "^6.37.3",
@@ -64,7 +65,7 @@
64
65
  "@types/uuid": "^9.0.8",
65
66
  "@typescript-eslint/eslint-plugin": "^6.21.0",
66
67
  "@typescript-eslint/parser": "^6.21.0",
67
- "@vitest/coverage-v8": "^1.6.0",
68
+ "@vitest/coverage-v8": "^2.0.5",
68
69
  "document-model": "^1.7.0",
69
70
  "document-model-libs": "^1.70.0",
70
71
  "eslint": "^8.57.0",
@@ -74,12 +75,12 @@
74
75
  "msw": "^2.3.1",
75
76
  "prettier": "^3.3.3",
76
77
  "prettier-plugin-organize-imports": "^3.2.4",
77
- "prisma": "^5.17.0",
78
+ "prisma": "^5.18.0",
78
79
  "semantic-release": "^23.1.1",
79
80
  "sequelize": "^6.37.2",
80
81
  "sqlite3": "^5.1.7",
81
82
  "typescript": "^5.5.3",
82
- "vitest": "^1.6.0"
83
+ "vitest": "^2.0.5"
83
84
  },
84
85
  "packageManager": "pnpm@9.1.4+sha256.30a1801ac4e723779efed13a21f4c39f9eb6c9fbb4ced101bce06b422593d7c9"
85
86
  }
@@ -33,3 +33,21 @@ export class MissingOperationError extends OperationError {
33
33
  super('MISSING', operation, `Missing operation on index ${index}`);
34
34
  }
35
35
  }
36
+
37
+ export class DriveAlreadyExistsError extends Error {
38
+ driveId: string;
39
+
40
+ constructor(driveId: string) {
41
+ super(`Drive already exists. ID: ${driveId}`);
42
+ this.driveId = driveId;
43
+ }
44
+ }
45
+
46
+ export class DriveNotFoundError extends Error {
47
+ driveId: string;
48
+
49
+ constructor(driveId: string) {
50
+ super(`Drive with id ${driveId} not found`);
51
+ this.driveId = driveId;
52
+ }
53
+ }
@@ -42,6 +42,7 @@ import type {
42
42
  IDriveStorage
43
43
  } from '../storage/types';
44
44
  import { generateUUID, isBefore, isDocumentDrive } from '../utils';
45
+ import { DefaultDrivesManager } from '../utils/default-drives-manager';
45
46
  import {
46
47
  attachBranch,
47
48
  garbageCollect,
@@ -54,7 +55,11 @@ import {
54
55
  } from '../utils/document-helpers';
55
56
  import { requestPublicDrive } from '../utils/graphql';
56
57
  import { logger } from '../utils/logger';
57
- import { ConflictOperationError, OperationError } from './error';
58
+ import {
59
+ ConflictOperationError,
60
+ DriveAlreadyExistsError,
61
+ OperationError
62
+ } from './error';
58
63
  import { ListenerManager } from './listener/manager';
59
64
  import {
60
65
  CancelPullLoop,
@@ -67,6 +72,7 @@ import {
67
72
  import {
68
73
  AddOperationOptions,
69
74
  BaseDocumentDriveServer,
75
+ DocumentDriveServerOptions,
70
76
  DriveEvents,
71
77
  GetDocumentOptions,
72
78
  IOperationResult,
@@ -75,6 +81,7 @@ import {
75
81
  StrandUpdate,
76
82
  SynchronizationUnitQuery,
77
83
  SyncStatus,
84
+ SyncUnitStatusObject,
78
85
  type CreateDocumentInput,
79
86
  type DriveInput,
80
87
  type OperationUpdate,
@@ -87,7 +94,6 @@ export * from './listener';
87
94
  export type * from './types';
88
95
 
89
96
  export const PULL_DRIVE_INTERVAL = 5000;
90
-
91
97
  export class DocumentDriveServer extends BaseDocumentDriveServer {
92
98
  private emitter = createNanoEvents<DriveEvents>();
93
99
  private cache: ICache;
@@ -98,15 +104,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
98
104
  DocumentDriveState['id'],
99
105
  Map<Trigger['id'], CancelPullLoop>
100
106
  >();
101
- private syncStatus = new Map<string, SyncStatus>();
107
+ private syncStatus = new Map<string, SyncUnitStatusObject>();
102
108
 
103
109
  private queueManager: IQueueManager;
110
+ private initializePromise: Promise<Error[] | null>;
111
+
112
+ private defaultDrivesManager: DefaultDrivesManager;
104
113
 
105
114
  constructor(
106
115
  documentModels: DocumentModel[],
107
116
  storage: IDriveStorage = new MemoryStorage(),
108
117
  cache: ICache = new InMemoryCache(),
109
- queueManager: IQueueManager = new BaseQueueManager()
118
+ queueManager: IQueueManager = new BaseQueueManager(),
119
+ options?: DocumentDriveServerOptions
110
120
  ) {
111
121
  super();
112
122
  this.listenerStateManager = new ListenerManager(this);
@@ -114,6 +124,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
114
124
  this.storage = storage;
115
125
  this.cache = cache;
116
126
  this.queueManager = queueManager;
127
+ this.defaultDrivesManager = new DefaultDrivesManager(
128
+ this,
129
+ this.defaultDrivesManagerDelegate,
130
+ options
131
+ );
117
132
 
118
133
  this.storage.setStorageDelegate?.({
119
134
  getCachedOperations: async (drive, id) => {
@@ -126,18 +141,139 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
126
141
  }
127
142
  }
128
143
  });
144
+
145
+ this.initializePromise = this._initialize();
146
+ }
147
+
148
+ getDefaultRemoteDrives() {
149
+ return this.defaultDrivesManager.getDefaultRemoteDrives();
150
+ }
151
+
152
+ private getOperationSource(source: StrandUpdateSource) {
153
+ return source.type === 'local' ? 'push' : 'pull';
154
+ }
155
+
156
+ private getCombinedSyncUnitStatus(
157
+ syncUnitStatus: SyncUnitStatusObject
158
+ ): SyncStatus {
159
+ if (!syncUnitStatus.pull && !syncUnitStatus.push) return 'INITIAL_SYNC';
160
+ if (syncUnitStatus.pull === 'INITIAL_SYNC') return 'INITIAL_SYNC';
161
+ if (syncUnitStatus.push === 'INITIAL_SYNC')
162
+ return syncUnitStatus.pull || 'INITIAL_SYNC';
163
+
164
+ const order: Array<SyncStatus> = [
165
+ 'ERROR',
166
+ 'MISSING',
167
+ 'CONFLICT',
168
+ 'SYNCING',
169
+ 'SUCCESS'
170
+ ];
171
+ const sortedStatus = Object.values(syncUnitStatus).sort(
172
+ (a, b) => order.indexOf(a) - order.indexOf(b)
173
+ );
174
+
175
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
176
+ return sortedStatus[0]!;
177
+ }
178
+
179
+ private initSyncStatus(
180
+ syncUnitId: string,
181
+ status: Partial<SyncUnitStatusObject>
182
+ ) {
183
+ const defaultSyncUnitStatus: SyncUnitStatusObject = Object.entries(
184
+ status
185
+ ).reduce((acc, [key, _status]) => {
186
+ return {
187
+ ...acc,
188
+ [key]: _status !== 'SYNCING' ? _status : 'INITIAL_SYNC'
189
+ };
190
+ }, {});
191
+
192
+ this.syncStatus.set(syncUnitId, defaultSyncUnitStatus);
193
+ this.emit(
194
+ 'syncStatus',
195
+ syncUnitId,
196
+ this.getCombinedSyncUnitStatus(defaultSyncUnitStatus),
197
+ undefined,
198
+ defaultSyncUnitStatus
199
+ );
129
200
  }
130
201
 
131
- private updateSyncStatus(
202
+ private async initializeDriveSyncStatus(
132
203
  driveId: string,
133
- status: SyncStatus | null,
204
+ drive: DocumentDriveDocument
205
+ ) {
206
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
207
+ const syncStatus: SyncUnitStatusObject = {
208
+ pull:
209
+ drive.state.local.triggers.length > 0
210
+ ? 'INITIAL_SYNC'
211
+ : undefined,
212
+ push: drive.state.local.listeners.length > 0 ? 'SUCCESS' : undefined
213
+ };
214
+
215
+ if (!syncStatus.pull && !syncStatus.push) return;
216
+
217
+ const syncUnitsIds = [driveId, ...syncUnits.map(s => s.syncId)];
218
+
219
+ for (const syncUnitId of syncUnitsIds) {
220
+ this.initSyncStatus(syncUnitId, syncStatus);
221
+ }
222
+ }
223
+
224
+ private updateSyncUnitStatus(
225
+ syncUnitId: string,
226
+ status: Partial<SyncUnitStatusObject> | null,
134
227
  error?: Error
135
228
  ) {
136
229
  if (status === null) {
137
- this.syncStatus.delete(driveId);
138
- } else if (this.syncStatus.get(driveId) !== status) {
139
- this.syncStatus.set(driveId, status);
140
- this.emit('syncStatus', driveId, status, error);
230
+ this.syncStatus.delete(syncUnitId);
231
+ return;
232
+ }
233
+
234
+ const syncUnitStatus = this.syncStatus.get(syncUnitId);
235
+
236
+ if (!syncUnitStatus) {
237
+ this.initSyncStatus(syncUnitId, status);
238
+ return;
239
+ }
240
+
241
+ const shouldUpdateStatus = Object.entries(status).some(
242
+ ([key, _status]) =>
243
+ syncUnitStatus[key as keyof SyncUnitStatusObject] !== _status
244
+ );
245
+
246
+ if (shouldUpdateStatus) {
247
+ const newstatus = Object.entries(status).reduce(
248
+ (acc, [key, _status]) => {
249
+ return {
250
+ ...acc,
251
+ // do not replace initial_syncing if it has not finished yet
252
+ [key]:
253
+ acc[key as keyof SyncUnitStatusObject] ===
254
+ 'INITIAL_SYNC' && _status === 'SYNCING'
255
+ ? 'INITIAL_SYNC'
256
+ : _status
257
+ };
258
+ },
259
+ syncUnitStatus
260
+ );
261
+
262
+ const previousCombinedStatus =
263
+ this.getCombinedSyncUnitStatus(syncUnitStatus);
264
+ const newCombinedStatus = this.getCombinedSyncUnitStatus(newstatus);
265
+
266
+ this.syncStatus.set(syncUnitId, newstatus);
267
+
268
+ if (previousCombinedStatus !== newCombinedStatus) {
269
+ this.emit(
270
+ 'syncStatus',
271
+ syncUnitId,
272
+ this.getCombinedSyncUnitStatus(newstatus),
273
+ error,
274
+ newstatus
275
+ );
276
+ }
141
277
  }
142
278
  }
143
279
 
@@ -162,7 +298,27 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
162
298
  ));
163
299
 
164
300
  if (result.status === 'ERROR') {
165
- this.updateSyncStatus(strand.driveId, result.status, result.error);
301
+ const syncUnits =
302
+ strand.documentId !== ''
303
+ ? (
304
+ await this.getSynchronizationUnitsIds(
305
+ strand.driveId,
306
+ [strand.documentId],
307
+ [strand.scope],
308
+ [strand.branch]
309
+ )
310
+ ).map(s => s.syncId)
311
+ : [strand.driveId];
312
+
313
+ const operationSource = this.getOperationSource(source);
314
+
315
+ for (const syncUnit of syncUnits) {
316
+ this.updateSyncUnitStatus(
317
+ syncUnit,
318
+ { [operationSource]: result.status },
319
+ result.error
320
+ );
321
+ }
166
322
  }
167
323
  this.emit('strandUpdate', strand);
168
324
  return result;
@@ -177,11 +333,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
177
333
  `Listener ${listener.listener.label ?? listener.listener.listenerId} error:`,
178
334
  error
179
335
  );
180
- this.updateSyncStatus(
181
- driveId,
182
- error instanceof OperationError ? error.status : 'ERROR',
183
- error
184
- );
336
+
337
+ const status = error instanceof OperationError ? error.status : 'ERROR';
338
+
339
+ this.updateSyncUnitStatus(driveId, { push: status }, error);
185
340
  }
186
341
 
187
342
  private shouldSyncRemoteDrive(drive: DocumentDriveDocument) {
@@ -213,10 +368,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
213
368
  driveTriggers = new Map();
214
369
  }
215
370
 
216
- this.updateSyncStatus(driveId, 'SYNCING');
371
+ this.updateSyncUnitStatus(driveId, { pull: 'SYNCING' });
217
372
 
218
373
  for (const syncUnit of syncUnits) {
219
- this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
374
+ this.updateSyncUnitStatus(syncUnit.syncId, { pull: 'SYNCING' });
220
375
  }
221
376
 
222
377
  if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
@@ -226,11 +381,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
226
381
  trigger,
227
382
  this.saveStrand.bind(this),
228
383
  error => {
229
- this.updateSyncStatus(
230
- driveId,
384
+ const statusError =
231
385
  error instanceof OperationError
232
386
  ? error.status
233
- : 'ERROR',
387
+ : 'ERROR';
388
+
389
+ this.updateSyncUnitStatus(
390
+ driveId,
391
+ { pull: statusError },
234
392
  error
235
393
  );
236
394
 
@@ -248,28 +406,47 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
248
406
  const errorRevision = revisions.filter(
249
407
  r => r.status !== 'SUCCESS'
250
408
  );
409
+
251
410
  if (errorRevision.length < 1) {
252
- this.updateSyncStatus(driveId, 'SUCCESS');
411
+ this.updateSyncUnitStatus(driveId, {
412
+ pull: 'SUCCESS'
413
+ });
253
414
  }
254
415
 
255
- for (const syncUnit of syncUnits) {
256
- const fileErrorRevision = errorRevision.find(
257
- r => r.documentId === syncUnit.documentId
258
- );
416
+ const documentIdsFromRevision = revisions
417
+ .filter(rev => rev.documentId !== '')
418
+ .map(rev => rev.documentId);
259
419
 
260
- if (fileErrorRevision) {
261
- this.updateSyncStatus(
262
- syncUnit.syncId,
263
- fileErrorRevision.status,
264
- fileErrorRevision.error
265
- );
266
- } else {
267
- this.updateSyncStatus(
268
- syncUnit.syncId,
269
- 'SUCCESS'
270
- );
271
- }
272
- }
420
+ this.getSynchronizationUnitsIds(
421
+ driveId,
422
+ documentIdsFromRevision
423
+ )
424
+ .then(revSyncUnits => {
425
+ for (const syncUnit of revSyncUnits) {
426
+ const fileErrorRevision =
427
+ errorRevision.find(
428
+ r =>
429
+ r.documentId ===
430
+ syncUnit.documentId
431
+ );
432
+
433
+ if (fileErrorRevision) {
434
+ this.updateSyncUnitStatus(
435
+ syncUnit.syncId,
436
+ { pull: fileErrorRevision.status },
437
+ fileErrorRevision.error
438
+ );
439
+ } else {
440
+ this.updateSyncUnitStatus(
441
+ syncUnit.syncId,
442
+ {
443
+ pull: 'SUCCESS'
444
+ }
445
+ );
446
+ }
447
+ }
448
+ })
449
+ .catch(console.error);
273
450
 
274
451
  // if it is the first pull and returns empty
275
452
  // then updates corresponding push transmitter
@@ -311,20 +488,25 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
311
488
 
312
489
  private async stopSyncRemoteDrive(driveId: string) {
313
490
  const syncUnits = await this.getSynchronizationUnitsIds(driveId);
314
- const fileNodes = syncUnits
491
+ const filesNodeSyncId = syncUnits
315
492
  .filter(syncUnit => syncUnit.documentId !== '')
316
- .map(syncUnit => syncUnit.documentId);
493
+ .map(syncUnit => syncUnit.syncId);
317
494
 
318
495
  const triggers = this.triggerMap.get(driveId);
319
496
  triggers?.forEach(cancel => cancel());
320
- this.updateSyncStatus(driveId, null);
497
+ this.updateSyncUnitStatus(driveId, null);
321
498
 
322
- for (const fileNode of fileNodes) {
323
- this.updateSyncStatus(fileNode, null);
499
+ for (const fileNodeSyncId of filesNodeSyncId) {
500
+ this.updateSyncUnitStatus(fileNodeSyncId, null);
324
501
  }
325
502
  return this.triggerMap.delete(driveId);
326
503
  }
327
504
 
505
+ private defaultDrivesManagerDelegate = {
506
+ emit: (...args: Parameters<DriveEvents['defaultRemoteDrive']>) =>
507
+ this.emit('defaultRemoteDrive', ...args)
508
+ };
509
+
328
510
  private queueDelegate = {
329
511
  checkDocumentExists: (
330
512
  driveId: string,
@@ -372,7 +554,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
372
554
  }
373
555
  };
374
556
 
375
- async initialize() {
557
+ initialize() {
558
+ return this.initializePromise;
559
+ }
560
+
561
+ private async _initialize() {
562
+ try {
563
+ await this.defaultDrivesManager.removeOldremoteDrives();
564
+ } catch (error) {
565
+ logger.error(error);
566
+ }
567
+
376
568
  const errors: Error[] = [];
377
569
  const drives = await this.getDrives();
378
570
  for (const drive of drives) {
@@ -387,6 +579,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
387
579
  errors.push(error);
388
580
  });
389
581
 
582
+ await this.defaultDrivesManager.initializeDefaultRemoteDrives();
583
+
390
584
  // if network connect comes back online
391
585
  // then triggers the listeners update
392
586
  if (typeof window !== 'undefined') {
@@ -411,6 +605,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
411
605
 
412
606
  private async _initializeDrive(driveId: string) {
413
607
  const drive = await this.getDrive(driveId);
608
+ await this.initializeDriveSyncStatus(driveId, drive);
414
609
 
415
610
  if (this.shouldSyncRemoteDrive(drive)) {
416
611
  await this.startSyncRemoteDrive(driveId);
@@ -689,7 +884,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
689
884
 
690
885
  const drives = await this.storage.getDrives();
691
886
  if (drives.includes(id)) {
692
- throw new Error('Drive already exists');
887
+ throw new DriveAlreadyExistsError(id);
693
888
  }
694
889
 
695
890
  const document = utils.createDocument({
@@ -711,7 +906,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
711
906
  url: string,
712
907
  options: RemoteDriveOptions
713
908
  ): Promise<DocumentDriveDocument> {
714
- const { id, name, slug, icon } = await requestPublicDrive(url);
909
+ const { id, name, slug, icon } =
910
+ options.expectedDriveInfo || (await requestPublicDrive(url));
911
+
715
912
  const {
716
913
  pullFilter,
717
914
  pullInterval,
@@ -879,6 +1076,16 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
879
1076
  };
880
1077
  await this.storage.createDocument(driveId, input.id, documentStorage);
881
1078
 
1079
+ // set initial state for new syncUnits
1080
+ for (const syncUnit of input.synchronizationUnits) {
1081
+ this.initSyncStatus(syncUnit.syncId, {
1082
+ pull: this.triggerMap.get(driveId) ? 'INITIAL_SYNC' : undefined,
1083
+ push: this.listenerStateManager.driveHasListeners(driveId)
1084
+ ? 'SUCCESS'
1085
+ : undefined
1086
+ });
1087
+ }
1088
+
882
1089
  // if the document contains operations then
883
1090
  // stores the operations in the storage
884
1091
  const operations = Object.values(document.operations).flat();
@@ -907,6 +1114,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
907
1114
  const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
908
1115
  id
909
1116
  ]);
1117
+
1118
+ // remove document sync units status when a document is deleted
1119
+ for (const syncUnit of syncUnits) {
1120
+ this.updateSyncUnitStatus(syncUnit.syncId, null);
1121
+ }
910
1122
  await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
911
1123
  } catch (error) {
912
1124
  logger.warn('Error deleting document', error);
@@ -989,7 +1201,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
989
1201
  );
990
1202
  document = appliedResult.document;
991
1203
  signals.push(...appliedResult.signals);
992
- operationsApplied.push(...appliedResult.operation);
1204
+ operationsApplied.push(appliedResult.operation);
1205
+
1206
+ // TODO what to do if one of the applied operations has an error?
993
1207
  } catch (e) {
994
1208
  error =
995
1209
  e instanceof OperationError
@@ -1189,21 +1403,26 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1189
1403
  { skip: operation.skip, reuseOperationResultingState: true }
1190
1404
  ) as T;
1191
1405
 
1192
- const appliedOperation = newDocument.operations[operation.scope].filter(
1406
+ const appliedOperations = newDocument.operations[
1407
+ operation.scope
1408
+ ].filter(
1193
1409
  op => op.index == operation.index && op.skip == operation.skip
1194
1410
  );
1411
+ const appliedOperation = appliedOperations.at(0);
1195
1412
 
1196
- if (appliedOperation.length < 1) {
1413
+ if (!appliedOperation) {
1197
1414
  throw new OperationError(
1198
1415
  'ERROR',
1199
1416
  operation,
1200
1417
  `Operation with index ${operation.index}:${operation.skip} was not applied.`
1201
1418
  );
1202
- } else if (
1203
- appliedOperation[0]!.hash !== operation.hash &&
1419
+ }
1420
+ if (
1421
+ !appliedOperation.error &&
1422
+ appliedOperation.hash !== operation.hash &&
1204
1423
  !skipHashValidation
1205
1424
  ) {
1206
- throw new ConflictOperationError(operation, appliedOperation[0]!);
1425
+ throw new ConflictOperationError(operation, appliedOperation);
1207
1426
  }
1208
1427
 
1209
1428
  for (const signalHandler of operationSignals) {
@@ -1546,26 +1765,38 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1546
1765
  : (options?.source ?? { type: 'local' });
1547
1766
 
1548
1767
  // update listener cache
1768
+
1769
+ const operationSource = this.getOperationSource(source);
1770
+
1549
1771
  this.listenerStateManager
1550
1772
  .updateSynchronizationRevisions(
1551
1773
  drive,
1552
1774
  syncUnits,
1553
1775
  source,
1554
1776
  () => {
1555
- this.updateSyncStatus(drive, 'SYNCING');
1777
+ this.updateSyncUnitStatus(drive, {
1778
+ [operationSource]: 'SYNCING'
1779
+ });
1556
1780
 
1557
1781
  for (const syncUnit of syncUnits) {
1558
- this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
1782
+ this.updateSyncUnitStatus(syncUnit.syncId, {
1783
+ [operationSource]: 'SYNCING'
1784
+ });
1559
1785
  }
1560
1786
  },
1561
1787
  this.handleListenerError.bind(this),
1562
1788
  options?.forceSync ?? source.type === 'local'
1563
1789
  )
1564
1790
  .then(updates => {
1565
- updates.length && this.updateSyncStatus(drive, 'SUCCESS');
1791
+ updates.length &&
1792
+ this.updateSyncUnitStatus(drive, {
1793
+ [operationSource]: 'SUCCESS'
1794
+ });
1566
1795
 
1567
1796
  for (const syncUnit of syncUnits) {
1568
- this.updateSyncStatus(syncUnit.syncId, 'SUCCESS');
1797
+ this.updateSyncUnitStatus(syncUnit.syncId, {
1798
+ [operationSource]: 'SUCCESS'
1799
+ });
1569
1800
  }
1570
1801
  })
1571
1802
  .catch(error => {
@@ -1573,12 +1804,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1573
1804
  'Non handled error updating sync revision',
1574
1805
  error
1575
1806
  );
1576
- this.updateSyncStatus(drive, 'ERROR', error as Error);
1807
+ this.updateSyncUnitStatus(
1808
+ drive,
1809
+ {
1810
+ [operationSource]: 'ERROR'
1811
+ },
1812
+ error as Error
1813
+ );
1577
1814
 
1578
1815
  for (const syncUnit of syncUnits) {
1579
- this.updateSyncStatus(
1816
+ this.updateSyncUnitStatus(
1580
1817
  syncUnit.syncId,
1581
- 'ERROR',
1818
+ {
1819
+ [operationSource]: 'ERROR'
1820
+ },
1582
1821
  error as Error
1583
1822
  );
1584
1823
  }
@@ -1772,7 +2011,6 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1772
2011
  return result;
1773
2012
  }
1774
2013
 
1775
- const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
1776
2014
  try {
1777
2015
  await this._addDriveOperations(drive, async documentStorage => {
1778
2016
  const result = await this._processOperations<
@@ -1811,26 +2049,6 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1811
2049
  }
1812
2050
  }
1813
2051
 
1814
- const syncUnits = await this.getSynchronizationUnitsIds(
1815
- drive,
1816
- undefined,
1817
- undefined,
1818
- undefined,
1819
- undefined,
1820
- document
1821
- );
1822
-
1823
- const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
1824
- const syncUnitsIds = syncUnits.map(unit => unit.syncId);
1825
-
1826
- const newSyncUnits = syncUnitsIds.filter(
1827
- syncUnitId => !prevSyncUnitsIds.includes(syncUnitId)
1828
- );
1829
-
1830
- const removedSyncUnits = prevSyncUnitsIds.filter(
1831
- syncUnitId => !syncUnitsIds.includes(syncUnitId)
1832
- );
1833
-
1834
2052
  // update listener cache
1835
2053
  const lastOperation = operationsApplied
1836
2054
  .filter(op => op.scope === 'global')
@@ -1857,6 +2075,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1857
2075
  ? { type: 'local' }
1858
2076
  : (options?.source ?? { type: 'local' });
1859
2077
 
2078
+ const operationSource = this.getOperationSource(source);
2079
+
1860
2080
  this.listenerStateManager
1861
2081
  .updateSynchronizationRevisions(
1862
2082
  drive,
@@ -1874,29 +2094,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1874
2094
  ],
1875
2095
  source,
1876
2096
  () => {
1877
- this.updateSyncStatus(drive, 'SYNCING');
1878
-
1879
- for (const syncUnitId of [
1880
- ...newSyncUnits,
1881
- ...removedSyncUnits
1882
- ]) {
1883
- this.updateSyncStatus(syncUnitId, 'SYNCING');
1884
- }
2097
+ this.updateSyncUnitStatus(drive, {
2098
+ [operationSource]: 'SYNCING'
2099
+ });
1885
2100
  },
1886
2101
  this.handleListenerError.bind(this),
1887
2102
  options?.forceSync ?? source.type === 'local'
1888
2103
  )
1889
2104
  .then(updates => {
1890
2105
  if (updates.length) {
1891
- this.updateSyncStatus(drive, 'SUCCESS');
1892
-
1893
- for (const syncUnitId of newSyncUnits) {
1894
- this.updateSyncStatus(syncUnitId, 'SUCCESS');
1895
- }
1896
-
1897
- for (const syncUnitId of removedSyncUnits) {
1898
- this.updateSyncStatus(syncUnitId, null);
1899
- }
2106
+ this.updateSyncUnitStatus(drive, {
2107
+ [operationSource]: 'SUCCESS'
2108
+ });
1900
2109
  }
1901
2110
  })
1902
2111
  .catch(error => {
@@ -1904,18 +2113,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1904
2113
  'Non handled error updating sync revision',
1905
2114
  error
1906
2115
  );
1907
- this.updateSyncStatus(drive, 'ERROR', error as Error);
1908
-
1909
- for (const syncUnitId of [
1910
- ...newSyncUnits,
1911
- ...removedSyncUnits
1912
- ]) {
1913
- this.updateSyncStatus(
1914
- syncUnitId,
1915
- 'ERROR',
1916
- error as Error
1917
- );
1918
- }
2116
+ this.updateSyncUnitStatus(
2117
+ drive,
2118
+ { [operationSource]: 'ERROR' },
2119
+ error as Error
2120
+ );
1919
2121
  });
1920
2122
  }
1921
2123
 
@@ -2108,7 +2310,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
2108
2310
  logger.error(`Sync status not found for drive ${drive}`);
2109
2311
  throw new Error(`Sync status not found for drive ${drive}`);
2110
2312
  }
2111
- return status;
2313
+ return this.getCombinedSyncUnitStatus(status);
2112
2314
  }
2113
2315
 
2114
2316
  on<K extends keyof DriveEvents>(event: K, cb: DriveEvents[K]): Unsubscribe {
@@ -56,6 +56,10 @@ export class ListenerManager extends BaseListenerManager {
56
56
  return Promise.resolve(this.transmitters[driveId]?.[listenerId]);
57
57
  }
58
58
 
59
+ driveHasListeners(driveId: string) {
60
+ return this.listenerState.has(driveId);
61
+ }
62
+
59
63
  async addListener(listener: Listener) {
60
64
  const drive = listener.driveId;
61
65
 
@@ -20,6 +20,7 @@ import type {
20
20
  State
21
21
  } from 'document-model/document';
22
22
  import { Unsubscribe } from 'nanoevents';
23
+ import { DriveInfo } from '../utils/graphql';
23
24
  import { OperationError } from './error';
24
25
  import {
25
26
  ITransmitter,
@@ -36,6 +37,7 @@ export type RemoteDriveOptions = DocumentDriveLocalState & {
36
37
  // TODO make local state optional
37
38
  pullFilter?: ListenerFilter;
38
39
  pullInterval?: number;
40
+ expectedDriveInfo?: DriveInfo;
39
41
  };
40
42
 
41
43
  export type CreateDocumentInput = CreateChildDocumentInput;
@@ -138,10 +140,38 @@ export type StrandUpdate = {
138
140
  operations: OperationUpdate[];
139
141
  };
140
142
 
141
- export type SyncStatus = 'SYNCING' | UpdateStatus;
143
+ export type SyncStatus = 'INITIAL_SYNC' | 'SYNCING' | UpdateStatus;
144
+
145
+ export type PullSyncStatus = SyncStatus;
146
+ export type PushSyncStatus = SyncStatus;
147
+
148
+ export type SyncUnitStatusObject = {
149
+ push?: PushSyncStatus;
150
+ pull?: PullSyncStatus;
151
+ };
152
+
153
+ export type AddRemoteDriveStatus =
154
+ | 'SUCCESS'
155
+ | 'ERROR'
156
+ | 'PENDING'
157
+ | 'ADDING'
158
+ | 'ALREADY_ADDED';
142
159
 
143
160
  export interface DriveEvents {
144
- syncStatus: (driveId: string, status: SyncStatus, error?: Error) => void;
161
+ syncStatus: (
162
+ driveId: string,
163
+ status: SyncStatus,
164
+ error?: Error,
165
+ syncUnitStatus?: SyncUnitStatusObject
166
+ ) => void;
167
+ defaultRemoteDrive: (
168
+ status: AddRemoteDriveStatus,
169
+ defaultDrives: Map<string, DefaultRemoteDriveInfo>,
170
+ driveInput: DefaultRemoteDriveInput,
171
+ driveId?: string,
172
+ driveName?: string,
173
+ error?: Error
174
+ ) => void;
145
175
  strandUpdate: (update: StrandUpdate) => void;
146
176
  clientStrandsError: (
147
177
  driveId: string,
@@ -168,6 +198,45 @@ export type AddOperationOptions = {
168
198
  source: StrandUpdateSource;
169
199
  };
170
200
 
201
+ export type DefaultRemoteDriveInput = {
202
+ url: string;
203
+ options: RemoteDriveOptions;
204
+ };
205
+
206
+ export type DefaultRemoteDriveInfo = DefaultRemoteDriveInput & {
207
+ status: AddRemoteDriveStatus;
208
+ metadata?: DriveInfo;
209
+ };
210
+
211
+ export type RemoveOldRemoteDrivesOption =
212
+ | {
213
+ strategy: 'remove-all';
214
+ }
215
+ | {
216
+ strategy: 'preserve-all';
217
+ }
218
+ | {
219
+ strategy: 'remove-by-id';
220
+ ids: string[];
221
+ }
222
+ | {
223
+ strategy: 'remove-by-url';
224
+ urls: string[];
225
+ }
226
+ | {
227
+ strategy: 'preserve-by-id';
228
+ ids: string[];
229
+ }
230
+ | {
231
+ strategy: 'preserve-by-url';
232
+ urls: string[];
233
+ };
234
+
235
+ export type DocumentDriveServerOptions = {
236
+ defaultRemoteDrives?: Array<DefaultRemoteDriveInput>;
237
+ removeOldRemoteDrives?: RemoveOldRemoteDrivesOption;
238
+ };
239
+
171
240
  export abstract class BaseDocumentDriveServer {
172
241
  /** Public methods **/
173
242
  abstract getDrives(): Promise<string[]>;
@@ -380,6 +449,7 @@ export abstract class BaseListenerManager {
380
449
  abstract initDrive(drive: DocumentDriveDocument): Promise<void>;
381
450
  abstract removeDrive(driveId: DocumentDriveState['id']): Promise<void>;
382
451
 
452
+ abstract driveHasListeners(driveId: string): boolean;
383
453
  abstract addListener(listener: Listener): Promise<ITransmitter>;
384
454
  abstract removeListener(
385
455
  driveId: string,
@@ -6,7 +6,10 @@ import {
6
6
  Operation,
7
7
  OperationScope
8
8
  } from 'document-model/document';
9
- import { mergeOperations, type SynchronizationUnitQuery } from '..';
9
+ import { DriveNotFoundError } from '../server/error';
10
+ import { SynchronizationUnitQuery } from '../server/types';
11
+ import { mergeOperations } from '../utils';
12
+ import { migrateDocumentOperationSigatures } from '../utils/migrations';
10
13
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
11
14
 
12
15
  export class BrowserStorage implements IDriveStorage {
@@ -110,7 +113,7 @@ export class BrowserStorage implements IDriveStorage {
110
113
  this.buildKey(BrowserStorage.DRIVES_KEY, id)
111
114
  );
112
115
  if (!drive) {
113
- throw new Error(`Drive with id ${id} not found`);
116
+ throw new DriveNotFoundError(id);
114
117
  }
115
118
  return drive;
116
119
  }
@@ -214,4 +217,28 @@ export class BrowserStorage implements IDriveStorage {
214
217
  return acc;
215
218
  }, []);
216
219
  }
220
+
221
+ // migrates all stored operations from legacy signature to signatures array
222
+ async migrateOperationSignatures() {
223
+ const drives = await this.getDrives();
224
+ for (const drive of drives) {
225
+ await this.migrateDocument(BrowserStorage.DRIVES_KEY, drive);
226
+
227
+ const documents = await this.getDocuments(drive);
228
+ await Promise.all(
229
+ documents.map(async docId => this.migrateDocument(drive, docId))
230
+ );
231
+ }
232
+ }
233
+
234
+ private async migrateDocument(drive: string, id: string) {
235
+ const document = await this.getDocument(drive, id);
236
+ const migratedDocument = migrateDocumentOperationSigatures(document);
237
+ if (migratedDocument !== document) {
238
+ return (await this.db).setItem(
239
+ this.buildKey(drive, id),
240
+ migratedDocument
241
+ );
242
+ }
243
+ }
217
244
  }
@@ -17,6 +17,7 @@ import fs from 'fs/promises';
17
17
  import stringify from 'json-stringify-deterministic';
18
18
  import path from 'path';
19
19
  import sanitize from 'sanitize-filename';
20
+ import { DriveNotFoundError } from '../server/error';
20
21
  import type { SynchronizationUnitQuery } from '../server/types';
21
22
  import { mergeOperations } from '../utils';
22
23
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
@@ -199,7 +200,7 @@ export class FilesystemStorage implements IDriveStorage {
199
200
  id
200
201
  )) as DocumentDriveStorage;
201
202
  } catch {
202
- throw new Error(`Drive with id ${id} not found`);
203
+ throw new DriveNotFoundError(id);
203
204
  }
204
205
  }
205
206
 
@@ -6,6 +6,7 @@ import {
6
6
  Operation,
7
7
  OperationScope
8
8
  } from 'document-model/document';
9
+ import { DriveNotFoundError } from '../server/error';
9
10
  import type { SynchronizationUnitQuery } from '../server/types';
10
11
  import { mergeOperations } from '../utils';
11
12
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
@@ -31,7 +32,7 @@ export class MemoryStorage implements IDriveStorage {
31
32
  async getDocument(driveId: string, id: string) {
32
33
  const drive = this.documents[driveId];
33
34
  if (!drive) {
34
- throw new Error(`Drive with id ${driveId} not found`);
35
+ throw new DriveNotFoundError(driveId);
35
36
  }
36
37
  const document = drive[id];
37
38
  if (!document) {
@@ -102,7 +103,7 @@ export class MemoryStorage implements IDriveStorage {
102
103
 
103
104
  async deleteDocument(drive: string, id: string) {
104
105
  if (!this.documents[drive]) {
105
- throw new Error(`Drive with id ${drive} not found`);
106
+ throw new DriveNotFoundError(drive);
106
107
  }
107
108
  delete this.documents[drive]![id];
108
109
  }
@@ -114,7 +115,7 @@ export class MemoryStorage implements IDriveStorage {
114
115
  async getDrive(id: string) {
115
116
  const drive = this.drives[id];
116
117
  if (!drive) {
117
- throw new Error(`Drive with id ${id} not found`);
118
+ throw new DriveNotFoundError(id);
118
119
  }
119
120
  return drive;
120
121
  }
@@ -19,7 +19,7 @@ import type {
19
19
  State
20
20
  } from 'document-model/document';
21
21
  import { IBackOffOptions, backOff } from 'exponential-backoff';
22
- import { ConflictOperationError } from '../server/error';
22
+ import { ConflictOperationError, DriveNotFoundError } from '../server/error';
23
23
  import type { SynchronizationUnitQuery } from '../server/types';
24
24
  import { logger } from '../utils/logger';
25
25
  import {
@@ -533,7 +533,7 @@ export class PrismaStorage implements IDriveStorage {
533
533
  return doc as DocumentDriveStorage;
534
534
  } catch (e) {
535
535
  logger.error(e);
536
- throw new Error(`Drive with id ${id} not found`);
536
+ throw new DriveNotFoundError(id);
537
537
  }
538
538
  }
539
539
 
@@ -561,6 +561,13 @@ export class PrismaStorage implements IDriveStorage {
561
561
 
562
562
  // delete drive document and its operations
563
563
  await this.deleteDocument('drives', id);
564
+
565
+ // deletes all documents of the drive
566
+ await this.db.document.deleteMany({
567
+ where: {
568
+ driveId: id
569
+ }
570
+ });
564
571
  }
565
572
 
566
573
  async getOperationResultingState(
@@ -648,4 +655,22 @@ export class PrismaStorage implements IDriveStorage {
648
655
  lastUpdated: new Date(row.lastUpdated).toISOString()
649
656
  }));
650
657
  }
658
+
659
+ // migrates all stored operations from legacy signature to signatures array
660
+ async migrateOperationSignatures() {
661
+ const count = await this.db.$executeRaw`
662
+ UPDATE "Operation"
663
+ SET context = jsonb_set(
664
+ context #- '{signer,signature}', -- Remove the old 'signature' field
665
+ '{signer,signatures}', -- Path to the new 'signatures' field
666
+ CASE
667
+ WHEN context->'signer'->>'signature' = '' THEN '[]'::jsonb
668
+ ELSE to_jsonb(array[context->'signer'->>'signature'])
669
+ END
670
+ )
671
+ WHERE context->'signer' ? 'signature' -- Check if the 'signature' key exists
672
+ `;
673
+ logger.info(`Migrated ${count} operations`);
674
+ return;
675
+ }
651
676
  }
@@ -0,0 +1,239 @@
1
+ import {
2
+ BaseDocumentDriveServer,
3
+ DefaultRemoteDriveInfo,
4
+ DocumentDriveServerOptions,
5
+ DriveEvents,
6
+ RemoveOldRemoteDrivesOption
7
+ } from '../server';
8
+ import { DriveNotFoundError } from '../server/error';
9
+ import { requestPublicDrive } from './graphql';
10
+ import { logger } from './logger';
11
+
12
+ export interface IServerDelegateDrivesManager {
13
+ emit: (...args: Parameters<DriveEvents['defaultRemoteDrive']>) => void;
14
+ }
15
+
16
+ export class DefaultDrivesManager {
17
+ private defaultRemoteDrives = new Map<string, DefaultRemoteDriveInfo>();
18
+ private removeOldRemoteDrivesConfig: RemoveOldRemoteDrivesOption;
19
+
20
+ constructor(
21
+ private server: BaseDocumentDriveServer,
22
+ private delegate: IServerDelegateDrivesManager,
23
+ options?: Pick<
24
+ DocumentDriveServerOptions,
25
+ 'defaultRemoteDrives' | 'removeOldRemoteDrives'
26
+ >
27
+ ) {
28
+ if (options?.defaultRemoteDrives) {
29
+ for (const defaultDrive of options.defaultRemoteDrives) {
30
+ this.defaultRemoteDrives.set(defaultDrive.url, {
31
+ ...defaultDrive,
32
+ status: 'PENDING'
33
+ });
34
+ }
35
+ }
36
+
37
+ const strategyFromEnv =
38
+ process.env.DOCUMENT_DRIVE_OLD_REMOTE_DRIVES_STRATEGY;
39
+ const urlsFromEnv = process.env.DOCUMENT_DRIVE_OLD_REMOTE_DRIVES_URLS;
40
+ const idsFromEnv = process.env.DOCUMENT_DRIVE_OLD_REMOTE_DRIVES_IDS;
41
+
42
+ const strategy: RemoveOldRemoteDrivesOption['strategy'] =
43
+ strategyFromEnv !== undefined && strategyFromEnv !== ''
44
+ ? (strategyFromEnv as RemoveOldRemoteDrivesOption['strategy'])
45
+ : 'preserve-all';
46
+
47
+ const urls =
48
+ urlsFromEnv !== undefined && urlsFromEnv !== ''
49
+ ? urlsFromEnv.split(',')
50
+ : [];
51
+
52
+ const ids =
53
+ idsFromEnv !== undefined && idsFromEnv !== ''
54
+ ? idsFromEnv.split(',')
55
+ : [];
56
+
57
+ this.removeOldRemoteDrivesConfig = options?.removeOldRemoteDrives || {
58
+ strategy,
59
+ urls,
60
+ ids
61
+ };
62
+ }
63
+
64
+ getDefaultRemoteDrives() {
65
+ return this.defaultRemoteDrives;
66
+ }
67
+
68
+ private async deleteDriveById(driveId: string) {
69
+ try {
70
+ await this.server.deleteDrive(driveId);
71
+ } catch (error) {
72
+ if (!(error instanceof DriveNotFoundError)) {
73
+ logger.error(error);
74
+ }
75
+ }
76
+ }
77
+
78
+ private async preserveDrivesById(
79
+ drivesIdsToRemove: string[],
80
+ drives: string[]
81
+ ) {
82
+ const getAllDrives = drives.map(driveId =>
83
+ this.server.getDrive(driveId)
84
+ );
85
+
86
+ const drivesToRemove = (await Promise.all(getAllDrives))
87
+ .filter(
88
+ drive =>
89
+ drive.state.local.listeners.length > 0 ||
90
+ drive.state.local.triggers.length > 0
91
+ )
92
+ .filter(
93
+ drive => !drivesIdsToRemove.includes(drive.state.global.id)
94
+ );
95
+
96
+ const driveIds = drivesToRemove.map(drive => drive.state.global.id);
97
+ await this.removeDrivesById(driveIds);
98
+ }
99
+
100
+ private async removeDrivesById(driveIds: string[]) {
101
+ for (const driveId of driveIds) {
102
+ await this.deleteDriveById(driveId);
103
+ }
104
+ }
105
+
106
+ async removeOldremoteDrives() {
107
+ const driveids = await this.server.getDrives();
108
+
109
+ switch (this.removeOldRemoteDrivesConfig.strategy) {
110
+ case 'preserve-by-id': {
111
+ await this.preserveDrivesById(
112
+ this.removeOldRemoteDrivesConfig.ids,
113
+ driveids
114
+ );
115
+ break;
116
+ }
117
+ case 'preserve-by-url': {
118
+ const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
119
+ url => requestPublicDrive(url)
120
+ );
121
+
122
+ const drivesIdsToPreserve = (
123
+ await Promise.all(getDrivesInfo)
124
+ ).map(driveInfo => driveInfo.id);
125
+
126
+ await this.preserveDrivesById(drivesIdsToPreserve, driveids);
127
+ break;
128
+ }
129
+ case 'remove-by-id': {
130
+ const drivesIdsToRemove =
131
+ this.removeOldRemoteDrivesConfig.ids.filter(driveId =>
132
+ driveids.includes(driveId)
133
+ );
134
+
135
+ await this.removeDrivesById(drivesIdsToRemove);
136
+ break;
137
+ }
138
+ case 'remove-by-url': {
139
+ const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
140
+ driveUrl => requestPublicDrive(driveUrl)
141
+ );
142
+ const drivesInfo = await Promise.all(getDrivesInfo);
143
+
144
+ const drivesIdsToRemove = drivesInfo
145
+ .map(driveInfo => driveInfo.id)
146
+ .filter(driveId => driveids.includes(driveId));
147
+
148
+ await this.removeDrivesById(drivesIdsToRemove);
149
+ break;
150
+ }
151
+ case 'remove-all': {
152
+ const getDrives = driveids.map(driveId =>
153
+ this.server.getDrive(driveId)
154
+ );
155
+ const drives = await Promise.all(getDrives);
156
+ const drivesToRemove = drives
157
+ .filter(
158
+ drive =>
159
+ drive.state.local.listeners.length > 0 ||
160
+ drive.state.local.triggers.length > 0
161
+ )
162
+ .map(drive => drive.state.global.id);
163
+
164
+ await this.removeDrivesById(drivesToRemove);
165
+ break;
166
+ }
167
+ }
168
+ }
169
+
170
+ async initializeDefaultRemoteDrives() {
171
+ const drives = await this.server.getDrives();
172
+
173
+ for (const remoteDrive of this.defaultRemoteDrives.values()) {
174
+ let remoteDriveInfo = { ...remoteDrive };
175
+
176
+ try {
177
+ const driveInfo = await requestPublicDrive(remoteDrive.url);
178
+
179
+ remoteDriveInfo = { ...remoteDrive, metadata: driveInfo };
180
+
181
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
182
+
183
+ if (drives.includes(driveInfo.id)) {
184
+ remoteDriveInfo.status = 'ALREADY_ADDED';
185
+
186
+ this.defaultRemoteDrives.set(
187
+ remoteDrive.url,
188
+ remoteDriveInfo
189
+ );
190
+ this.delegate.emit(
191
+ 'ALREADY_ADDED',
192
+ this.defaultRemoteDrives,
193
+ remoteDriveInfo,
194
+ driveInfo.id,
195
+ driveInfo.name
196
+ );
197
+ continue;
198
+ }
199
+
200
+ remoteDriveInfo.status = 'ADDING';
201
+
202
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
203
+ this.delegate.emit(
204
+ 'ADDING',
205
+ this.defaultRemoteDrives,
206
+ remoteDriveInfo
207
+ );
208
+
209
+ await this.server.addRemoteDrive(remoteDrive.url, {
210
+ ...remoteDrive.options,
211
+ expectedDriveInfo: driveInfo
212
+ });
213
+
214
+ remoteDriveInfo.status = 'SUCCESS';
215
+
216
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
217
+ this.delegate.emit(
218
+ 'SUCCESS',
219
+ this.defaultRemoteDrives,
220
+ remoteDriveInfo,
221
+ driveInfo.id,
222
+ driveInfo.name
223
+ );
224
+ } catch (error) {
225
+ remoteDriveInfo.status = 'ERROR';
226
+
227
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
228
+ this.delegate.emit(
229
+ 'ERROR',
230
+ this.defaultRemoteDrives,
231
+ remoteDriveInfo,
232
+ undefined,
233
+ undefined,
234
+ error as Error
235
+ );
236
+ }
237
+ }
238
+ }
239
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ Action,
3
+ Document,
4
+ DocumentOperations,
5
+ Operation,
6
+ OperationScope
7
+ } from 'document-model/document';
8
+ import { DocumentStorage } from '../storage/types';
9
+
10
+ export function migrateDocumentOperationSigatures<D extends Document>(
11
+ document: DocumentStorage<D>
12
+ ): DocumentStorage<D> | undefined {
13
+ let legacy = false;
14
+ const operations = Object.entries(document.operations).reduce<
15
+ DocumentOperations<Action>
16
+ >(
17
+ (acc, [key, operations]) => {
18
+ const scope = key as unknown as OperationScope;
19
+ for (const op of operations) {
20
+ const newOp = migrateLegacyOperationSignature(op);
21
+ acc[scope].push(newOp);
22
+ if (newOp !== op) {
23
+ legacy = true;
24
+ }
25
+ }
26
+ return acc;
27
+ },
28
+ { global: [], local: [] }
29
+ );
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
31
+ return legacy ? { ...document, operations } : document;
32
+ }
33
+
34
+ export function migrateLegacyOperationSignature<A extends Action>(
35
+ operation: Operation<A>
36
+ ): Operation<A> {
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
38
+ if (!operation.context?.signer || operation.context.signer.signatures) {
39
+ return operation;
40
+ }
41
+ const { signer } = operation.context;
42
+ if ('signature' in signer) {
43
+ const signature = signer.signature as string | undefined;
44
+ return {
45
+ ...operation,
46
+ context: {
47
+ ...operation.context,
48
+ signer: {
49
+ user: signer.user,
50
+ app: signer.app,
51
+ signatures: signature?.length ? [signature] : []
52
+ }
53
+ }
54
+ };
55
+ } else {
56
+ return operation;
57
+ }
58
+ }