document-drive 1.0.0-alpha.71 → 1.0.0-alpha.73

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.71",
3
+ "version": "1.0.0-alpha.73",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -61,6 +61,7 @@ import {
61
61
  ListenerState,
62
62
  RemoteDriveOptions,
63
63
  StrandUpdate,
64
+ SynchronizationUnitQuery,
64
65
  SyncStatus,
65
66
  type CreateDocumentInput,
66
67
  type DriveInput,
@@ -87,7 +88,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
87
88
  DocumentDriveState['id'],
88
89
  Map<Trigger['id'], CancelPullLoop>
89
90
  >();
90
- private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
91
+ private syncStatus = new Map<string, SyncStatus>();
91
92
 
92
93
  private queueManager: IQueueManager;
93
94
 
@@ -186,6 +187,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
186
187
  const drive = await this.getDrive(driveId);
187
188
  let driveTriggers = this.triggerMap.get(driveId);
188
189
 
190
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
191
+
189
192
  for (const trigger of drive.state.local.triggers) {
190
193
  if (driveTriggers?.get(trigger.id)) {
191
194
  continue;
@@ -196,6 +199,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
196
199
  }
197
200
 
198
201
  this.updateSyncStatus(driveId, 'SYNCING');
202
+
203
+ for (const syncUnit of syncUnits) {
204
+ this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
205
+ }
206
+
199
207
  if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
200
208
  const cancelPullLoop = PullResponderTransmitter.setupPull(
201
209
  driveId,
@@ -211,12 +219,31 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
211
219
  );
212
220
  },
213
221
  revisions => {
214
- const errorRevision = revisions.find(
222
+ const errorRevision = revisions.filter(
215
223
  r => r.status !== 'SUCCESS'
216
224
  );
217
- if (!errorRevision) {
225
+ if (errorRevision.length < 1) {
218
226
  this.updateSyncStatus(driveId, 'SUCCESS');
219
227
  }
228
+
229
+ for (const syncUnit of syncUnits) {
230
+ const fileErrorRevision = errorRevision.find(
231
+ r => r.documentId === syncUnit.documentId
232
+ );
233
+
234
+ if (fileErrorRevision) {
235
+ this.updateSyncStatus(
236
+ syncUnit.syncId,
237
+ fileErrorRevision.status,
238
+ fileErrorRevision.error
239
+ );
240
+ } else {
241
+ this.updateSyncStatus(
242
+ syncUnit.syncId,
243
+ 'SUCCESS'
244
+ );
245
+ }
246
+ }
220
247
  }
221
248
  );
222
249
  driveTriggers.set(trigger.id, cancelPullLoop);
@@ -226,9 +253,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
226
253
  }
227
254
 
228
255
  private async stopSyncRemoteDrive(driveId: string) {
256
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
257
+ const fileNodes = syncUnits
258
+ .filter(syncUnit => syncUnit.documentId !== '')
259
+ .map(syncUnit => syncUnit.documentId);
260
+
229
261
  const triggers = this.triggerMap.get(driveId);
230
262
  triggers?.forEach(cancel => cancel());
231
263
  this.updateSyncStatus(driveId, null);
264
+
265
+ for (const fileNode of fileNodes) {
266
+ this.updateSyncStatus(fileNode, null);
267
+ }
232
268
  return this.triggerMap.delete(driveId);
233
269
  }
234
270
 
@@ -303,6 +339,34 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
303
339
  ) {
304
340
  const drive = await this.getDrive(driveId);
305
341
 
342
+ const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType, drive);
343
+ const revisions = await this.storage.getSynchronizationUnitsRevision(synchronizationUnitsQuery);
344
+
345
+ const synchronizationUnits: SynchronizationUnit[] = synchronizationUnitsQuery.map(s => ({ ...s, lastUpdated: drive.created, revision: -1 }));
346
+ for (const revision of revisions) {
347
+ const syncUnit = synchronizationUnits.find(s =>
348
+ revision.driveId === s.driveId &&
349
+ revision.documentId === s.documentId &&
350
+ revision.scope === s.scope &&
351
+ revision.branch === s.branch
352
+ );
353
+ if (syncUnit) {
354
+ syncUnit.revision = revision.revision;
355
+ syncUnit.lastUpdated = revision.lastUpdated
356
+ }
357
+ }
358
+ return synchronizationUnits;
359
+ }
360
+
361
+ public async getSynchronizationUnitsIds(
362
+ driveId: string,
363
+ documentId?: string[],
364
+ scope?: string[],
365
+ branch?: string[],
366
+ documentType?: string[],
367
+ loadedDrive?: DocumentDriveDocument
368
+ ): Promise<SynchronizationUnitQuery[]> {
369
+ const drive = loadedDrive ?? await this.getDrive(driveId);
306
370
  const nodes = drive.state.global.nodes.filter(
307
371
  node =>
308
372
  isFileNode(node) &&
@@ -336,8 +400,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
336
400
  });
