document-drive 1.0.0-alpha.72 → 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.72",
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,
@@ -186,7 +187,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
186
187
  const drive = await this.getDrive(driveId);
187
188
  let driveTriggers = this.triggerMap.get(driveId);
188
189
 
189
- const syncUnits = await this.getSynchronizationUnits(driveId);
190
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
190
191
 
191
192
  for (const trigger of drive.state.local.triggers) {
192
193
  if (driveTriggers?.get(trigger.id)) {
@@ -252,7 +253,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
252
253
  }
253
254
 
254
255
  private async stopSyncRemoteDrive(driveId: string) {
255
- const syncUnits = await this.getSynchronizationUnits(driveId);
256
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
256
257
  const fileNodes = syncUnits
257
258
  .filter(syncUnit => syncUnit.documentId !== '')
258
259
  .map(syncUnit => syncUnit.documentId);
@@ -338,6 +339,34 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
338
339
  ) {
339
340
  const drive = await this.getDrive(driveId);
340
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);
341
370
  const nodes = drive.state.global.nodes.filter(
342
371
  node =>
343
372
  isFileNode(node) &&
@@ -371,8 +400,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
371
400
  });
372
401
  }
373
402
 
374
- const synchronizationUnits: SynchronizationUnit[] = [];
375
-
403
+ const synchronizationUnitsQuery: Omit<SynchronizationUnit, "revision" | "lastUpdated">[] = [];
376
404
  for (const node of nodes) {
377
405
  const nodeUnits =
378
406
  scope?.length || branch?.length
@@ -389,35 +417,22 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
389
417
  if (!nodeUnits.length) {
390
418
  continue;
391
419
  }
392
-
393
- const document = await (node.id
394
- ? this.getDocument(driveId, node.id)
395
- : this.getDrive(driveId));
396
-
397
- for (const { syncId, scope, branch } of nodeUnits) {
398
- const operations =
399
- document.operations[scope as OperationScope] ?? [];
400
- const lastOperation = operations[operations.length - 1];
401
- synchronizationUnits.push({
402
- syncId,
403
- scope,
404
- branch,
405
- driveId,
406
- documentId: node.id,
407
- documentType: node.documentType,
408
- lastUpdated:
409
- lastOperation?.timestamp ?? document.lastModified,
410
- revision: lastOperation?.index ?? 0
411
- });
412
- }
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
+ })));
413
428
  }
414
- return synchronizationUnits;
429
+ return synchronizationUnitsQuery;
415
430
  }
416
431
 
417
- public async getSynchronizationUnit(
432
+ public async getSynchronizationUnitIdInfo(
418
433
  driveId: string,
419
434
  syncId: string
420
- ): Promise<SynchronizationUnit> {
435
+ ): Promise<Omit<SynchronizationUnit, "revision" | "lastUpdated"> | undefined> {
421
436
  const drive = await this.getDrive(driveId);
422
437
  const node = drive.state.global.nodes.find(
423
438
  node =>
@@ -426,14 +441,40 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
426
441
  );
427
442
 
428
443
  if (!node || !isFileNode(node)) {
429
- throw new Error('Synchronization unit not found');
444
+ return undefined;
430
445
  }
431
446
 
432
- const { scope, branch } = node.synchronizationUnits.find(
447
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
448
+ const syncUnit = node.synchronizationUnits.find(
433
449
  unit => unit.syncId === syncId
434
- )!;
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
+ }
464
+
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
+ }
435
474
 
436
- const documentId = node.id;
475
+ const { scope, branch, documentId, documentType } = syncUnit;
476
+
477
+ // TODO: REPLACE WITH GET DOCUMENT OPERATIONS
437
478
  const document = await this.getDocument(driveId, documentId);
438
479
  const operations = document.operations[scope as OperationScope] ?? [];
439
480
  const lastOperation = operations[operations.length - 1];
@@ -444,7 +485,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
444
485
  branch,
445
486
  driveId,
446
487
  documentId,
447
- documentType: node.documentType,
488
+ documentType,
448
489
  lastUpdated: lastOperation?.timestamp ?? document.lastModified,
449
490
  revision: lastOperation?.index ?? 0
450
491
  };
@@ -458,17 +499,21 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
458
499
  fromRevision?: number | undefined;
459
500
  }
460
501
  ): Promise<OperationUpdate[]> {
461
- const { documentId, scope } =
502
+ const syncUnit =
462
503
  syncId === '0'
463
504
  ? { documentId: '', scope: 'global' }
464
- : 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
+ }
465
510
 
466
511
  const document =
467
512
  syncId === '0'
468
513
  ? await this.getDrive(driveId)
469
- : await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
514
+ : await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
470
515
 
471
- 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
472
517
  const filteredOperations = operations.filter(
473
518
  operation =>
474
519
  Object.keys(filter).length === 0 ||
@@ -685,7 +730,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
685
730
 
686
731
  async deleteDocument(driveId: string, id: string) {
687
732
  try {
688
- const syncUnits = await this.getSynchronizationUnits(driveId, [id]);
733
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
689
734
  await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
690
735
  } catch (error) {
691
736
  logger.warn('Error deleting document', error);
@@ -1259,7 +1304,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1259
1304
  const signals: SignalResult[] = [];
1260
1305
  let error: Error | undefined;
1261
1306
 
1262
- const prevSyncUnits = await this.getSynchronizationUnits(drive);
1307
+ const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
1263
1308
 
1264
1309
  try {
1265
1310
  await this._addDriveOperations(drive, async documentStorage => {
@@ -1300,7 +1345,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1300
1345
  }
1301
1346
  }
1302
1347
 
1303
- const syncUnits = await this.getSynchronizationUnits(drive);
1348
+ const syncUnits = await this.getSynchronizationUnitsIds(drive);
1304
1349
 
1305
1350
  const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
1306
1351
  const syncUnitsIds = syncUnits.map(unit => unit.syncId);
@@ -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
+ }