document-drive 1.0.0-experimental.9 → 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 +772 -200
- 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 +313 -78
- 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,6 +850,7 @@ 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
|
) {
|
|
@@ -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,26 +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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
+
);
|
|
736
972
|
|
|
737
973
|
return baseUtils.replayDocument(
|
|
738
974
|
documentStorage.initialState,
|
|
@@ -741,12 +977,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
741
977
|
undefined,
|
|
742
978
|
documentStorage,
|
|
743
979
|
undefined,
|
|
744
|
-
{
|
|
980
|
+
{
|
|
981
|
+
...options,
|
|
982
|
+
checkHashes: options?.checkHashes ?? true,
|
|
983
|
+
reuseOperationResultingState: options?.checkHashes ?? true
|
|
984
|
+
}
|
|
745
985
|
) as T;
|
|
746
986
|
}
|
|
747
987
|
|
|
748
988
|
private async _performOperation<T extends Document>(
|
|
749
989
|
drive: string,
|
|
990
|
+
id: string | undefined,
|
|
750
991
|
document: T,
|
|
751
992
|
operation: Operation,
|
|
752
993
|
skipHashValidation = false
|
|
@@ -756,6 +997,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
756
997
|
const signalResults: SignalResult[] = [];
|
|
757
998
|
let newDocument = document;
|
|
758
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
|
+
|
|
759
1030
|
const operationSignals: (() => Promise<SignalResult>)[] = [];
|
|
760
1031
|
newDocument = documentModel.reducer(
|
|
761
1032
|
newDocument,
|
|
@@ -792,7 +1063,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
792
1063
|
);
|
|
793
1064
|
}
|
|
794
1065
|
},
|
|
795
|
-
{ skip: operation.skip }
|
|
1066
|
+
{ skip: operation.skip, reuseOperationResultingState: true }
|
|
796
1067
|
) as T;
|
|
797
1068
|
|
|
798
1069
|
const appliedOperation = newDocument.operations[operation.scope].filter(
|
|
@@ -803,16 +1074,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
803
1074
|
throw new OperationError(
|
|
804
1075
|
'ERROR',
|
|
805
1076
|
operation,
|
|
806
|
-
`Operation with index ${operation.index}:${operation.skip} was not applied.`
|
|
1077
|
+
`Operation with index ${operation.index}:${operation.skip || 0} was not applied.`
|
|
807
1078
|
);
|
|
808
1079
|
} else if (
|
|
809
1080
|
appliedOperation[0]!.hash !== operation.hash &&
|
|
810
1081
|
!skipHashValidation
|
|
811
1082
|
) {
|
|
812
|
-
throw new ConflictOperationError(
|
|
813
|
-
operation,
|
|
814
|
-
appliedOperation[0]!
|
|
815
|
-
);
|
|
1083
|
+
throw new ConflictOperationError(operation, appliedOperation[0]!);
|
|
816
1084
|
}
|
|
817
1085
|
|
|
818
1086
|
for (const signalHandler of operationSignals) {
|
|
@@ -827,7 +1095,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
827
1095
|
};
|
|
828
1096
|
}
|
|
829
1097
|
|
|
830
|
-
addOperation(
|
|
1098
|
+
addOperation(
|
|
1099
|
+
drive: string,
|
|
1100
|
+
id: string,
|
|
1101
|
+
operation: Operation,
|
|
1102
|
+
forceSync = true
|
|
1103
|
+
): Promise<IOperationResult> {
|
|
831
1104
|
return this.addOperations(drive, id, [operation], forceSync);
|
|
832
1105
|
}
|
|
833
1106
|
|
|
@@ -837,21 +1110,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
837
1110
|
callback: (document: DocumentStorage) => Promise<{
|
|
838
1111
|
operations: Operation[];
|
|
839
1112
|
header: DocumentHeader;
|
|
840
|
-
newState: State<any, any> | undefined;
|
|
841
1113
|
}>
|
|
842
1114
|
) {
|
|
843
1115
|
if (!this.storage.addDocumentOperationsWithTransaction) {
|
|
844
1116
|
const documentStorage = await this.storage.getDocument(drive, id);
|
|
845
1117
|
const result = await callback(documentStorage);
|
|
846
1118
|
// saves the applied operations to storage
|
|
847
|
-
if (
|
|
848
|
-
result.operations.length > 0
|
|
849
|
-
) {
|
|
1119
|
+
if (result.operations.length > 0) {
|
|
850
1120
|
await this.storage.addDocumentOperations(
|
|
851
1121
|
drive,
|
|
852
1122
|
id,
|
|
853
1123
|
result.operations,
|
|
854
|
-
result.header
|
|
1124
|
+
result.header
|
|
855
1125
|
);
|
|
856
1126
|
}
|
|
857
1127
|
} else {
|
|
@@ -863,47 +1133,213 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
863
1133
|
}
|
|
864
1134
|
}
|
|
865
1135
|
|
|
866
|
-
queueOperation(
|
|
1136
|
+
queueOperation(
|
|
1137
|
+
drive: string,
|
|
1138
|
+
id: string,
|
|
1139
|
+
operation: Operation,
|
|
1140
|
+
forceSync = true
|
|
1141
|
+
): Promise<IOperationResult> {
|
|
867
1142
|
return this.queueOperations(drive, id, [operation], forceSync);
|
|
868
1143
|
}
|
|
869
1144
|
|
|
870
|
-
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,
|
|
871
1181
|
id: string,
|
|
872
1182
|
operations: Operation[],
|
|
873
|
-
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
|
+
});
|
|
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
|
+
}
|
|
874
1239
|
|
|
1240
|
+
async queueActions(
|
|
1241
|
+
drive: string,
|
|
1242
|
+
id: string,
|
|
1243
|
+
actions: Action[],
|
|
1244
|
+
forceSync?: boolean | undefined
|
|
1245
|
+
): Promise<IOperationResult> {
|
|
875
1246
|
try {
|
|
876
|
-
const jobId = await this.queueManager.addJob({
|
|
1247
|
+
const jobId = await this.queueManager.addJob({
|
|
1248
|
+
driveId: drive,
|
|
1249
|
+
documentId: id,
|
|
1250
|
+
actions,
|
|
1251
|
+
forceSync
|
|
1252
|
+
});
|
|
877
1253
|
|
|
878
1254
|
return new Promise<IOperationResult>((resolve, reject) => {
|
|
879
|
-
const unsubscribe = this.queueManager.on(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1255
|
+
const unsubscribe = this.queueManager.on(
|
|
1256
|
+
'jobCompleted',
|
|
1257
|
+
(job, result) => {
|
|
1258
|
+
if (job.jobId === jobId) {
|
|
1259
|
+
unsubscribe();
|
|
1260
|
+
unsubscribeError();
|
|
1261
|
+
resolve(result);
|
|
1262
|
+
}
|
|
884
1263
|
}
|
|
885
|
-
|
|
886
|
-
const unsubscribeError = this.queueManager.on(
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
+
}
|
|
892
1273
|
}
|
|
893
|
-
|
|
894
|
-
})
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
895
1276
|
} catch (error) {
|
|
896
1277
|
logger.error('Error adding job', error);
|
|
897
1278
|
throw error;
|
|
898
1279
|
}
|
|
899
1280
|
}
|
|
900
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
|
+
|
|
901
1328
|
async addOperations(
|
|
902
1329
|
drive: string,
|
|
903
1330
|
id: string,
|
|
904
1331
|
operations: Operation[],
|
|
905
1332
|
forceSync = true
|
|
906
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
|
+
}
|
|
907
1343
|
let document: Document | undefined;
|
|
908
1344
|
const operationsApplied: Operation[] = [];
|
|
909
1345
|
const signals: SignalResult[] = [];
|
|
@@ -913,6 +1349,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
913
1349
|
await this._addOperations(drive, id, async documentStorage => {
|
|
914
1350
|
const result = await this._processOperations(
|
|
915
1351
|
drive,
|
|
1352
|
+
id,
|
|
916
1353
|
documentStorage,
|
|
917
1354
|
operations
|
|
918
1355
|
);
|
|
@@ -960,21 +1397,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
960
1397
|
.updateSynchronizationRevisions(
|
|
961
1398
|
drive,
|
|
962
1399
|
syncUnits,
|
|
963
|
-
() =>
|
|
1400
|
+
() => {
|
|
1401
|
+
this.updateSyncStatus(drive, 'SYNCING');
|
|
1402
|
+
|
|
1403
|
+
for (const syncUnit of syncUnits) {
|
|
1404
|
+
this.updateSyncStatus(syncUnit.syncId, 'SYNCING');
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
964
1407
|
this.handleListenerError.bind(this),
|
|
965
1408
|
forceSync
|
|
966
1409
|
)
|
|
967
|
-
.then(
|
|
968
|
-
updates
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
+
})
|
|
972
1417
|
.catch(error => {
|
|
973
1418
|
logger.error(
|
|
974
1419
|
'Non handled error updating sync revision',
|
|
975
1420
|
error
|
|
976
1421
|
);
|
|
977
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
|
+
}
|
|
978
1431
|
});
|
|
979
1432
|
|
|
980
1433
|
// after applying all the valid operations,throws
|
|
@@ -994,11 +1447,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
994
1447
|
error instanceof OperationError
|
|
995
1448
|
? error
|
|
996
1449
|
: new OperationError(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1450
|
+
'ERROR',
|
|
1451
|
+
undefined,
|
|
1452
|
+
(error as Error).message,
|
|
1453
|
+
(error as Error).cause
|
|
1454
|
+
);
|
|
1002
1455
|
|
|
1003
1456
|
return {
|
|
1004
1457
|
status: operationError.status,
|
|
@@ -1053,33 +1506,92 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1053
1506
|
}
|
|
1054
1507
|
}
|
|
1055
1508
|
|
|
1056
|
-
queueDriveOperation(
|
|
1509
|
+
queueDriveOperation(
|
|
1510
|
+
drive: string,
|
|
1511
|
+
operation: Operation<DocumentDriveAction | BaseAction>,
|
|
1512
|
+
forceSync = true
|
|
1513
|
+
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1057
1514
|
return this.queueDriveOperations(drive, [operation], forceSync);
|
|
1058
1515
|
}
|
|
1059
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
|
+
|
|
1060
1550
|
async queueDriveOperations(
|
|
1061
1551
|
drive: string,
|
|
1062
1552
|
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
1063
1553
|
forceSync = true
|
|
1064
1554
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
});
|
|
1074
|
-
const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
|
|
1075
|
-
if (job.jobId === jobId) {
|
|
1076
|
-
unsubscribe();
|
|
1077
|
-
unsubscribeError();
|
|
1078
|
-
reject(error);
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
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
|
+
}
|
|
1081
1563
|
|
|
1082
|
-
|
|
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
|
+
);
|
|
1083
1595
|
}
|
|
1084
1596
|
|
|
1085
1597
|
async addDriveOperations(
|
|
@@ -1093,12 +1605,23 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1093
1605
|
const signals: SignalResult[] = [];
|
|
1094
1606
|
let error: Error | undefined;
|
|
1095
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
|
+
|
|
1096
1619
|
try {
|
|
1097
1620
|
await this._addDriveOperations(drive, async documentStorage => {
|
|
1098
1621
|
const result = await this._processOperations<
|
|
1099
1622
|
DocumentDriveDocument,
|
|
1100
1623
|
DocumentDriveAction
|
|
1101
|
-
>(drive, documentStorage, operations.slice());
|
|
1624
|
+
>(drive, undefined, documentStorage, operations.slice());
|
|
1102
1625
|
|
|
1103
1626
|
document = result.document;
|
|
1104
1627
|
operationsApplied.push(...result.operationsApplied);
|
|
@@ -1107,7 +1630,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1107
1630
|
|
|
1108
1631
|
return {
|
|
1109
1632
|
operations: result.operationsApplied,
|
|
1110
|
-
header: result.document
|
|
1633
|
+
header: result.document
|
|
1111
1634
|
};
|
|
1112
1635
|
});
|
|
1113
1636
|
|
|
@@ -1132,6 +1655,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1132
1655
|
}
|
|
1133
1656
|
}
|
|
1134
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
|
+
|
|
1135
1671
|
// update listener cache
|
|
1136
1672
|
const lastOperation = operationsApplied
|
|
1137
1673
|
.filter(op => op.scope === 'global')
|
|
@@ -1153,21 +1689,49 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1153
1689
|
revision: lastOperation.index
|
|
1154
1690
|
}
|
|
1155
1691
|
],
|
|
1156
|
-
() =>
|
|
1692
|
+
() => {
|
|
1693
|
+
this.updateSyncStatus(drive, 'SYNCING');
|
|
1694
|
+
|
|
1695
|
+
for (const syncUnitId of [
|
|
1696
|
+
...newSyncUnits,
|
|
1697
|
+
...removedSyncUnits
|
|
1698
|
+
]) {
|
|
1699
|
+
this.updateSyncStatus(syncUnitId, 'SYNCING');
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1157
1702
|
this.handleListenerError.bind(this),
|
|
1158
1703
|
forceSync
|
|
1159
1704
|
)
|
|
1160
|
-
.then(
|
|
1161
|
-
updates
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
})
|
|
1165
1718
|
.catch(error => {
|
|
1166
1719
|
logger.error(
|
|
1167
1720
|
'Non handled error updating sync revision',
|
|
1168
1721
|
error
|
|
1169
1722
|
);
|
|
1170
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
|
+
}
|
|
1171
1735
|
});
|
|
1172
1736
|
}
|
|
1173
1737
|
|
|
@@ -1194,11 +1758,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1194
1758
|
error instanceof OperationError
|
|
1195
1759
|
? error
|
|
1196
1760
|
: new OperationError(
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1761
|
+
'ERROR',
|
|
1762
|
+
undefined,
|
|
1763
|
+
(error as Error).message,
|
|
1764
|
+
(error as Error).cause
|
|
1765
|
+
);
|
|
1202
1766
|
|
|
1203
1767
|
return {
|
|
1204
1768
|
status: operationError.status,
|
|
@@ -1230,36 +1794,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1230
1794
|
async addAction(
|
|
1231
1795
|
drive: string,
|
|
1232
1796
|
id: string,
|
|
1233
|
-
action: Action
|
|
1797
|
+
action: Action,
|
|
1798
|
+
forceSync = true
|
|
1234
1799
|
): Promise<IOperationResult> {
|
|
1235
|
-
return this.addActions(drive, id, [action]);
|
|
1800
|
+
return this.addActions(drive, id, [action], forceSync);
|
|
1236
1801
|
}
|
|
1237
1802
|
|
|
1238
1803
|
async addActions(
|
|
1239
1804
|
drive: string,
|
|
1240
1805
|
id: string,
|
|
1241
|
-
actions: Action[]
|
|
1806
|
+
actions: Action[],
|
|
1807
|
+
forceSync = true
|
|
1242
1808
|
): Promise<IOperationResult> {
|
|
1243
1809
|
const document = await this.getDocument(drive, id);
|
|
1244
1810
|
const operations = this._buildOperations(document, actions);
|
|
1245
|
-
return this.
|
|
1811
|
+
return this.addOperations(drive, id, operations, forceSync);
|
|
1246
1812
|
}
|
|
1247
1813
|
|
|
1248
1814
|
async addDriveAction(
|
|
1249
1815
|
drive: string,
|
|
1250
|
-
action: DocumentDriveAction | BaseAction
|
|
1816
|
+
action: DocumentDriveAction | BaseAction,
|
|
1817
|
+
forceSync = true
|
|
1251
1818
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1252
|
-
return this.addDriveActions(drive, [action]);
|
|
1819
|
+
return this.addDriveActions(drive, [action], forceSync);
|
|
1253
1820
|
}
|
|
1254
1821
|
|
|
1255
1822
|
async addDriveActions(
|
|
1256
1823
|
drive: string,
|
|
1257
|
-
actions: (DocumentDriveAction | BaseAction)[]
|
|
1824
|
+
actions: (DocumentDriveAction | BaseAction)[],
|
|
1825
|
+
forceSync = true
|
|
1258
1826
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1259
1827
|
const document = await this.getDrive(drive);
|
|
1260
1828
|
const operations = this._buildOperations(document, actions);
|
|
1261
|
-
const result = await this.
|
|
1262
|
-
|
|
1829
|
+
const result = await this.addDriveOperations(
|
|
1830
|
+
drive,
|
|
1831
|
+
operations,
|
|
1832
|
+
forceSync
|
|
1833
|
+
);
|
|
1834
|
+
return result;
|
|
1263
1835
|
}
|
|
1264
1836
|
|
|
1265
1837
|
async addInternalListener(
|