337
401
  }
338
402
 
339
- const synchronizationUnits: SynchronizationUnit[] = [];
340
-
403
+ const synchronizationUnitsQuery: Omit<SynchronizationUnit, "revision" | "lastUpdated">[] = [];
341
404
  for (const node of nodes) {
342
405
  const nodeUnits =
343
406
  scope?.length || branch?.length
@@ -354,35 +417,22 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
354
417
  if (!nodeUnits.length) {
355
418
  continue;
356
419
  }
357
-
358
- const document = await (node.id
359
- ? this.getDocument(driveId, node.id)
360
- : this.getDrive(driveId));
361
-
362
- for (const { syncId, scope, branch } of nodeUnits) {
363
- const operations =
364
- document.operations[scope as OperationScope] ?? [];
365
- const lastOperation = operations[operations.length - 1];
366
- synchronizationUnits.push({
367
- syncId,
368
- scope,
369
- branch,
370
- driveId,
371
- documentId: node.id,
372
- documentType: node.documentType,
373
- lastUpdated:
374
- lastOperation?.timestamp ?? document.lastModified,
375
- revision: lastOperation?.index ?? 0
376
- });
377
- }
420
+ synchronizationUnitsQuery.push(...nodeUnits.map(n => ({
421
+ driveId,
422
+ documentId: node.id,
423
+ syncId: n.syncId,
424
+ documentType: node.documentType,
425
+ scope: n.scope,
426
+ branch: n.branch
427
+ })));
378
428
  }
379
- return synchronizationUnits;
429
+ return synchronizationUnitsQuery;
380
430
  }
381
431
 
382
- public async getSynchronizationUnit(
432
+ public async getSynchronizationUnitIdInfo(
383
433
  driveId: string,
384
434
  syncId: string
385
- ): Promise<SynchronizationUnit> {
435
+ ): Promise<Omit<SynchronizationUnit, "revision" | "lastUpdated"> | undefined> {
386
436
  const drive = await this.getDrive(driveId);
387
437
  const node = drive.state.global.nodes.find(
388
438
  node =>
@@ -391,14 +441,40 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
391
441
  );
392
442
 
393
443
  if (!node || !isFileNode(node)) {
394
- throw new Error('Synchronization unit not found');
444
+ return undefined;
395
445
  }
396
446
 
397
- const { scope, branch } = node.synchronizationUnits.find(
447
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
448
+ const syncUnit = node.synchronizationUnits.find(
398
449
  unit => unit.syncId === syncId
399
- )!;
450
+ );
451
+ if (!syncUnit) {
452
+ return undefined;
453
+ }
454
+
455
+ return {
456
+ syncId,
457
+ scope: syncUnit.scope,
458
+ branch: syncUnit.branch,
459
+ driveId,
460
+ documentId: node.id,
461
+ documentType: node.documentType,
462
+ };
463
+ }
400
464
 
401
- const documentId = node.id;
465
+ public async getSynchronizationUnit(
466
+ driveId: string,
467
+ syncId: string
468
+ ): Promise<SynchronizationUnit | undefined> {
469
+ const syncUnit = await this.getSynchronizationUnitIdInfo(driveId, syncId);
470
+
471
+ if (!syncUnit) {
472
+ return undefined;
473
+ }
474
+
475
+ const { scope, branch, documentId, documentType } = syncUnit;
476
+
477
+ // TODO: REPLACE WITH GET DOCUMENT OPERATIONS
402
478
  const document = await this.getDocument(driveId, documentId);
403
479
  const operations = document.operations[scope as OperationScope] ?? [];
404
480
  const lastOperation = operations[operations.length - 1];
@@ -409,7 +485,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
409
485
  branch,
410
486
  driveId,
411
487
  documentId,
412
- documentType: node.documentType,
488
+ documentType,
413
489
  lastUpdated: lastOperation?.timestamp ?? document.lastModified,
414
490
  revision: lastOperation?.index ?? 0
415
491
  };
@@ -423,17 +499,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
423
499
  fromRevision?: number | undefined;
424
500
  }
