document-drive 1.0.0-experimental.8 → 1.0.0-websockets
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/CHANGELOG.md +148 -0
- package/package.json +15 -10
- package/src/queue/base.ts +19 -34
- package/src/queue/redis.ts +15 -25
- package/src/queue/types.ts +32 -16
- package/src/server/index.ts +773 -197
- package/src/server/listener/manager.ts +60 -22
- package/src/server/listener/transmitter/index.ts +1 -0
- package/src/server/listener/transmitter/internal.ts +5 -0
- package/src/server/listener/transmitter/pull-responder.ts +11 -15
- package/src/server/listener/transmitter/subscription.ts +364 -0
- package/src/server/listener/transmitter/types.ts +15 -0
- package/src/server/types.ts +55 -6
- package/src/storage/browser.ts +69 -5
- package/src/storage/filesystem.ts +60 -2
- package/src/storage/memory.ts +67 -4
- package/src/storage/prisma.ts +316 -81
- package/src/storage/sequelize.ts +69 -7
- package/src/storage/types.ts +19 -5
- package/src/queue/manager.ts +0 -22
package/src/server/index.ts
CHANGED
|
@@ -18,18 +18,27 @@ import {
|
|
|
18
18
|
Document,
|
|
19
19
|
DocumentHeader,
|
|
20
20
|
DocumentModel,
|
|
21
|
+
utils as DocumentUtils,
|
|
21
22
|
Operation,
|
|
22
|
-
OperationScope
|
|
23
|
-
State
|
|
23
|
+
OperationScope
|
|
24
24
|
} from 'document-model/document';
|
|
25
25
|
import { createNanoEvents, Unsubscribe } from 'nanoevents';
|
|
26
26
|
import { ICache } from '../cache';
|
|
27
27
|
import InMemoryCache from '../cache/memory';
|
|
28
|
+
import { BaseQueueManager } from '../queue/base';
|
|
29
|
+
import {
|
|
30
|
+
ActionJob,
|
|
31
|
+
IQueueManager,
|
|
32
|
+
isActionJob,
|
|
33
|
+
isOperationJob,
|
|
34
|
+
Job,
|
|
35
|
+
OperationJob
|
|
36
|
+
} from '../queue/types';
|
|
28
37
|
import { MemoryStorage } from '../storage/memory';
|
|
29
38
|
import type {
|
|
30
39
|
DocumentDriveStorage,
|
|
31
40
|
DocumentStorage,
|
|
32
|
-
IDriveStorage
|
|
41
|
+
IDriveStorage
|
|
33
42
|
} from '../storage/types';
|
|
34
43
|
import { generateUUID, isBefore, isDocumentDrive } from '../utils';
|
|
35
44
|
import {
|
|
@@ -51,7 +60,8 @@ import {
|
|
|
51
60
|
InternalTransmitter,
|
|
52
61
|
IReceiver,
|
|
53
62
|
ITransmitter,
|
|
54
|
-
PullResponderTransmitter
|
|
63
|
+
PullResponderTransmitter,
|
|
64
|
+
SubscriptionTransmitter
|
|
55
65
|
} from './listener/transmitter';
|
|
56
66
|
import {
|
|
57
67
|
BaseDocumentDriveServer,
|
|
@@ -61,6 +71,7 @@ import {
|
|
|
61
71
|
ListenerState,
|
|
62
72
|
RemoteDriveOptions,
|
|
63
73
|
StrandUpdate,
|
|
74
|
+
SynchronizationUnitQuery,
|
|
64
75
|
SyncStatus,
|
|
65
76
|
type CreateDocumentInput,
|
|
66
77
|
type DriveInput,
|
|
@@ -69,8 +80,6 @@ import {
|
|
|
69
80
|
type SynchronizationUnit
|
|
70
81
|
} from './types';
|
|
71
82
|
import { filterOperationsByRevision } from './utils';
|
|
72
|
-
import { BaseQueueManager } from '../queue/base';
|
|
73
|
-
import { IQueueManager } from '../queue/types';
|
|
74
83
|
|
|
75
84
|
export * from './listener';
|
|
76
85
|
export type * from './types';
|
|
@@ -87,7 +96,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
87
96
|
DocumentDriveState['id'],
|
|
88
97
|
Map<Trigger['id'], CancelPullLoop>
|
|
89
98
|
>();
|
|
90
|
-
private syncStatus = new Map<
|
|
99
|
+
private syncStatus = new Map<string, SyncStatus>();
|
|
91
100
|
|
|
92
101
|
private queueManager: IQueueManager;
|
|
93
102
|
|
|
@@ -95,7 +104,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
95
104
|
documentModels: DocumentModel[],
|
|
96
105
|
storage: IDriveStorage = new MemoryStorage(),
|
|
97
106
|
cache: ICache = new InMemoryCache(),
|
|
98
|
-
queueManager: IQueueManager = new BaseQueueManager()
|
|
107
|
+
queueManager: IQueueManager = new BaseQueueManager()
|
|
99
108
|
) {
|
|
100
109
|
super();
|
|
101
110
|
this.listenerStateManager = new ListenerManager(this);
|
|
@@ -103,6 +112,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
103
112
|
this.storage = storage;
|
|
104
113
|
this.cache = cache;
|
|
105
114
|
this.queueManager = queueManager;
|
|
115
|
+
|
|
116
|
+
this.storage.setStorageDelegate?.({
|
|
117
|
+
getCachedOperations: async (drive, id) => {
|
|
118
|
+
try {
|
|
119
|
+
const document = await this.cache.getDocument(drive, id);
|
|
120
|
+
return document?.operations;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error(error);
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
private updateSyncStatus(
|
|
@@ -119,31 +140,30 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
private async saveStrand(strand: StrandUpdate) {
|
|
122
|
-
const operations: Operation[] = strand.operations.map(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
128
|
-
);
|
|
143
|
+
const operations: Operation[] = strand.operations.map(op => ({
|
|
144
|
+
...op,
|
|
145
|
+
scope: strand.scope,
|
|
146
|
+
branch: strand.branch
|
|
147
|
+
}));
|
|
129
148
|
|
|
130
149
|
const result = await (!strand.documentId
|
|
131
150
|
? this.queueDriveOperations(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
strand.driveId,
|
|
152
|
+
operations as Operation<DocumentDriveAction | BaseAction>[],
|
|
153
|
+
false
|
|
154
|
+
)
|
|
136
155
|
: this.queueOperations(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
strand.driveId,
|
|
157
|
+
strand.documentId,
|
|
158
|
+
operations,
|
|
159
|
+
false
|
|
160
|
+
));
|
|
142
161
|
|
|
143
162
|
if (result.status === 'ERROR') {
|
|
144
163
|
this.updateSyncStatus(strand.driveId, result.status, result.error);
|
|
164
|
+
} else {
|
|
165
|
+
this.emit('strandUpdate', strand);
|
|
145
166
|
}
|
|
146
|
-
this.emit('strandUpdate', strand);
|
|
147
167
|
return result;
|
|
148
168
|
}
|
|
149
169
|
|
|
@@ -174,6 +194,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
174
194
|
const drive = await this.getDrive(driveId);
|
|
175
195
|
let driveTriggers = this.triggerMap.get(driveId);
|
|
176
196
|
|
|
197
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
198
|
+
|
|
177
199
|
for (const trigger of drive.state.local.triggers) {
|
|
178
200
|
if (driveTriggers?.get(trigger.id)) {
|
|
179
201
|
continue;
|
|
@@ -184,8 +206,56 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
184
206
|
}
|
|
185
207
|
|
|
186
208
|
this.updateSyncStatus(driveId, 'SYNCING');
|
|
209
|
+
|
|
210
|
+
for (const syncUnit of syncUnits) {
|
|
211
|
+
this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let cancelTrigger: (() => void) | undefined = undefined;
|
|
187
215
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
188
|
-
|
|
216
|
+
cancelTrigger = PullResponderTransmitter.setupPull(
|
|
217
|
+
driveId,
|
|
218
|
+
trigger,
|
|
219
|
+
this.saveStrand.bind(this),
|
|
220
|
+
error => {
|
|
221
|
+
this.updateSyncStatus(
|
|
222
|
+
driveId,
|
|
223
|
+
error instanceof OperationError
|
|
224
|
+
? error.status
|
|
225
|
+
: 'ERROR',
|
|
226
|
+
error
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
revisions => {
|
|
230
|
+
const errorRevision = revisions.filter(
|
|
231
|
+
r => r.status !== 'SUCCESS'
|
|
232
|
+
);
|
|
233
|
+
if (errorRevision.length < 1) {
|
|
234
|
+
this.updateSyncStatus(driveId, 'SUCCESS');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const syncUnit of syncUnits) {
|
|
238
|
+
const fileErrorRevision = errorRevision.find(
|
|
239
|
+
r => r.documentId === syncUnit.documentId
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (fileErrorRevision) {
|
|
243
|
+
this.updateSyncStatus(
|
|
244
|
+
syncUnit.syncId,
|
|
245
|
+
fileErrorRevision.status,
|
|
246
|
+
fileErrorRevision.error
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
this.updateSyncStatus(
|
|
250
|
+
syncUnit.syncId,
|
|
251
|
+
'SUCCESS'
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
} else if (SubscriptionTransmitter.isTrigger(trigger)) {
|
|
258
|
+
cancelTrigger = SubscriptionTransmitter.setup(
|
|
189
259
|
driveId,
|
|
190
260
|
trigger,
|
|
191
261
|
this.saveStrand.bind(this),
|
|
@@ -207,19 +277,78 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
207
277
|
}
|
|
208
278
|
}
|
|
209
279
|
);
|
|
210
|
-
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (cancelTrigger) {
|
|
283
|
+
driveTriggers.set(trigger.id, cancelTrigger);
|
|
211
284
|
this.triggerMap.set(driveId, driveTriggers);
|
|
212
285
|
}
|
|
213
286
|
}
|
|
214
287
|
}
|
|
215
288
|
|
|
216
289
|
private async stopSyncRemoteDrive(driveId: string) {
|
|
290
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
291
|
+
const fileNodes = syncUnits
|
|
292
|
+
.filter(syncUnit => syncUnit.documentId !== '')
|
|
293
|
+
.map(syncUnit => syncUnit.documentId);
|
|
294
|
+
|
|
217
295
|
const triggers = this.triggerMap.get(driveId);
|
|
218
296
|
triggers?.forEach(cancel => cancel());
|
|
219
297
|
this.updateSyncStatus(driveId, null);
|
|
298
|
+
|
|
299
|
+
for (const fileNode of fileNodes) {
|
|
300
|
+
this.updateSyncStatus(fileNode, null);
|
|
301
|
+
}
|
|
220
302
|
return this.triggerMap.delete(driveId);
|
|
221
303
|
}
|
|
222
304
|
|
|
305
|
+
private queueDelegate = {
|
|
306
|
+
checkDocumentExists: (
|
|
307
|
+
driveId: string,
|
|
308
|
+
documentId: string
|
|
309
|
+
): Promise<boolean> =>
|
|
310
|
+
this.storage.checkDocumentExists(driveId, documentId),
|
|
311
|
+
processOperationJob: async ({
|
|
312
|
+
driveId,
|
|
313
|
+
documentId,
|
|
314
|
+
operations,
|
|
315
|
+
forceSync
|
|
316
|
+
}: OperationJob) => {
|
|
317
|
+
return documentId
|
|
318
|
+
? this.addOperations(driveId, documentId, operations, forceSync)
|
|
319
|
+
: this.addDriveOperations(
|
|
320
|
+
driveId,
|
|
321
|
+
operations as Operation<
|
|
322
|
+
DocumentDriveAction | BaseAction
|
|
323
|
+
>[],
|
|
324
|
+
forceSync
|
|
325
|
+
);
|
|
326
|
+
},
|
|
327
|
+
processActionJob: async ({
|
|
328
|
+
driveId,
|
|
329
|
+
documentId,
|
|
330
|
+
actions,
|
|
331
|
+
forceSync
|
|
332
|
+
}: ActionJob) => {
|
|
333
|
+
return documentId
|
|
334
|
+
? this.addActions(driveId, documentId, actions, forceSync)
|
|
335
|
+
: this.addDriveActions(
|
|
336
|
+
driveId,
|
|
337
|
+
actions as Operation<DocumentDriveAction | BaseAction>[],
|
|
338
|
+
forceSync
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
processJob: async (job: Job) => {
|
|
342
|
+
if (isOperationJob(job)) {
|
|
343
|
+
return this.queueDelegate.processOperationJob(job);
|
|
344
|
+
} else if (isActionJob(job)) {
|
|
345
|
+
return this.queueDelegate.processActionJob(job);
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error('Unknown job type', job);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
223
352
|
async initialize() {
|
|
224
353
|
const errors: Error[] = [];
|
|
225
354
|
const drives = await this.getDrives();
|
|
@@ -230,16 +359,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
230
359
|
});
|
|
231
360
|
}
|
|
232
361
|
|
|
233
|
-
await this.queueManager.init({
|
|
234
|
-
checkDocumentExists: (driveId: string, documentId: string): Promise<boolean> => this.storage.checkDocumentExists(driveId, documentId),
|
|
235
|
-
processOperationJob: ({ driveId, documentId, operations, forceSync }) => documentId ?
|
|
236
|
-
this.addOperations(driveId, documentId, operations, forceSync)
|
|
237
|
-
: this.addDriveOperations(driveId, operations as Operation<DocumentDriveAction | BaseAction>[], forceSync)
|
|
238
|
-
|
|
239
|
-
}, error => {
|
|
362
|
+
await this.queueManager.init(this.queueDelegate, error => {
|
|
240
363
|
logger.error(`Error initializing queue manager`, error);
|
|
241
364
|
errors.push(error);
|
|
242
|
-
})
|
|
365
|
+
});
|
|
243
366
|
|
|
244
367
|
// if network connect comes online then
|
|
245
368
|
// triggers the listeners update
|
|
@@ -278,6 +401,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
278
401
|
) {
|
|
279
402
|
const drive = await this.getDrive(driveId);
|
|
280
403
|
|
|
404
|
+
const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
|
|
405
|
+
driveId,
|
|
406
|
+
documentId,
|
|
407
|
+
scope,
|
|
408
|
+
branch,
|
|
409
|
+
documentType,
|
|
410
|
+
drive
|
|
411
|
+
);
|
|
412
|
+
const revisions = await this.storage.getSynchronizationUnitsRevision(
|
|
413
|
+
synchronizationUnitsQuery
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const synchronizationUnits: SynchronizationUnit[] =
|
|
417
|
+
synchronizationUnitsQuery.map(s => ({
|
|
418
|
+
...s,
|
|
419
|
+
lastUpdated: drive.created,
|
|
420
|
+
revision: -1
|
|
421
|
+
}));
|
|
422
|
+
for (const revision of revisions) {
|
|
423
|
+
const syncUnit = synchronizationUnits.find(
|
|
424
|
+
s =>
|
|
425
|
+
revision.driveId === s.driveId &&
|
|
426
|
+
revision.documentId === s.documentId &&
|
|
427
|
+
revision.scope === s.scope &&
|
|
428
|
+
revision.branch === s.branch
|
|
429
|
+
);
|
|
430
|
+
if (syncUnit) {
|
|
431
|
+
syncUnit.revision = revision.revision;
|
|
432
|
+
syncUnit.lastUpdated = revision.lastUpdated;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return synchronizationUnits;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
public async getSynchronizationUnitsIds(
|
|
439
|
+
driveId: string,
|
|
440
|
+
documentId?: string[],
|
|
441
|
+
scope?: string[],
|
|
442
|
+
branch?: string[],
|
|
443
|
+
documentType?: string[],
|
|
444
|
+
loadedDrive?: DocumentDriveDocument
|
|
445
|
+
): Promise<SynchronizationUnitQuery[]> {
|
|
446
|
+
const drive = loadedDrive ?? (await this.getDrive(driveId));
|
|
281
447
|
const nodes = drive.state.global.nodes.filter(
|
|
282
448
|
node =>
|
|
283
449
|
isFileNode(node) &&
|
|
@@ -311,53 +477,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
311
477
|
});
|
|
312
478
|
}
|
|
313
479
|
|
|
314
|
-
const
|
|
315
|
-
|
|
480
|
+
const synchronizationUnitsQuery: Omit<
|
|
481
|
+
SynchronizationUnit,
|
|
482
|
+
'revision' | 'lastUpdated'
|
|
483
|
+
>[] = [];
|
|
316
484
|
for (const node of nodes) {
|
|
317
485
|
const nodeUnits =
|
|
318
486
|
scope?.length || branch?.length
|
|
319
487
|
? node.synchronizationUnits.filter(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
488
|
+
unit =>
|
|
489
|
+
(!scope?.length ||
|
|
490
|
+
scope.includes(unit.scope) ||
|
|
491
|
+
scope.includes('*')) &&
|
|
492
|
+
(!branch?.length ||
|
|
493
|
+
branch.includes(unit.branch) ||
|
|
494
|
+
branch.includes('*'))
|
|
495
|
+
)
|
|
328
496
|
: node.synchronizationUnits;
|
|
329
497
|
if (!nodeUnits.length) {
|
|
330
498
|
continue;
|
|
331
499
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
? this.getDocument(driveId, node.id)
|
|
335
|
-
: this.getDrive(driveId));
|
|
336
|
-
|
|
337
|
-
for (const { syncId, scope, branch } of nodeUnits) {
|
|
338
|
-
const operations =
|
|
339
|
-
document.operations[scope as OperationScope] ?? [];
|
|
340
|
-
const lastOperation = operations[operations.length - 1];
|
|
341
|
-
synchronizationUnits.push({
|
|
342
|
-
syncId,
|
|
343
|
-
scope,
|
|
344
|
-
branch,
|
|
500
|
+
synchronizationUnitsQuery.push(
|
|
501
|
+
...nodeUnits.map(n => ({
|
|
345
502
|
driveId,
|
|
346
503
|
documentId: node.id,
|
|
504
|
+
syncId: n.syncId,
|
|
347
505
|
documentType: node.documentType,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
506
|
+
scope: n.scope,
|
|
507
|
+
branch: n.branch
|
|
508
|
+
}))
|
|
509
|
+
);
|
|
353
510
|
}
|
|
354
|
-
return
|
|
511
|
+
return synchronizationUnitsQuery;
|
|
355
512
|
}
|
|
356
513
|
|
|
357
|
-
public async
|
|
514
|
+
public async getSynchronizationUnitIdInfo(
|
|
358
515
|
driveId: string,
|
|
359
516
|
syncId: string
|
|
360
|
-
): Promise<
|
|
517
|
+
): Promise<SynchronizationUnitQuery | undefined> {
|
|
361
518
|
const drive = await this.getDrive(driveId);
|
|
362
519
|
const node = drive.state.global.nodes.find(
|
|
363
520
|
node =>
|
|
@@ -366,14 +523,43 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
366
523
|
);
|
|
367
524
|
|
|
368
525
|
if (!node || !isFileNode(node)) {
|
|
369
|
-
|
|
526
|
+
return undefined;
|
|
370
527
|
}
|
|
371
528
|
|
|
372
|
-
|
|
529
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
530
|
+
const syncUnit = node.synchronizationUnits.find(
|
|
373
531
|
unit => unit.syncId === syncId
|
|
374
|
-
)
|
|
532
|
+
);
|
|
533
|
+
if (!syncUnit) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
375
536
|
|
|
376
|
-
|
|
537
|
+
return {
|
|
538
|
+
syncId,
|
|
539
|
+
scope: syncUnit.scope,
|
|
540
|
+
branch: syncUnit.branch,
|
|
541
|
+
driveId,
|
|
542
|
+
documentId: node.id,
|
|
543
|
+
documentType: node.documentType
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
public async getSynchronizationUnit(
|
|
548
|
+
driveId: string,
|
|
549
|
+
syncId: string
|
|
550
|
+
): Promise<SynchronizationUnit | undefined> {
|
|
551
|
+
const syncUnit = await this.getSynchronizationUnitIdInfo(
|
|
552
|
+
driveId,
|
|
553
|
+
syncId
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
if (!syncUnit) {
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const { scope, branch, documentId, documentType } = syncUnit;
|
|
561
|
+
|
|
562
|
+
// TODO: REPLACE WITH GET DOCUMENT OPERATIONS
|
|
377
563
|
const document = await this.getDocument(driveId, documentId);
|
|
378
564
|
const operations = document.operations[scope as OperationScope] ?? [];
|
|
379
565
|
const lastOperation = operations[operations.length - 1];
|
|
@@ -384,7 +570,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
384
570
|
branch,
|
|
385
571
|
driveId,
|
|
386
572
|
documentId,
|
|
387
|
-
documentType
|
|
573
|
+
documentType,
|
|
388
574
|
lastUpdated: lastOperation?.timestamp ?? document.lastModified,
|
|
389
575
|
revision: lastOperation?.index ?? 0
|
|
390
576
|
};
|
|
@@ -398,17 +584,22 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
398
584
|
fromRevision?: number | undefined;
|
|
399
585
|
}
|
|
400
586
|
): Promise<OperationUpdate[]> {
|
|
401
|
-
const
|
|
587
|
+
const syncUnit =
|
|
402
588
|
syncId === '0'
|
|
403
589
|
? { documentId: '', scope: 'global' }
|
|
404
|
-
: await this.
|
|
590
|
+
: await this.getSynchronizationUnitIdInfo(driveId, syncId);
|
|
591
|
+
|
|
592
|
+
if (!syncUnit) {
|
|
593
|
+
throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
|
|
594
|
+
}
|
|
405
595
|
|
|
406
596
|
const document =
|
|
407
597
|
syncId === '0'
|
|
408
598
|
? await this.getDrive(driveId)
|
|
409
|
-
: await this.getDocument(driveId, documentId); // TODO replace with getDocumentOperations
|
|
599
|
+
: await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
|
|
410
600
|
|
|
411
|
-
const operations =
|
|
601
|
+
const operations =
|
|
602
|
+
document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
|
|
412
603
|
const filteredOperations = operations.filter(
|
|
413
604
|
operation =>
|
|
414
605
|
Object.keys(filter).length === 0 ||
|
|
@@ -425,7 +616,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
425
616
|
type: operation.type,
|
|
426
617
|
input: operation.input as object,
|
|
427
618
|
skip: operation.skip,
|
|
428
|
-
context: operation.context
|
|
619
|
+
context: operation.context,
|
|
620
|
+
id: operation.id
|
|
429
621
|
}));
|
|
430
622
|
}
|
|
431
623
|
|
|
@@ -455,12 +647,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
455
647
|
});
|
|
456
648
|
|
|
457
649
|
await this.storage.createDrive(id, document);
|
|
650
|
+
|
|
651
|
+
if (drive.global.slug) {
|
|
652
|
+
await this.cache.deleteDocument('drives-slug', drive.global.slug);
|
|
653
|
+
}
|
|
654
|
+
|
|
458
655
|
await this._initializeDrive(id);
|
|
459
656
|
|
|
460
657
|
return document;
|
|
461
658
|
}
|
|
462
659
|
|
|
463
|
-
async addRemoteDrive(
|
|
660
|
+
async addRemoteDrive(
|
|
661
|
+
url: string,
|
|
662
|
+
options: RemoteDriveOptions
|
|
663
|
+
): Promise<DocumentDriveDocument> {
|
|
464
664
|
const { id, name, slug, icon } = await requestPublicDrive(url);
|
|
465
665
|
const {
|
|
466
666
|
pullFilter,
|
|
@@ -471,11 +671,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
471
671
|
triggers
|
|
472
672
|
} = options;
|
|
473
673
|
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
pullInterval
|
|
478
|
-
});
|
|
674
|
+
const trigger = await SubscriptionTransmitter.createTrigger(id, url, {
|
|
675
|
+
pullFilter
|
|
676
|
+
});
|
|
479
677
|
|
|
480
678
|
return await this.addDrive({
|
|
481
679
|
global: {
|
|
@@ -485,7 +683,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
485
683
|
icon: icon ?? null
|
|
486
684
|
},
|
|
487
685
|
local: {
|
|
488
|
-
triggers: [...triggers,
|
|
686
|
+
triggers: [...triggers, trigger],
|
|
489
687
|
listeners: listeners,
|
|
490
688
|
availableOffline,
|
|
491
689
|
sharingType
|
|
@@ -494,9 +692,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
494
692
|
}
|
|
495
693
|
|
|
496
694
|
async deleteDrive(id: string) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
695
|
+
const result = await Promise.allSettled([
|
|
696
|
+
this.stopSyncRemoteDrive(id),
|
|
697
|
+
this.listenerStateManager.removeDrive(id),
|
|
698
|
+
this.cache.deleteDocument('drives', id),
|
|
699
|
+
this.storage.deleteDrive(id)
|
|
700
|
+
]);
|
|
701
|
+
|
|
702
|
+
result.forEach(r => {
|
|
703
|
+
if (r.status === 'rejected') {
|
|
704
|
+
throw r.reason;
|
|
705
|
+
}
|
|
706
|
+
});
|
|
500
707
|
}
|
|
501
708
|
|
|
502
709
|
getDrives() {
|
|
@@ -505,7 +712,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
505
712
|
|
|
506
713
|
async getDrive(drive: string, options?: GetDocumentOptions) {
|
|
507
714
|
try {
|
|
508
|
-
const document = await this.cache.getDocument('drives', drive);
|
|
715
|
+
const document = await this.cache.getDocument('drives', drive); // TODO support GetDocumentOptions
|
|
509
716
|
if (document && isDocumentDrive(document)) {
|
|
510
717
|
return document;
|
|
511
718
|
}
|
|
@@ -528,7 +735,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
528
735
|
|
|
529
736
|
async getDriveBySlug(slug: string, options?: GetDocumentOptions) {
|
|
530
737
|
try {
|
|
531
|
-
const document = await this.cache.getDocument('drives', slug);
|
|
738
|
+
const document = await this.cache.getDocument('drives-slug', slug);
|
|
532
739
|
if (document && isDocumentDrive(document)) {
|
|
533
740
|
return document;
|
|
534
741
|
}
|
|
@@ -544,7 +751,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
544
751
|
);
|
|
545
752
|
} else {
|
|
546
753
|
this.cache
|
|
547
|
-
.setDocument('drives', slug, document)
|
|
754
|
+
.setDocument('drives-slug', slug, document)
|
|
548
755
|
.catch(logger.error);
|
|
549
756
|
return document;
|
|
550
757
|
}
|
|
@@ -552,16 +759,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
552
759
|
|
|
553
760
|
async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
|
|
554
761
|
try {
|
|
555
|
-
const document = await this.cache.getDocument(drive, id);
|
|
762
|
+
const document = await this.cache.getDocument(drive, id); // TODO support GetDocumentOptions
|
|
556
763
|
if (document) {
|
|
557
764
|
return document;
|
|
558
765
|
}
|
|
559
766
|
} catch (e) {
|
|
560
767
|
logger.error('Error getting document from cache', e);
|
|
561
768
|
}
|
|
562
|
-
const documentStorage =
|
|
563
|
-
|
|
564
|
-
const document = this._buildDocument(documentStorage, options)
|
|
769
|
+
const documentStorage = await this.storage.getDocument(drive, id);
|
|
770
|
+
const document = this._buildDocument(documentStorage, options);
|
|
565
771
|
|
|
566
772
|
this.cache.setDocument(drive, id, document).catch(logger.error);
|
|
567
773
|
return document;
|
|
@@ -579,14 +785,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
579
785
|
let state = undefined;
|
|
580
786
|
if (input.document) {
|
|
581
787
|
if (input.documentType !== input.document.documentType) {
|
|
582
|
-
throw new Error(
|
|
788
|
+
throw new Error(
|
|
789
|
+
`Provided document is not ${input.documentType}`
|
|
790
|
+
);
|
|
583
791
|
}
|
|
584
792
|
const doc = this._buildDocument(input.document);
|
|
585
793
|
state = doc.state;
|
|
586
794
|
}
|
|
587
795
|
|
|
588
796
|
// if no document was provided then create a new one
|
|
589
|
-
const document =
|
|
797
|
+
const document =
|
|
798
|
+
input.document ??
|
|
590
799
|
this._getDocumentModel(input.documentType).utils.createDocument();
|
|
591
800
|
|
|
592
801
|
// stores document information
|
|
@@ -608,9 +817,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
608
817
|
const operations = Object.values(document.operations).flat();
|
|
609
818
|
if (operations.length) {
|
|
610
819
|
if (isDocumentDrive(document)) {
|
|
611
|
-
await this.storage.addDriveOperations(
|
|
820
|
+
await this.storage.addDriveOperations(
|
|
821
|
+
driveId,
|
|
822
|
+
operations as Operation<DocumentDriveAction>[],
|
|
823
|
+
document
|
|
824
|
+
);
|
|
612
825
|
} else {
|
|
613
|
-
await this.storage.addDocumentOperations(
|
|
826
|
+
await this.storage.addDocumentOperations(
|
|
827
|
+
driveId,
|
|
828
|
+
input.id,
|
|
829
|
+
operations,
|
|
830
|
+
document
|
|
831
|
+
);
|
|
614
832
|
}
|
|
615
833
|
}
|
|
616
834
|
|
|
@@ -619,7 +837,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
619
837
|
|
|
620
838
|
async deleteDocument(driveId: string, id: string) {
|
|
621
839
|
try {
|
|
622
|
-
const syncUnits = await this.
|
|
840
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
|
|
841
|
+
id
|
|
842
|
+
]);
|
|
623
843
|
await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
|
|
624
844
|
} catch (error) {
|
|
625
845
|
logger.warn('Error deleting document', error);
|
|
@@ -630,12 +850,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
630
850
|
|
|
631
851
|
async _processOperations<T extends Document, A extends Action>(
|
|
632
852
|
drive: string,
|
|
853
|
+
documentId: string | undefined,
|
|
633
854
|
storageDocument: DocumentStorage<T>,
|
|
634
855
|
operations: Operation<A | BaseAction>[]
|
|
635
856
|
) {
|
|
636
857
|
const operationsApplied: Operation<A | BaseAction>[] = [];
|
|
637
858
|
const signals: SignalResult[] = [];
|
|
638
|
-
let document: T = storageDocument
|
|
859
|
+
let document: T = this._buildDocument(storageDocument);
|
|
639
860
|
|
|
640
861
|
let error: OperationError | undefined; // TODO: replace with an array of errors/consistency issues
|
|
641
862
|
const operationsByScope = groupOperationsByScope(operations);
|
|
@@ -667,7 +888,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
667
888
|
: merge(trunk, invertedTrunk, reshuffleByTimestamp);
|
|
668
889
|
|
|
669
890
|
const newOperations = newHistory.filter(
|
|
670
|
-
|
|
891
|
+
op => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
|
|
671
892
|
);
|
|
672
893
|
|
|
673
894
|
for (const nextOperation of newOperations) {
|
|
@@ -676,15 +897,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
676
897
|
// when dealing with a merge (tail.length > 0) we have to skip hash validation
|
|
677
898
|
// for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
|
|
678
899
|
if (tail.length > 0) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
invertedTrunkOp.hash === nextOperation.hash
|
|
900
|
+
const sourceOperation = operations.find(
|
|
901
|
+
op => op.hash === nextOperation.hash
|
|
682
902
|
);
|
|
903
|
+
|
|
904
|
+
skipHashValidation =
|
|
905
|
+
!sourceOperation ||
|
|
906
|
+
sourceOperation.index !== nextOperation.index ||
|
|
907
|
+
sourceOperation.skip !== nextOperation.skip;
|
|
683
908
|
}
|
|
684
909
|
|
|
685
910
|
try {
|
|
686
911
|
const appliedResult = await this._performOperation(
|
|
687
912
|
drive,
|
|
913
|
+
documentId,
|
|
688
914
|
document,
|
|
689
915
|
nextOperation,
|
|
690
916
|
skipHashValidation
|
|
@@ -697,11 +923,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
697
923
|
e instanceof OperationError
|
|
698
924
|
? e
|
|
699
925
|
: new OperationError(
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
926
|
+
'ERROR',
|
|
927
|
+
nextOperation,
|
|
928
|
+
(e as Error).message,
|
|
929
|
+
(e as Error).cause
|
|
930
|
+
);
|
|
705
931
|
|
|
706
932
|
// TODO: don't break on errors...
|
|
707
933
|
break;
|
|
@@ -713,22 +939,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
713
939
|
document,
|
|
714
940
|
operationsApplied,
|
|
715
941
|
signals,
|
|
716
|
-
error
|
|
942
|
+
error
|
|
717
943
|
} as const;
|
|
718
944
|
}
|
|
719
945
|
|
|
720
946
|
private _buildDocument<T extends Document>(
|
|
721
|
-
documentStorage: DocumentStorage<T>,
|
|
947
|
+
documentStorage: DocumentStorage<T>,
|
|
948
|
+
options?: GetDocumentOptions
|
|
722
949
|
): T {
|
|
950
|
+
if (
|
|
951
|
+
documentStorage.state &&
|
|
952
|
+
(!options || options.checkHashes === false)
|
|
953
|
+
) {
|
|
954
|
+
return documentStorage as T;
|
|
955
|
+
}
|
|
956
|
+
|
|
723
957
|
const documentModel = this._getDocumentModel(
|
|
724
958
|
documentStorage.documentType
|
|
725
959
|
);
|
|
726
960
|
|
|
727
|
-
const revisionOperations =
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
961
|
+
const revisionOperations =
|
|
962
|
+
options?.revisions !== undefined
|
|
963
|
+
? filterOperationsByRevision(
|
|
964
|
+
documentStorage.operations,
|
|
965
|
+
options.revisions
|
|
966
|
+
)
|
|
967
|
+
: documentStorage.operations;
|
|
968
|
+
const operations =
|
|
969
|
+
baseUtils.documentHelpers.garbageCollectDocumentOperations(
|
|
970
|
+
revisionOperations
|
|
971
|
+
);
|
|
732
972
|
|
|
733
973
|
return baseUtils.replayDocument(
|
|
734
974
|
documentStorage.initialState,
|
|
@@ -737,12 +977,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
737
977
|
undefined,
|
|
738
978
|
documentStorage,
|
|
739
979
|
undefined,
|
|
740
|
-
{
|
|
980
|
+
{
|
|
981
|
+
...options,
|
|
982
|
+
checkHashes: options?.checkHashes ?? true,
|
|
983
|
+
reuseOperationResultingState: options?.checkHashes ?? true
|
|
984
|
+
}
|
|
741
985
|
) as T;
|
|
742
986
|
}
|
|
743
987
|
|
|
744
988
|
private async _performOperation<T extends Document>(
|
|
745
989
|
drive: string,
|
|
990
|
+
id: string | undefined,
|
|
746
991
|
document: T,
|
|
747
992
|
operation: Operation,
|
|
748
993
|
skipHashValidation = false
|
|
@@ -752,6 +997,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
752
997
|
const signalResults: SignalResult[] = [];
|
|
753
998
|
let newDocument = document;
|
|
754
999
|
|
|
1000
|
+
const scope = operation.scope;
|
|
1001
|
+
const documentOperations =
|
|
1002
|
+
DocumentUtils.documentHelpers.garbageCollectDocumentOperations({
|
|
1003
|
+
...document.operations,
|
|
1004
|
+
[scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
|
|
1005
|
+
document.operations[scope],
|
|
1006
|
+
operation
|
|
1007
|
+
)
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
1011
|
+
// if the latest operation doesn't have a resulting state then tries
|
|
1012
|
+
// to retrieve it from the db to avoid rerunning all the operations
|
|
1013
|
+
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
1014
|
+
lastRemainingOperation.resultingState = await (id
|
|
1015
|
+
? this.storage.getOperationResultingState?.(
|
|
1016
|
+
drive,
|
|
1017
|
+
id,
|
|
1018
|
+
lastRemainingOperation.index,
|
|
1019
|
+
lastRemainingOperation.scope,
|
|
1020
|
+
'main'
|
|
1021
|
+
)
|
|
1022
|
+
: this.storage.getDriveOperationResultingState?.(
|
|
1023
|
+
drive,
|
|
1024
|
+
lastRemainingOperation.index,
|
|
1025
|
+
lastRemainingOperation.scope,
|
|
1026
|
+
'main'
|
|
1027
|
+
));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
755
1030
|
const operationSignals: (() => Promise<SignalResult>)[] = [];
|
|
756
1031
|
newDocument = documentModel.reducer(
|
|
757
1032
|
newDocument,
|
|
@@ -788,7 +1063,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
788
1063
|
);
|
|
789
1064
|
}
|
|
790
1065
|
},
|
|
791
|
-
{ skip: operation.skip }
|
|
1066
|
+
{ skip: operation.skip, reuseOperationResultingState: true }
|
|
792
1067
|
) as T;
|
|
793
1068
|
|
|
794
1069
|
const appliedOperation = newDocument.operations[operation.scope].filter(
|
|
@@ -799,16 +1074,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
799
1074
|
throw new OperationError(
|
|
800
1075
|
'ERROR',
|
|
801
1076
|
operation,
|
|
802
|
-
`Operation with index ${operation.index}:${operation.skip} was not applied.`
|
|
1077
|
+
`Operation with index ${operation.index}:${operation.skip || 0} was not applied.`
|
|
803
1078
|
);
|
|
804
1079
|
} else if (
|
|
805
1080
|
appliedOperation[0]!.hash !== operation.hash &&
|
|
806
1081
|
!skipHashValidation
|
|
807
1082
|
) {
|
|
808
|
-
throw new ConflictOperationError(
|
|
809
|
-
operation,
|
|
810
|
-
appliedOperation[0]!
|
|
811
|
-
);
|
|
1083
|
+
throw new ConflictOperationError(operation, appliedOperation[0]!);
|
|
812
1084
|
}
|
|
813
1085
|
|
|
814
1086
|
for (const signalHandler of operationSignals) {
|
|
@@ -823,7 +1095,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
823
1095
|
};
|
|
824
1096
|
}
|
|
825
1097
|
|
|
826
|
-
addOperation(
|
|
1098
|
+
addOperation(
|
|
1099
|
+
drive: string,
|
|
1100
|
+
id: string,
|
|
1101
|
+
operation: Operation,
|
|
1102
|
+
forceSync = true
|
|
1103
|
+
): Promise<IOperationResult> {
|
|
827
1104
|
return this.addOperations(drive, id, [operation], forceSync);
|
|
828
1105
|
}
|
|
829
1106
|
|
|
@@ -833,21 +1110,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
833
1110
|
callback: (document: DocumentStorage) => Promise<{
|
|
834
1111
|
operations: Operation[];
|
|
835
1112
|
header: DocumentHeader;
|
|
836
|
-
newState: State<any, any> | undefined;
|
|
837
1113
|
}>
|
|
838
1114
|
) {
|
|
839
1115
|
if (!this.storage.addDocumentOperationsWithTransaction) {
|
|
840
1116
|
const documentStorage = await this.storage.getDocument(drive, id);
|
|
841
1117
|
const result = await callback(documentStorage);
|
|
842
1118
|
// saves the applied operations to storage
|
|
843
|
-
if (
|
|
844
|
-
result.operations.length > 0
|
|
845
|
-
) {
|
|
1119
|
+
if (result.operations.length > 0) {
|
|
846
1120
|
await this.storage.addDocumentOperations(
|
|
847
1121
|
drive,
|
|
848
1122
|
id,
|
|
849
1123
|
result.operations,
|
|
850
|
-
result.header
|
|
1124
|
+
result.header
|
|
851
1125
|
);
|
|
852
1126
|
}
|
|
853
1127
|
} else {
|
|
@@ -859,47 +1133,213 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
859
1133
|
}
|
|
860
1134
|
}
|
|
861
1135
|
|
|
862
|
-
queueOperation(
|
|
1136
|
+
queueOperation(
|
|
1137
|
+
drive: string,
|
|
1138
|
+
id: string,
|
|
1139
|
+
operation: Operation,
|
|
1140
|
+
forceSync = true
|
|
1141
|
+
): Promise<IOperationResult> {
|
|
863
1142
|
return this.queueOperations(drive, id, [operation], forceSync);
|
|
864
1143
|
}
|
|
865
1144
|
|
|
866
|
-
async
|
|
1145
|
+
private async resultIfExistingOperations(
|
|
1146
|
+
drive: string,
|
|
1147
|
+
id: string,
|
|
1148
|
+
operations: Operation[]
|
|
1149
|
+
): Promise<IOperationResult | undefined> {
|
|
1150
|
+
try {
|
|
1151
|
+
const document = await this.getDocument(drive, id);
|
|
1152
|
+
const newOperation = operations.find(
|
|
1153
|
+
op =>
|
|
1154
|
+
!op.id ||
|
|
1155
|
+
!document.operations[op.scope].find(
|
|
1156
|
+
existingOp =>
|
|
1157
|
+
existingOp.id === op.id &&
|
|
1158
|
+
existingOp.index === op.index &&
|
|
1159
|
+
existingOp.type === op.type &&
|
|
1160
|
+
existingOp.hash === op.hash
|
|
1161
|
+
)
|
|
1162
|
+
);
|
|
1163
|
+
if (!newOperation) {
|
|
1164
|
+
return {
|
|
1165
|
+
status: 'SUCCESS',
|
|
1166
|
+
document,
|
|
1167
|
+
operations,
|
|
1168
|
+
signals: []
|
|
1169
|
+
};
|
|
1170
|
+
} else {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
console.error(error); // TODO error
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async queueOperations(
|
|
1180
|
+
drive: string,
|
|
867
1181
|
id: string,
|
|
868
1182
|
operations: Operation[],
|
|
869
|
-
forceSync = true
|
|
1183
|
+
forceSync = true
|
|
1184
|
+
) {
|
|
1185
|
+
// if operations are already stored then returns cached document
|
|
1186
|
+
const result = await this.resultIfExistingOperations(
|
|
1187
|
+
drive,
|
|
1188
|
+
id,
|
|
1189
|
+
operations
|
|
1190
|
+
);
|
|
1191
|
+
if (result) {
|
|
1192
|
+
console.log('Duplicated operations!');
|
|
1193
|
+
return result;
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const jobId = await this.queueManager.addJob({
|
|
1197
|
+
driveId: drive,
|
|
1198
|
+
documentId: id,
|
|
1199
|
+
operations,
|
|
1200
|
+
forceSync
|
|
1201
|
+
});
|
|
870
1202
|
|
|
1203
|
+
return new Promise<IOperationResult>((resolve, reject) => {
|
|
1204
|
+
const unsubscribe = this.queueManager.on(
|
|
1205
|
+
'jobCompleted',
|
|
1206
|
+
(job, result) => {
|
|
1207
|
+
if (job.jobId === jobId) {
|
|
1208
|
+
unsubscribe();
|
|
1209
|
+
unsubscribeError();
|
|
1210
|
+
resolve(result);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
1214
|
+
const unsubscribeError = this.queueManager.on(
|
|
1215
|
+
'jobFailed',
|
|
1216
|
+
(job, error) => {
|
|
1217
|
+
if (job.jobId === jobId) {
|
|
1218
|
+
unsubscribe();
|
|
1219
|
+
unsubscribeError();
|
|
1220
|
+
reject(error);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
1224
|
+
});
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
logger.error('Error adding job', error);
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
async queueAction(
|
|
1232
|
+
drive: string,
|
|
1233
|
+
id: string,
|
|
1234
|
+
action: Action,
|
|
1235
|
+
forceSync?: boolean | undefined
|
|
1236
|
+
): Promise<IOperationResult> {
|
|
1237
|
+
return this.queueActions(drive, id, [action], forceSync);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async queueActions(
|
|
1241
|
+
drive: string,
|
|
1242
|
+
id: string,
|
|
1243
|
+
actions: Action[],
|
|
1244
|
+
forceSync?: boolean | undefined
|
|
1245
|
+
): Promise<IOperationResult> {
|
|
871
1246
|
try {
|
|
872
|
-
const jobId = await this.queueManager.addJob({
|
|
1247
|
+
const jobId = await this.queueManager.addJob({
|
|
1248
|
+
driveId: drive,
|
|
1249
|
+
documentId: id,
|
|
1250
|
+
actions,
|
|
1251
|
+
forceSync
|
|
1252
|
+
});
|
|
873
1253
|
|
|
874
1254
|
return new Promise<IOperationResult>((resolve, reject) => {
|
|
875
|
-
const unsubscribe = this.queueManager.on(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1255
|
+
const unsubscribe = this.queueManager.on(
|
|
1256
|
+
'jobCompleted',
|
|
1257
|
+
(job, result) => {
|
|
1258
|
+
if (job.jobId === jobId) {
|
|
1259
|
+
unsubscribe();
|
|
1260
|
+
unsubscribeError();
|
|
1261
|
+
resolve(result);
|
|
1262
|
+
}
|
|
880
1263
|
}
|
|
881
|
-
|
|
882
|
-
const unsubscribeError = this.queueManager.on(
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1264
|
+
);
|
|
1265
|
+
const unsubscribeError = this.queueManager.on(
|
|
1266
|
+
'jobFailed',
|
|
1267
|
+
(job, error) => {
|
|
1268
|
+
if (job.jobId === jobId) {
|
|
1269
|
+
unsubscribe();
|
|
1270
|
+
unsubscribeError();
|
|
1271
|
+
reject(error);
|
|
1272
|
+
}
|
|
888
1273
|
}
|
|
889
|
-
|
|
890
|
-
})
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
891
1276
|
} catch (error) {
|
|
892
1277
|
logger.error('Error adding job', error);
|
|
893
1278
|
throw error;
|
|
894
1279
|
}
|
|
895
1280
|
}
|
|
896
1281
|
|
|
1282
|
+
async queueDriveAction(
|
|
1283
|
+
drive: string,
|
|
1284
|
+
action: DocumentDriveAction | BaseAction,
|
|
1285
|
+
forceSync?: boolean | undefined
|
|
1286
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1287
|
+
return this.queueDriveActions(drive, [action], forceSync);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async queueDriveActions(
|
|
1291
|
+
drive: string,
|
|
1292
|
+
actions: (DocumentDriveAction | BaseAction)[],
|
|
1293
|
+
forceSync?: boolean | undefined
|
|
1294
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1295
|
+
const jobId = await this.queueManager.addJob({
|
|
1296
|
+
driveId: drive,
|
|
1297
|
+
actions,
|
|
1298
|
+
forceSync
|
|
1299
|
+
});
|
|
1300
|
+
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1301
|
+
(resolve, reject) => {
|
|
1302
|
+
const unsubscribe = this.queueManager.on(
|
|
1303
|
+
'jobCompleted',
|
|
1304
|
+
(job, result) => {
|
|
1305
|
+
if (job.jobId === jobId) {
|
|
1306
|
+
unsubscribe();
|
|
1307
|
+
unsubscribeError();
|
|
1308
|
+
resolve(
|
|
1309
|
+
result as IOperationResult<DocumentDriveDocument>
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1314
|
+
const unsubscribeError = this.queueManager.on(
|
|
1315
|
+
'jobFailed',
|
|
1316
|
+
(job, error) => {
|
|
1317
|
+
if (job.jobId === jobId) {
|
|
1318
|
+
unsubscribe();
|
|
1319
|
+
unsubscribeError();
|
|
1320
|
+
reject(error);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
897
1328
|
async addOperations(
|
|
898
1329
|
drive: string,
|
|
899
1330
|
id: string,
|
|
900
1331
|
operations: Operation[],
|
|
901
1332
|
forceSync = true
|
|
902
1333
|
) {
|
|
1334
|
+
// if operations are already stored then returns the result
|
|
1335
|
+
const result = await this.resultIfExistingOperations(
|
|
1336
|
+
drive,
|
|
1337
|
+
id,
|
|
1338
|
+
operations
|
|
1339
|
+
);
|
|
1340
|
+
if (result) {
|
|
1341
|
+
return result;
|
|
1342
|
+
}
|
|
903
1343
|
let document: Document | undefined;
|
|
904
1344
|
const operationsApplied: Operation[] = [];
|
|
905
1345
|
const signals: SignalResult[] = [];
|
|
@@ -909,6 +1349,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
909
1349
|
await this._addOperations(drive, id, async documentStorage => {
|
|
910
1350
|
const result = await this._processOperations(
|
|
911
1351
|
drive,
|
|
1352
|
+
id,
|
|
912
1353
|
documentStorage,
|
|
913
1354
|
operations
|
|
914
1355
|
);
|
|
@@ -956,21 +1397,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
956
1397
|
.updateSynchronizationRevisions(
|
|
957
1398
|
drive,
|
|
958
1399
|
syncUnits,
|
|
959
|
-
() =>
|
|
1400
|
+
() => {
|
|
1401
|
+
this.updateSyncStatus(drive, 'SYNCING');
|
|
1402
|
+
|
|
1403
|
+
for (const syncUnit of syncUnits) {
|
|
1404
|
+
this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
960
1407
|
this.handleListenerError.bind(this),
|
|
961
1408
|
forceSync
|
|
962
1409
|
)
|
|
963
|
-
.then(
|
|
964
|
-
updates
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1410
|
+
.then(updates => {
|
|
1411
|
+
updates.length && this.updateSyncStatus(drive, 'SUCCESS');
|
|
1412
|
+
|
|
1413
|
+
for (const syncUnit of syncUnits) {
|
|
1414
|
+
this.updateSyncStatus(syncUnit.syncId, 'SUCCESS');
|
|
1415
|
+
}
|
|
1416
|
+
})
|
|
968
1417
|
.catch(error => {
|
|
969
1418
|
logger.error(
|
|
970
1419
|
'Non handled error updating sync revision',
|
|
971
1420
|
error
|
|
972
1421
|
);
|
|
973
1422
|
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
1423
|
+
|
|
1424
|
+
for (const syncUnit of syncUnits) {
|
|
1425
|
+
this.updateSyncStatus(
|
|
1426
|
+
syncUnit.syncId,
|
|
1427
|
+
'ERROR',
|
|
1428
|
+
error as Error
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
974
1431
|
});
|
|
975
1432
|
|
|
976
1433
|
// after applying all the valid operations,throws
|
|
@@ -990,11 +1447,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
990
1447
|
error instanceof OperationError
|
|
991
1448
|
? error
|
|
992
1449
|
: new OperationError(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1450
|
+
'ERROR',
|
|
1451
|
+
undefined,
|
|
1452
|
+
(error as Error).message,
|
|
1453
|
+
(error as Error).cause
|
|
1454
|
+
);
|
|
998
1455
|
|
|
999
1456
|
return {
|
|
1000
1457
|
status: operationError.status,
|
|
@@ -1049,33 +1506,92 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1049
1506
|
}
|
|
1050
1507
|
}
|
|
1051
1508
|
|
|
1052
|
-
queueDriveOperation(
|
|
1509
|
+
queueDriveOperation(
|
|
1510
|
+
drive: string,
|
|
1511
|
+
operation: Operation<DocumentDriveAction | BaseAction>,
|
|
1512
|
+
forceSync = true
|
|
1513
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1053
1514
|
return this.queueDriveOperations(drive, [operation], forceSync);
|
|
1054
1515
|
}
|
|
1055
1516
|
|
|
1517
|
+
private async resultIfExistingDriveOperations(
|
|
1518
|
+
driveId: string,
|
|
1519
|
+
operations: Operation<DocumentDriveAction | BaseAction>[]
|
|
1520
|
+
): Promise<IOperationResult<DocumentDriveDocument> | undefined> {
|
|
1521
|
+
try {
|
|
1522
|
+
const drive = await this.getDrive(driveId);
|
|
1523
|
+
const newOperation = operations.find(
|
|
1524
|
+
op =>
|
|
1525
|
+
!op.id ||
|
|
1526
|
+
!drive.operations[op.scope].find(
|
|
1527
|
+
existingOp =>
|
|
1528
|
+
existingOp.id === op.id &&
|
|
1529
|
+
existingOp.index === op.index &&
|
|
1530
|
+
existingOp.type === op.type &&
|
|
1531
|
+
existingOp.hash === op.hash
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
if (!newOperation) {
|
|
1535
|
+
return {
|
|
1536
|
+
status: 'SUCCESS',
|
|
1537
|
+
document: drive,
|
|
1538
|
+
operations: operations,
|
|
1539
|
+
signals: []
|
|
1540
|
+
} as IOperationResult<DocumentDriveDocument>;
|
|
1541
|
+
} else {
|
|
1542
|
+
return undefined;
|
|
1543
|
+
}
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
console.error(error); // TODO error
|
|
1546
|
+
return undefined;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1056
1550
|
async queueDriveOperations(
|
|
1057
1551
|
drive: string,
|
|
1058
1552
|
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
1059
1553
|
forceSync = true
|
|
1060
1554
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
});
|
|
1070
|
-
const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
|
|
1071
|
-
if (job.jobId === jobId) {
|
|
1072
|
-
unsubscribe();
|
|
1073
|
-
unsubscribeError();
|
|
1074
|
-
reject(error);
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
1555
|
+
// if operations are already stored then returns cached document
|
|
1556
|
+
const result = await this.resultIfExistingDriveOperations(
|
|
1557
|
+
drive,
|
|
1558
|
+
operations
|
|
1559
|
+
);
|
|
1560
|
+
if (result) {
|
|
1561
|
+
return result;
|
|
1562
|
+
}
|
|
1077
1563
|
|
|
1078
|
-
|
|
1564
|
+
const jobId = await this.queueManager.addJob({
|
|
1565
|
+
driveId: drive,
|
|
1566
|
+
operations,
|
|
1567
|
+
forceSync
|
|
1568
|
+
});
|
|
1569
|
+
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1570
|
+
(resolve, reject) => {
|
|
1571
|
+
const unsubscribe = this.queueManager.on(
|
|
1572
|
+
'jobCompleted',
|
|
1573
|
+
(job, result) => {
|
|
1574
|
+
if (job.jobId === jobId) {
|
|
1575
|
+
unsubscribe();
|
|
1576
|
+
unsubscribeError();
|
|
1577
|
+
resolve(
|
|
1578
|
+
result as IOperationResult<DocumentDriveDocument>
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1583
|
+
const unsubscribeError = this.queueManager.on(
|
|
1584
|
+
'jobFailed',
|
|
1585
|
+
(job, error) => {
|
|
1586
|
+
if (job.jobId === jobId) {
|
|
1587
|
+
unsubscribe();
|
|
1588
|
+
unsubscribeError();
|
|
1589
|
+
reject(error);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1079
1595
|
}
|
|
1080
1596
|
|
|
1081
1597
|
async addDriveOperations(
|
|
@@ -1089,12 +1605,23 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1089
1605
|
const signals: SignalResult[] = [];
|
|
1090
1606
|
let error: Error | undefined;
|
|
1091
1607
|
|
|
1608
|
+
// if operations are already stored then returns cached drive
|
|
1609
|
+
const result = await this.resultIfExistingDriveOperations(
|
|
1610
|
+
drive,
|
|
1611
|
+
operations
|
|
1612
|
+
);
|
|
1613
|
+
if (result) {
|
|
1614
|
+
return result;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1618
|
+
|
|
1092
1619
|
try {
|
|
1093
1620
|
await this._addDriveOperations(drive, async documentStorage => {
|
|
1094
1621
|
const result = await this._processOperations<
|
|
1095
1622
|
DocumentDriveDocument,
|
|
1096
1623
|
DocumentDriveAction
|
|
1097
|
-
>(drive, documentStorage, operations.slice());
|
|
1624
|
+
>(drive, undefined, documentStorage, operations.slice());
|
|
1098
1625
|
|
|
1099
1626
|
document = result.document;
|
|
1100
1627
|
operationsApplied.push(...result.operationsApplied);
|
|
@@ -1103,7 +1630,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1103
1630
|
|
|
1104
1631
|
return {
|
|
1105
1632
|
operations: result.operationsApplied,
|
|
1106
|
-
header: result.document
|
|
1633
|
+
header: result.document
|
|
1107
1634
|
};
|
|
1108
1635
|
});
|
|
1109
1636
|
|
|
@@ -1128,6 +1655,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1128
1655
|
}
|
|
1129
1656
|
}
|
|
1130
1657
|
|
|
1658
|
+
const syncUnits = await this.getSynchronizationUnitsIds(drive);
|
|
1659
|
+
|
|
1660
|
+
const prevSyncUnitsIds = prevSyncUnits.map(unit => unit.syncId);
|
|
1661
|
+
const syncUnitsIds = syncUnits.map(unit => unit.syncId);
|
|
1662
|
+
|
|
1663
|
+
const newSyncUnits = syncUnitsIds.filter(
|
|
1664
|
+
syncUnitId => !prevSyncUnitsIds.includes(syncUnitId)
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
const removedSyncUnits = prevSyncUnitsIds.filter(
|
|
1668
|
+
syncUnitId => !syncUnitsIds.includes(syncUnitId)
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1131
1671
|
// update listener cache
|
|
1132
1672
|
const lastOperation = operationsApplied
|
|
1133
1673
|
.filter(op => op.scope === 'global')
|
|
@@ -1149,21 +1689,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1149
1689
|
revision: lastOperation.index
|
|
1150
1690
|
}
|
|
1151
1691
|
],
|
|
1152
|
-
() =>
|
|
1692
|
+
() => {
|
|
1693
|
+
this.updateSyncStatus(drive, 'SYNCING');
|
|
1694
|
+
|
|
1695
|
+
for (const syncUnitId of [
|
|
1696
|
+
...newSyncUnits,
|
|
1697
|
+
...removedSyncUnits
|
|
1698
|
+
]) {
|
|
1699
|
+
this.updateSyncStatus(syncUnitId, 'SYNCING');
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1153
1702
|
this.handleListenerError.bind(this),
|
|
1154
1703
|
forceSync
|
|
1155
1704
|
)
|
|
1156
|
-
.then(
|
|
1157
|
-
updates
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1705
|
+
.then(updates => {
|
|
1706
|
+
if (updates.length) {
|
|
1707
|
+
this.updateSyncStatus(drive, 'SUCCESS');
|
|
1708
|
+
|
|
1709
|
+
for (const syncUnitId of newSyncUnits) {
|
|
1710
|
+
this.updateSyncStatus(syncUnitId, 'SUCCESS');
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
for (const syncUnitId of removedSyncUnits) {
|
|
1714
|
+
this.updateSyncStatus(syncUnitId, null);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
})
|
|
1161
1718
|
.catch(error => {
|
|
1162
1719
|
logger.error(
|
|
1163
1720
|
'Non handled error updating sync revision',
|
|
1164
1721
|
error
|
|
1165
1722
|
);
|
|
1166
1723
|
this.updateSyncStatus(drive, 'ERROR', error as Error);
|
|
1724
|
+
|
|
1725
|
+
for (const syncUnitId of [
|
|
1726
|
+
...newSyncUnits,
|
|
1727
|
+
...removedSyncUnits
|
|
1728
|
+
]) {
|
|
1729
|
+
this.updateSyncStatus(
|
|
1730
|
+
syncUnitId,
|
|
1731
|
+
'ERROR',
|
|
1732
|
+
error as Error
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1167
1735
|
});
|
|
1168
1736
|
}
|
|
1169
1737
|
|
|
@@ -1190,11 +1758,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1190
1758
|
error instanceof OperationError
|
|
1191
1759
|
? error
|
|
1192
1760
|
: new OperationError(
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1761
|
+
'ERROR',
|
|
1762
|
+
undefined,
|
|
1763
|
+
(error as Error).message,
|
|
1764
|
+
(error as Error).cause
|
|
1765
|
+
);
|
|
1198
1766
|
|
|
1199
1767
|
return {
|
|
1200
1768
|
status: operationError.status,
|
|
@@ -1226,36 +1794,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1226
1794
|
async addAction(
|
|
1227
1795
|
drive: string,
|
|
1228
1796
|
id: string,
|
|
1229
|
-
action: Action
|
|
1797
|
+
action: Action,
|
|
1798
|
+
forceSync = true
|
|
1230
1799
|
): Promise<IOperationResult> {
|
|
1231
|
-
return this.addActions(drive, id, [action]);
|
|
1800
|
+
return this.addActions(drive, id, [action], forceSync);
|
|
1232
1801
|
}
|
|
1233
1802
|
|
|
1234
1803
|
async addActions(
|
|
1235
1804
|
drive: string,
|
|
1236
1805
|
id: string,
|
|
1237
|
-
actions: Action[]
|
|
1806
|
+
actions: Action[],
|
|
1807
|
+
forceSync = true
|
|
1238
1808
|
): Promise<IOperationResult> {
|
|
1239
1809
|
const document = await this.getDocument(drive, id);
|
|
1240
1810
|
const operations = this._buildOperations(document, actions);
|
|
1241
|
-
return this.
|
|
1811
|
+
return this.addOperations(drive, id, operations, forceSync);
|
|
1242
1812
|
}
|
|
1243
1813
|
|
|
1244
1814
|
async addDriveAction(
|
|
1245
1815
|
drive: string,
|
|
1246
|
-
action: DocumentDriveAction | BaseAction
|
|
1816
|
+
action: DocumentDriveAction | BaseAction,
|
|
1817
|
+
forceSync = true
|
|
1247
1818
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1248
|
-
return this.addDriveActions(drive, [action]);
|
|
1819
|
+
return this.addDriveActions(drive, [action], forceSync);
|
|
1249
1820
|
}
|
|
1250
1821
|
|
|
1251
1822
|
async addDriveActions(
|
|
1252
1823
|
drive: string,
|
|
1253
|
-
actions: (DocumentDriveAction | BaseAction)[]
|
|
1824
|
+
actions: (DocumentDriveAction | BaseAction)[],
|
|
1825
|
+
forceSync = true
|
|
1254
1826
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1255
1827
|
const document = await this.getDrive(drive);
|
|
1256
1828
|
const operations = this._buildOperations(document, actions);
|
|
1257
|
-
const result = await this.
|
|
1258
|
-
|
|
1829
|
+
const result = await this.addDriveOperations(
|
|
1830
|
+
drive,
|
|
1831
|
+
operations,
|
|
1832
|
+
forceSync
|
|
1833
|
+
);
|
|
1834
|
+
return result;
|
|
1259
1835
|
}
|
|
1260
1836
|
|
|
1261
1837
|
async addInternalListener(
|