document-drive 1.0.0-alpha.72 → 1.0.0-alpha.74

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.74",
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<SynchronizationUnitQuery | 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
+ }
435
454
 
436
- const documentId = node.id;
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
+ }
474
+
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 ||
@@ -560,9 +605,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
560
605
  }
561
606
 
562
607
  async deleteDrive(id: string) {
563
- this.stopSyncRemoteDrive(id);
564
- await this.cache.deleteDocument('drives', id);
565
- return this.storage.deleteDrive(id);
608
+ const result = await Promise.allSettled([
609
+ this.stopSyncRemoteDrive(id),
610
+ this.listenerStateManager.removeDrive(id),
611
+ this.cache.deleteDocument('drives', id),
612
+ this.storage.deleteDrive(id)
613
+ ]);
614
+
615
+ result.forEach(r => {
616
+ if (r.status === "rejected") {
617
+ throw r.reason;
618
+ }
619
+ });
566
620
  }
567
621
 
568
622
  getDrives() {
@@ -685,7 +739,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
685
739
 
686
740
  async deleteDocument(driveId: string, id: string) {
687
741
  try {
688
- const syncUnits = await this.getSynchronizationUnits(driveId, [id]);
742
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
689
743
  await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
690
744
  } catch (error) {
691
745
  logger.warn('Error deleting document', error);
@@ -1259,7 +1313,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1259
1313
  const signals: SignalResult[] = [];
1260
1314
  let error: Error | undefined;
1261
1315
 
1262
- const prevSyncUnits = await this.getSynchronizationUnits(drive);
1316
+ const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
1263
1317
 
1264
1318
  try {
1265
1319
  await this._addDriveOperations(drive, async documentStorage => {
@@ -1300,7 +1354,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1300
1354
  }
1301
1355
  }
1302
1356
 
1303
- const syncUnits = await this.getSynchronizationUnits(drive);
1357
+ const syncUnits = await this.getSynchronizationUnitsIds(drive);
1304
1358
 
1305
1359
  const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
1306
1360
  const syncUnitsIds = syncUnits.map(unit => unit.syncId);
@@ -53,7 +53,7 @@ export class ListenerManager extends BaseListenerManager {
53
53
  driveId: string,
54
54
  listenerId: string
55
55
  ): Promise<ITransmitter | undefined> {
56
- return this.transmitters[driveId]?.[listenerId];
56
+ return Promise.resolve(this.transmitters[driveId]?.[listenerId]);
57
57
  }
58
58
 
59
59
  async addListener(listener: Listener) {
@@ -106,7 +106,7 @@ export class ListenerManager extends BaseListenerManager {
106
106
  const driveTransmitters = this.transmitters[drive] || {};
107
107
  driveTransmitters[listener.listenerId] = transmitter;
108
108
  this.transmitters[drive] = driveTransmitters;
109
- return transmitter;
109
+ return Promise.resolve(transmitter);
110
110
  }
111
111
 
112
112
  async removeListener(driveId: string, listenerId: string) {
@@ -115,10 +115,10 @@ export class ListenerManager extends BaseListenerManager {
115
115
  return false;
116
116
  }
117
117
 
118
- return driveMap.delete(listenerId);
118
+ return Promise.resolve(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;
@@ -128,6 +128,7 @@ export class ListenerManager extends BaseListenerManager {
128
128
  listener.syncUnits.delete(syncUnit.syncId);
129
129
  }
130
130
  }
131
+ return Promise.resolve();
131
132
  }
132
133
 
133
134
  async updateSynchronizationRevisions(
@@ -203,6 +204,8 @@ export class ListenerManager extends BaseListenerManager {
203
204
  } else {
204
205
  listener.syncUnits.set(syncId, { listenerRev, lastUpdated });
205
206
  }
207
+
208
+ return Promise.resolve();
206
209
  }
207
210
 
208
211
  triggerUpdate = debounce(
@@ -243,7 +246,7 @@ export class ListenerManager extends BaseListenerManager {
243
246
 
244
247
  const opData: OperationUpdate[] = [];
245
248
  try {
246
- const data = await this.drive.getOperationData(
249
+ const data = await this.drive.getOperationData( // TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
247
250
  driveId,
248
251
  syncUnit.syncId,
249
252
  {
@@ -373,6 +376,21 @@ export class ListenerManager extends BaseListenerManager {
373
376
  );
374
377
  }
375
378
 
379
+ getListenerSyncUnitIds(driveId: string, listenerId: string) {
380
+ const listener = this.listenerState.get(driveId)?.get(listenerId);
381
+ if (!listener) {
382
+ return [];
383
+ }
384
+ const filter = listener.listener.filter;
385
+ return this.drive.getSynchronizationUnitsIds(
386
+ driveId,
387
+ filter.documentId ?? ['*'],
388
+ filter.scope ?? ['*'],
389
+ filter.branch ?? ['*'],
390
+ filter.documentType ?? ['*']
391
+ );
392
+ }
393
+
376
394
  async initDrive(drive: DocumentDriveDocument) {
377
395
  const {
378
396
  state: {
@@ -398,6 +416,14 @@ export class ListenerManager extends BaseListenerManager {
398
416
  }
399
417
  }
400
418
 
419
+ async removeDrive(driveId: string): Promise<void> {
420
+ this.listenerState.delete(driveId);
421
+ const transmitters = this.transmitters[driveId];
422
+ if (transmitters) {
423
+ await Promise.all(Object.values(transmitters).map(t => t.disconnect?.()));
424
+ }
425
+ }
426
+
401
427
  getListener(driveId: string, listenerId: string): Promise<ListenerState> {
402
428
  const drive = this.listenerState.get(driveId);
403
429
  if (!drive) throw new Error('Drive not found');
@@ -420,32 +446,40 @@ export class ListenerManager extends BaseListenerManager {
420
446
  const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
421
447
 
422
448
  for (const syncUnit of syncUnits) {
449
+ if (syncUnit.revision < 0) {
450
+ continue;
451
+ }
423
452
  const entry = listener.syncUnits.get(syncUnit.syncId);
424
453
  if (entry && entry.listenerRev >= syncUnit.revision) {
425
454
  continue;
426
455
  }
427
456
 
428
457
  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
458
+ try {
459
+ const operations = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
460
+ driveId,
461
+ syncUnit.syncId,
462
+ {
463
+ since,
464
+ fromRevision: entry?.listenerRev
465
+ }
466
+ );
467
+
468
+ if (!operations.length) {
469
+ continue;
435
470
  }
436
- );
437
471
 
438
- if (!operations.length) {
472
+ strands.push({
473
+ driveId,
474
+ documentId,
475
+ scope: scope as OperationScope,
476
+ branch,
477
+ operations
478
+ });
479
+ } catch (error) {
480
+ logger.error(error);
439
481
  continue;
440
482
  }
441
-
442
- strands.push({
443
- driveId,
444
- documentId,
445
- scope: scope as OperationScope,
446
- branch,
447
- operations
448
- });
449
483
  }
450
484
 
451
485
  return strands;
@@ -12,6 +12,7 @@ import { logger } from '../../../utils/logger';
12
12
 
13
13
  export interface IReceiver {
14
14
  transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
15
+ disconnect: () => Promise<void>;
15
16
  }
16
17
 
17
18
  export type InternalTransmitterUpdate<
@@ -88,4 +89,8 @@ export class InternalTransmitter implements ITransmitter {
88
89
  setReceiver(receiver: IReceiver) {
89
90
  this.receiver = receiver;
90
91
  }
92
+
93
+ async disconnect(): Promise<void> {
94
+ await this.receiver?.disconnect();
95
+ }
91
96
  }
@@ -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
  );
@@ -6,6 +6,7 @@ import { ListenerRevision, StrandUpdate } from '../..';
6
6
 
7
7
  export interface ITransmitter {
8
8
  transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]>;
9
+ disconnect?(): Promise<void>;
9
10
  }
10
11
 
11
12
  export interface InternalTransmitterService extends ITransmitter {
@@ -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,
@@ -341,6 +351,7 @@ export abstract class BaseListenerManager {
341
351
  }
342
352
 
343
353
  abstract initDrive(drive: DocumentDriveDocument): Promise<void>;
354
+ abstract removeDrive(driveId: DocumentDriveState["id"]): Promise<void>;
344
355
 
345
356
  abstract addListener(listener: Listener): Promise<ITransmitter>;
346
357
  abstract removeListener(
@@ -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
+ }