425
501
  ): Promise<OperationUpdate[]> {
426
- const { documentId, scope } =
502
+ const syncUnit =
427
503
  syncId === '0'
428
504
  ? { documentId: '', scope: 'global' }
429
- : await this.getSynchronizationUnit(driveId, syncId);
505
+ : await this.getSynchronizationUnitIdInfo(driveId, syncId);
506
+
507
+ if (!syncUnit) {
508
+ throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
509
+ }
430
510
 
431
511
  const document =
432
512
  syncId === '0'
433
513
  ? await this.getDrive(driveId)
434
- : await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
514
+ : await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
435
515
 
436
- const operations = document.operations[scope as OperationScope] ?? []; // TODO filter by branch also
516
+ const operations = document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
437
517
  const filteredOperations = operations.filter(
438
518
  operation =>
439
519
  Object.keys(filter).length === 0 ||
@@ -650,7 +730,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
650
730
 
651
731
  async deleteDocument(driveId: string, id: string) {
652
732
  try {
653
- const syncUnits = await this.getSynchronizationUnits(driveId, [id]);
733
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
654
734
  await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
655
735
  } catch (error) {
656
736
  logger.warn('Error deleting document', error);
@@ -1075,21 +1155,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1075
1155
  .updateSynchronizationRevisions(
1076
1156
  drive,
1077
1157
  syncUnits,
1078
- () => this.updateSyncStatus(drive, 'SYNCING'),
1158
+ () => {
1159
+ this.updateSyncStatus(drive, 'SYNCING');
1160
+
1161
+ for (const syncUnit of syncUnits) {
1162
+ this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
1163
+ }
1164
+ },
1079
1165
  this.handleListenerError.bind(this),
1080
1166
  forceSync
1081
1167
  )
1082
- .then(
1083
- updates =>
1084
- updates.length &&
1085
- this.updateSyncStatus(drive, 'SUCCESS')
1086
- )
1168
+ .then(updates => {
1169
+ updates.length && this.updateSyncStatus(drive, 'SUCCESS');
1170
+
1171
+ for (const syncUnit of syncUnits) {
1172
+ this.updateSyncStatus(syncUnit.syncId, 'SUCCESS');
1173
+ }
1174
+ })
1087
1175
  .catch(error => {
1088
1176
  logger.error(
1089
1177
  'Non handled error updating sync revision',
1090
1178
  error
1091
1179
  );
1092
1180
  this.updateSyncStatus(drive, 'ERROR', error as Error);
1181
+
1182
+ for (const syncUnit of syncUnits) {
1183
+ this.updateSyncStatus(
1184
+ syncUnit.syncId,
1185
+ 'ERROR',
1186
+ error as Error
1187
+ );
1188
+ }
1093
1189
  });
1094
1190
 
1095
1191
  // after applying all the valid operations,throws
@@ -1208,6 +1304,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1208
1304
  const signals: SignalResult[] = [];
1209
1305
  let error: Error | undefined;
1210
1306
 
1307
+ const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
1308
+
1211
1309
  try {
1212
1310
  await this._addDriveOperations(drive, async documentStorage => {
1213
1311
  const result = await this._processOperations<
@@ -1247,6 +1345,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1247
1345
  }
1248
1346
  }
1249
1347
 
1348
+ const syncUnits = await this.getSynchronizationUnitsIds(drive);
1349
+
1350
+ const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
1351
+ const syncUnitsIds = syncUnits.map(unit => unit.syncId);
1352
+
1353
+ const newSyncUnits = syncUnitsIds.filter(
1354
+ syncUnitId => !prevSyncUnitsIds.includes(syncUnitId)
1355
+ );
1356
+
1357
+ const removedSyncUnits = prevSyncUnitsIds.filter(
1358
+ syncUnitId => !syncUnitsIds.includes(syncUnitId)
1359
+ );
1360
+
1250
1361
  // update listener cache
1251
1362
  const lastOperation = operationsApplied
1252
1363
  .filter(op => op.scope === 'global')
@@ -1268,21 +1379,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1268
1379
  revision: lastOperation.index
1269
1380
  }
1270
1381
  ],
1271
- () => this.updateSyncStatus(drive, 'SYNCING'),
1382
+ () => {
1383
+ this.updateSyncStatus(drive, 'SYNCING');
1384
+
1385
+ for (const syncUnitId of [
1386
+ ...newSyncUnits,
1387
+ ...removedSyncUnits
1388
+ ]) {
1389
+ this.updateSyncStatus(syncUnitId, 'SYNCING');
1390
+ }
1391
+ },
1272
1392
  this.handleListenerError.bind(this),
1273
1393
  forceSync
1274
1394
  )
1275
- .then(
1276
- updates =>
1277
- updates.length &&
1278
- this.updateSyncStatus(drive, 'SUCCESS')
1279
- )
1395
+ .then(updates => {
1396
+ if (updates.length) {
1397
+ this.updateSyncStatus(drive, 'SUCCESS');
1398
+
1399
+ for (const syncUnitId of newSyncUnits) {
1400
+ this.updateSyncStatus(syncUnitId, 'SUCCESS');
1401
+ }
1402
+
1403
+ for (const syncUnitId of removedSyncUnits) {
1404
+ this.updateSyncStatus(syncUnitId, null);
1405
+ }
1406
+ }
1407
+ })
1280
1408
  .catch(error => {
1281
1409
  logger.error(
1282
1410
  'Non handled error updating sync revision',
1283
1411
  error
1284
1412
  );
1285
1413
  this.updateSyncStatus(drive, 'ERROR', error as Error);
1414
+
1415
+ for (const syncUnitId of [
1416
+ ...newSyncUnits,
1417
+ ...removedSyncUnits
1418
+ ]) {
1419
+ this.updateSyncStatus(
1420
+ syncUnitId,
1421
+ 'ERROR',
1422
+ error as Error
1423
+ );
1424
+ }
1286
1425
  });
1287
1426
  }
1288
1427
 
@@ -118,7 +118,7 @@ export class ListenerManager extends BaseListenerManager {
118
118
  return driveMap.delete(listenerId);
119
119
  }
120
120
 
121
- async removeSyncUnits(driveId: string, syncUnits: SynchronizationUnit[]) {
121
+ async removeSyncUnits(driveId: string, syncUnits: Pick<SynchronizationUnit, "syncId">[]) {
122
122
  const listeners = this.listenerState.get(driveId);
123
123
  if (!listeners) {
124
124
  return;
@@ -243,7 +243,7 @@ export class ListenerManager extends BaseListenerManager {
243
243
 
244
244
  const opData: OperationUpdate[] = [];
245
245
  try {
246
- const data = await this.drive.getOperationData(
246
+ const data = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
247
247
  driveId,
248
248
  syncUnit.syncId,
249
249
  {
@@ -373,6 +373,21 @@ export class ListenerManager extends BaseListenerManager {
373
373
  );
374
374
  }
375
375
 
376
+ getListenerSyncUnitIds(driveId: string, listenerId: string) {
377
+ const listener = this.listenerState.get(driveId)?.get(listenerId);
378
+ if (!listener) {
379
+ return [];
380
+ }
381
+ const filter = listener.listener.filter;
382
+ return this.drive.getSynchronizationUnitsIds(
383
+ driveId,
384
+ filter.documentId ?? ['*'],
385
+ filter.scope ?? ['*'],
386
+ filter.branch ?? ['*'],
387
+ filter.documentType ?? ['*']
388
+ );
389
+ }
390
+
376
391
  async initDrive(drive: DocumentDriveDocument) {
377
392
  const {
378
393
  state: {
@@ -420,32 +435,40 @@ export class ListenerManager extends BaseListenerManager {
420
435
  const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
421
436
 
422
437
  for (const syncUnit of syncUnits) {
438
+ if (syncUnit.revision < 0) {
439
+ continue;
440
+ }
423
441
  const entry = listener.syncUnits.get(syncUnit.syncId);
424
442
  if (entry && entry.listenerRev >= syncUnit.revision) {
425
443
  continue;
426
444
  }
427
445
 
428
446
  const { documentId, driveId, scope, branch } = syncUnit;
429
- const operations = await this.drive.getOperationData(
430
- driveId,
431
- syncUnit.syncId,
432
- {
433
- since,
434
- fromRevision: entry?.listenerRev
447
+ try {
448
+ const operations = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
449
+ driveId,
450
+ syncUnit.syncId,
451
+ {
452
+ since,
453
+ fromRevision: entry?.listenerRev
454
+ }
455
+ );
456
+
457
+ if (!operations.length) {
458
+ continue;
435
459
  }
436
- );
437
460
 
438
- if (!operations.length) {
461
+ strands.push({
462
+ driveId,
463
+ documentId,
464
+ scope: scope as OperationScope,
465
+ branch,
466
+ operations
467
+ });
468
+ } catch (error) {
469
+ logger.error(error);
439
470
  continue;
440
471
  }
441
-
442
- strands.push({
443
- driveId,
444
- documentId,
445
- scope: scope as OperationScope,
446
- branch,
447
- operations
448
- });
449
472
  }
450
473
 
451
474
  return strands;
@@ -72,7 +72,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
72
72
  listenerId: string,
73
73
  revisions: ListenerRevision[]
74
74
  ): Promise<boolean> {
75
- const syncUnits = await this.manager.getListenerSyncUnits(
75
+ const syncUnits = await this.manager.getListenerSyncUnitIds(
76
76
  driveId,
77
77
  listenerId
78
78
  );
@@ -59,6 +59,8 @@ export type SynchronizationUnit = {
59
59
  revision: number;
60
60
  };
61
61
 
62
+ export type SynchronizationUnitQuery = Omit<SynchronizationUnit, "revision" | "lastUpdated">;
63
+
62
64
  export type Listener = {
63
65
  driveId: string;
64
66
  listenerId: string;
@@ -286,7 +288,15 @@ export abstract class BaseDocumentDriveServer {
286
288
  abstract getSynchronizationUnit(
287
289
  driveId: string,
288
290
  syncId: string
289
- ): Promise<SynchronizationUnit>;
291
+ ): Promise<SynchronizationUnit | undefined>;
292
+
293
+ abstract getSynchronizationUnitsIds(
294
+ driveId: string,
295
+ documentId?: string[],
296
+ scope?: string[],
297
+ branch?: string[],
298
+ documentType?: string[]
299
+ ): Promise<SynchronizationUnitQuery[]>;
290
300
 
291
301
  abstract getOperationData(
292
302
  driveId: string,
@@ -3,10 +3,15 @@ import {
3
3
  BaseAction,
4
4
  Document,
5
5
  DocumentHeader,
6
- Operation
6
+ Operation,
7
+ OperationScope
7
8
  } from 'document-model/document';
8
- import { mergeOperations } from '..';
9
- import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
9
+ import { mergeOperations, type SynchronizationUnitQuery } from '..';
10
+ import {
11
+ DocumentDriveStorage,
12
+ DocumentStorage,
13
+ IDriveStorage,
14
+ } from './types';
10
15
 
11
16
  export class BrowserStorage implements IDriveStorage {
12
17
  private db: Promise<LocalForage>;
@@ -18,7 +23,9 @@ export class BrowserStorage implements IDriveStorage {
18
23
  constructor(namespace?: string) {
19
24
  this.db = import('localforage').then(localForage =>
20
25
  localForage.default.createInstance({
21
- name: namespace ? `${namespace}:${BrowserStorage.DBName}` : BrowserStorage.DBName
26
+ name: namespace
27
+ ? `${namespace}:${BrowserStorage.DBName}`
28
+ : BrowserStorage.DBName
22
29
  })
23
30
  );
24
31
  }
@@ -68,7 +75,7 @@ export class BrowserStorage implements IDriveStorage {
68
75
  drive: string,
69
76
  id: string,
70
77
  operations: Operation[],
71
- header: DocumentHeader,
78
+ header: DocumentHeader
72
79
  ): Promise<void> {
73
80
  const document = await this.getDocument(drive, id);
74
81
  if (!document) {
@@ -154,4 +161,61 @@ export class BrowserStorage implements IDriveStorage {
154
161
  });
155
162
  return;
156
163
  }
164
+
165
+ async getSynchronizationUnitsRevision(
166
+ units: SynchronizationUnitQuery[]
167
+ ): Promise<
168
+ {
169
+ driveId: string;
170
+ documentId: string;
171
+ scope: string;
172
+ branch: string;
173
+ lastUpdated: string;
174
+ revision: number;
175
+ }[]
176
+ > {
177
+ const results = await Promise.allSettled(
178
+ units.map(async unit => {
179
+ try {
180
+ const document = await (unit.documentId
181
+ ? this.getDocument(unit.driveId, unit.documentId)
182
+ : this.getDrive(unit.driveId));
183
+ if (!document) {
184
+ return undefined;
185
+ }
186
+ const operation =
187
+ document.operations[unit.scope as OperationScope]?.at(
188
+ -1
189
+ );
190
+ if (operation) {
191
+ return {
192
+ driveId: unit.driveId,
193
+ documentId: unit.documentId,
194
+ scope: unit.scope,
195
+ branch: unit.branch,
196
+ lastUpdated: operation.timestamp,
197
+ revision: operation.index
198
+ };
199
+ }
200
+ } catch {
201
+ return undefined;
202
+ }
203
+ })
204
+ );
205
+ return results.reduce<
206
+ {
207
+ driveId: string;
208
+ documentId: string;
209
+ scope: string;
210
+ branch: string;
211
+ lastUpdated: string;
212
+ revision: number;
213
+ }[]
214
+ >((acc, curr) => {
215
+ if (curr.status === 'fulfilled' && curr.value !== undefined) {
216
+ acc.push(curr.value);
217
+ }
218
+ return acc;
219
+ }, []);
220
+ }
157
221
  }
@@ -1,5 +1,5 @@
1
1
  import { DocumentDriveAction } from 'document-model-libs/document-drive';
2
- import { BaseAction, DocumentHeader, Operation } from 'document-model/document';
2
+ import { BaseAction, DocumentHeader, Operation, OperationScope } from 'document-model/document';
3
3
  import type { Dirent } from 'fs';
4
4
  import {
5
5
  existsSync,
@@ -12,7 +12,8 @@ import fs from 'fs/promises';
12
12
  import stringify from 'json-stringify-deterministic';
13
13
  import path from 'path';
14
14
  import sanitize from 'sanitize-filename';
15
- import { mergeOperations } from '..';
15
+ import type { SynchronizationUnitQuery } from '../server/types';
16
+ import { mergeOperations } from '../utils';
16
17
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
17
18
 
18
19
  type FSError = {
@@ -235,4 +236,61 @@ export class FilesystemStorage implements IDriveStorage {
235
236
  operations: mergedOperations
236
237
  });
237
238
  }
239
+
240
+ async getSynchronizationUnitsRevision(
241
+ units: SynchronizationUnitQuery[]
242
+ ): Promise<
243
+ {
244
+ driveId: string;
245
+ documentId: string;
246
+ scope: string;
247
+ branch: string;
248
+ lastUpdated: string;
249
+ revision: number;
250
+ }[]
251
+ > {
252
+ const results = await Promise.allSettled(
253
+ units.map(async unit => {
254
+ try {
255
+ const document = await (unit.documentId
256
+ ? this.getDocument(unit.driveId, unit.documentId)
257
+ : this.getDrive(unit.driveId));
258
+ if (!document) {
259
+ return undefined;
260
+ }
261
+ const operation =
262
+ document.operations[unit.scope as OperationScope]?.at(
263
+ -1
264
+ );
265
+ if (operation) {
266
+ return {
267
+ driveId: unit.driveId,
268
+ documentId: unit.documentId,
269
+ scope: unit.scope,
270
+ branch: unit.branch,
271
+ lastUpdated: operation.timestamp,
272
+ revision: operation.index
273
+ };
274
+ }
275
+ } catch {
276
+ return undefined;
277
+ }
278
+ })
279
+ );
280
+ return results.reduce<
281
+ {
282
+ driveId: string;
283
+ documentId: string;
284
+ scope: string;
285
+ branch: string;
286
+ lastUpdated: string;
287
+ revision: number;
288
+ }[]
289
+ >((acc, curr) => {
290
+ if (curr.status === 'fulfilled' && curr.value !== undefined) {
291
+ acc.push(curr.value);
292
+ }
293
+ return acc;
294
+ }, []);
295
+ }
238
296
  }
@@ -3,10 +3,16 @@ import {
3
3
  BaseAction,
4
4
  Document,
5
5
  DocumentHeader,
6
- Operation
6
+ Operation,
7
+ OperationScope
7
8
  } from 'document-model/document';
8
- import { mergeOperations } from '..';
9
- import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
9
+ import {
10
+ DocumentDriveStorage,
11
+ DocumentStorage,
12
+ IDriveStorage,
13
+ } from './types';
14
+ import type { SynchronizationUnitQuery } from '../server/types';
15
+ import { mergeOperations } from '../utils';
10
16
 
11
17
  export class MemoryStorage implements IDriveStorage {
12
18
  private documents: Record<string, Record<string, DocumentStorage>>;
@@ -19,7 +25,7 @@ export class MemoryStorage implements IDriveStorage {
19
25
  }
20
26
 
21
27
  checkDocumentExists(drive: string, id: string): Promise<boolean> {
22
- return Promise.resolve(this.documents[drive]?.[id] !== undefined)
28
+ return Promise.resolve(this.documents[drive]?.[id] !== undefined);
23
29
  }
24
30
 
25
31
  async getDocuments(drive: string) {
@@ -153,4 +159,61 @@ export class MemoryStorage implements IDriveStorage {
153
159
  delete this.documents[id];
154
160
  delete this.drives[id];
155
161
  }
162
+
163
+ async getSynchronizationUnitsRevision(
164
+ units: SynchronizationUnitQuery[]
165
+ ): Promise<
166
+ {
167
+ driveId: string;
168
+ documentId: string;
169
+ scope: string;
170
+ branch: string;
171
+ lastUpdated: string;
172
+ revision: number;
173
+ }[]
174
+ > {
175
+ const results = await Promise.allSettled(
176
+ units.map(async unit => {
177
+ try {
178
+ const document = await (unit.documentId
179
+ ? this.getDocument(unit.driveId, unit.documentId)
180
+ : this.getDrive(unit.driveId));
181
+ if (!document) {
182
+ return undefined;
183
+ }
184
+ const operation =
185
+ document.operations[unit.scope as OperationScope]?.at(
186
+ -1
187
+ );
188
+ if (operation) {
189
+ return {
190
+ driveId: unit.driveId,
191
+ documentId: unit.documentId,
192
+ scope: unit.scope,
193
+ branch: unit.branch,
194
+ lastUpdated: operation.timestamp,
195
+ revision: operation.index
196
+ };
197
+ }
198
+ } catch {
199
+ return undefined;
200
+ }
201
+ })
202
+ );
203
+ return results.reduce<
204
+ {
205
+ driveId: string;
206
+ documentId: string;
207
+ scope: string;
208
+ branch: string;
209
+ lastUpdated: string;
210
+ revision: number;
211
+ }[]
212
+ >((acc, curr) => {
213
+ if (curr.status === 'fulfilled' && curr.value !== undefined) {
214
+ acc.push(curr.value);
215
+ }
216
+ return acc;
217
+ }, []);
218
+ }
156
219
  }
@@ -22,6 +22,7 @@ import { IBackOffOptions, backOff } from 'exponential-backoff';
22
22
  import { ConflictOperationError } from '../server/error';
23
23
  import { logger } from '../utils/logger';
24
24
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage, IStorageDelegate } from './types';
25
+ import type { SynchronizationUnitQuery } from '../server/types';
25
26
 
26
27
  type Transaction =
27
28
  | Omit<
@@ -552,4 +553,37 @@ export class PrismaStorage implements IDriveStorage {
552
553
  getDriveOperationResultingState(drive: string, index: number, scope: string, branch: string): Promise<unknown> {
553
554
  return this.getOperationResultingState("drives", drive, index, scope, branch);
554
555
  }
556
+
557
+ async getSynchronizationUnitsRevision(
558
+ units: SynchronizationUnitQuery[]
559
+ ): Promise<
560
+ {
561
+ driveId: string;
562
+ documentId: string;
563
+ scope: string;
564
+ branch: string;
565
+ lastUpdated: string;
566
+ revision: number;
567
+ }[]
568
+ > {
569
+ // TODO add branch condition
570
+ const whereClauses = units.map((_, index) => {
571
+ return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
572
+ }).join(' OR ');
573
+
574
+ const query = `
575
+ SELECT "driveId", "documentId", "scope", "branch", MAX("timestamp") as "lastUpdated", MAX("index") as revision FROM "Operation"
576
+ WHERE ${whereClauses}
577
+ GROUP BY "driveId", "documentId", "scope", "branch"
578
+ `;
579
+
580
+ const params = units.map(unit => [unit.documentId ? unit.driveId : "drives", unit.documentId || unit.driveId, unit.scope]).flat();
581
+ const results = await this.db.$queryRawUnsafe<{ driveId: string, documentId: string, lastUpdated: string, scope: OperationScope, branch: string, revision: number }[]>(query, ...params);
582
+ return results.map(row => ({
583
+ ...row,
584
+ driveId: row.driveId === "drives" ? row.documentId : row.driveId,
585
+ documentId: row.driveId === "drives" ? '' : row.documentId,
586
+ lastUpdated: new Date(row.lastUpdated).toISOString(),
587
+ }));
588
+ }
555
589
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from 'document-model/document';
12
12
  import { DataTypes, Options, Sequelize } from 'sequelize';
13
13
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
14
+ import type { SynchronizationUnitQuery } from '../server/types';
14
15
 
15
16
  export class SequelizeStorage implements IDriveStorage {
16
17
  private db: Sequelize;
@@ -448,4 +449,61 @@ export class SequelizeStorage implements IDriveStorage {
448
449
  });
449
450
  }
450
451
  }
452
+
453
+ async getSynchronizationUnitsRevision(
454
+ units: SynchronizationUnitQuery[]
455
+ ): Promise<
456
+ {
457
+ driveId: string;
458
+ documentId: string;
459
+ scope: string;
460
+ branch: string;
461
+ lastUpdated: string;
462
+ revision: number;
463
+ }[]
464
+ > {
465
+ const results = await Promise.allSettled(
466
+ units.map(async unit => {
467
+ try {
468
+ const document = await (unit.documentId
469
+ ? this.getDocument(unit.driveId, unit.documentId)
470
+ : this.getDrive(unit.driveId));
471
+ if (!document) {
472
+ return undefined;
473
+ }
474
+ const operation =
475
+ document.operations[unit.scope as OperationScope]?.at(
476
+ -1
477
+ );
478
+ if (operation) {
479
+ return {
480
+ driveId: unit.driveId,
481
+ documentId: unit.documentId,
482
+ scope: unit.scope,
483
+ branch: unit.branch,
484
+ lastUpdated: operation.timestamp,
485
+ revision: operation.index
486
+ };
487
+ }
488
+ } catch {
489
+ return undefined;
490
+ }
491
+ })
492
+ );
493
+ return results.reduce<
494
+ {
495
+ driveId: string;
496
+ documentId: string;
497
+ scope: string;
498
+ branch: string;
499
+ lastUpdated: string;
500
+ revision: number;
501
+ }[]
502
+ >((acc, curr) => {
503
+ if (curr.status === 'fulfilled' && curr.value !== undefined) {
504
+ acc.push(curr.value);
505
+ }
506
+ return acc;
507
+ }, []);
508
+ }
451
509
  }
@@ -10,6 +10,7 @@ import type {
10
10
  DocumentOperations,
11
11
  Operation,
12
12
  } from 'document-model/document';
13
+ import type { SynchronizationUnitQuery } from '../server/types';
13
14
 
14
15
  export type DocumentStorage<D extends Document = Document> = Omit<
15
16
  D,
@@ -48,6 +49,14 @@ export interface IStorage {
48
49
  deleteDocument(drive: string, id: string): Promise<void>;
49
50
  getOperationResultingState?(drive: string, id: string, index: number, scope: string, branch: string): Promise<unknown>;
50
51
  setStorageDelegate?(delegate: IStorageDelegate): void;
52
+ getSynchronizationUnitsRevision(units: SynchronizationUnitQuery[]): Promise<{
53
+ driveId: string;
54
+ documentId: string;
55
+ scope: string;
56
+ branch: string;
57
+ lastUpdated: string;
58
+ revision: number;
59
+ }[]>
51
60
  }
52
61
  export interface IDriveStorage extends IStorage {
53
62
  getDrives(): Promise<string[]>;
@@ -69,4 +78,4 @@ export interface IDriveStorage extends IStorage {
69
78
  }>
70
79
  ): Promise<void>;
71
80
  getDriveOperationResultingState?(drive: string, index: number, scope: string, branch: string): Promise<unknown>;
72
- }
81
+ }