document-drive 1.0.0-alpha.84 → 1.0.0-alpha.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/queue/base.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import { Action } from 'document-model/document';
|
|
6
6
|
import { Unsubscribe, createNanoEvents } from 'nanoevents';
|
|
7
7
|
import { generateUUID } from '../utils';
|
|
8
|
+
import { logger } from '../utils/logger';
|
|
8
9
|
import {
|
|
9
10
|
IJob,
|
|
10
11
|
IJobQueue,
|
|
@@ -172,7 +173,6 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
172
173
|
const queue = this.getQueue(job.driveId, input.id);
|
|
173
174
|
await queue.setDeleted(true);
|
|
174
175
|
}
|
|
175
|
-
|
|
176
176
|
await queue.addJob({ jobId, ...job });
|
|
177
177
|
|
|
178
178
|
return jobId;
|
|
@@ -209,14 +209,30 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
209
209
|
return this.queues.map(q => q.getId());
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
private retryNextJob() {
|
|
212
|
+
private retryNextJob(timeout?: number) {
|
|
213
|
+
const _timeout = timeout !== undefined ? timeout : this.timeout;
|
|
213
214
|
const retry =
|
|
214
|
-
|
|
215
|
+
_timeout === 0 && typeof setImmediate !== 'undefined'
|
|
215
216
|
? setImmediate
|
|
216
|
-
: (fn: () => void) => setTimeout(fn,
|
|
217
|
+
: (fn: () => void) => setTimeout(fn, _timeout);
|
|
217
218
|
return retry(() => this.processNextJob());
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
private async findFirstNonEmptyQueue(
|
|
222
|
+
ticker: number
|
|
223
|
+
): Promise<number | null> {
|
|
224
|
+
const numQueues = this.queues.length;
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < numQueues; i++) {
|
|
227
|
+
const index = (ticker + i) % numQueues;
|
|
228
|
+
const queue = this.queues[index];
|
|
229
|
+
if (queue && (await queue.amountOfJobs()) > 0) {
|
|
230
|
+
return index;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
220
236
|
private async processNextJob() {
|
|
221
237
|
if (!this.delegate) {
|
|
222
238
|
throw new Error('No server delegate defined');
|
|
@@ -228,20 +244,30 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
const queue = this.queues[this.ticker];
|
|
231
|
-
this.ticker =
|
|
232
|
-
this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
|
|
233
247
|
if (!queue) {
|
|
234
248
|
this.ticker = 0;
|
|
235
249
|
this.retryNextJob();
|
|
236
250
|
return;
|
|
237
251
|
}
|
|
238
252
|
|
|
253
|
+
// if no jobs in the current queue then looks for the
|
|
254
|
+
// next queue with jobs. If no jobs in any queue then
|
|
255
|
+
// retries after a timeout
|
|
239
256
|
const amountOfJobs = await queue.amountOfJobs();
|
|
240
257
|
if (amountOfJobs === 0) {
|
|
241
|
-
this.
|
|
258
|
+
const nextTicker = await this.findFirstNonEmptyQueue(this.ticker);
|
|
259
|
+
if (nextTicker !== null) {
|
|
260
|
+
this.ticker = nextTicker;
|
|
261
|
+
this.retryNextJob(0);
|
|
262
|
+
} else {
|
|
263
|
+
this.retryNextJob();
|
|
264
|
+
}
|
|
242
265
|
return;
|
|
243
266
|
}
|
|
244
267
|
|
|
268
|
+
this.ticker =
|
|
269
|
+
this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
|
|
270
|
+
|
|
245
271
|
const isBlocked = await queue.isBlocked();
|
|
246
272
|
if (isBlocked) {
|
|
247
273
|
this.retryNextJob();
|
|
@@ -274,10 +300,11 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
274
300
|
}
|
|
275
301
|
this.emit('jobCompleted', nextJob, result);
|
|
276
302
|
} catch (e) {
|
|
303
|
+
logger.error(`job failed`, e);
|
|
277
304
|
this.emit('jobFailed', nextJob, e as Error);
|
|
278
305
|
} finally {
|
|
279
306
|
await queue.setBlocked(false);
|
|
280
|
-
|
|
307
|
+
this.retryNextJob(0);
|
|
281
308
|
}
|
|
282
309
|
}
|
|
283
310
|
|
package/src/server/index.ts
CHANGED
|
@@ -220,6 +220,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
223
|
+
let firstPull = true;
|
|
223
224
|
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
224
225
|
driveId,
|
|
225
226
|
trigger,
|
|
@@ -269,6 +270,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
269
270
|
);
|
|
270
271
|
}
|
|
271
272
|
}
|
|
273
|
+
|
|
274
|
+
// if it is the first pull and returns empty
|
|
275
|
+
// then updates corresponding push transmitter
|
|
276
|
+
if (firstPull) {
|
|
277
|
+
firstPull = false;
|
|
278
|
+
const pushListener =
|
|
279
|
+
drive.state.local.listeners.find(
|
|
280
|
+
listener =>
|
|
281
|
+
trigger.data.url ===
|
|
282
|
+
listener.callInfo?.data
|
|
283
|
+
);
|
|
284
|
+
if (pushListener) {
|
|
285
|
+
this.getSynchronizationUnitsRevision(
|
|
286
|
+
driveId,
|
|
287
|
+
syncUnits
|
|
288
|
+
)
|
|
289
|
+
.then(syncUnitRevisions => {
|
|
290
|
+
for (const revision of syncUnitRevisions) {
|
|
291
|
+
this.listenerStateManager
|
|
292
|
+
.updateListenerRevision(
|
|
293
|
+
pushListener.listenerId,
|
|
294
|
+
driveId,
|
|
295
|
+
revision.syncId,
|
|
296
|
+
revision.revision
|
|
297
|
+
)
|
|
298
|
+
.catch(logger.error);
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.catch(logger.error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
272
304
|
}
|
|
273
305
|
);
|
|
274
306
|
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
@@ -405,16 +437,30 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
405
437
|
documentType,
|
|
406
438
|
drive
|
|
407
439
|
);
|
|
408
|
-
|
|
409
|
-
|
|
440
|
+
return this.getSynchronizationUnitsRevision(
|
|
441
|
+
driveId,
|
|
442
|
+
synchronizationUnitsQuery,
|
|
443
|
+
drive
|
|
410
444
|
);
|
|
445
|
+
}
|
|
411
446
|
|
|
412
|
-
|
|
413
|
-
|
|
447
|
+
public async getSynchronizationUnitsRevision(
|
|
448
|
+
driveId: string,
|
|
449
|
+
syncUnitsQuery: SynchronizationUnitQuery[],
|
|
450
|
+
loadedDrive?: DocumentDriveDocument
|
|
451
|
+
): Promise<SynchronizationUnit[]> {
|
|
452
|
+
const drive = loadedDrive || (await this.getDrive(driveId));
|
|
453
|
+
|
|
454
|
+
const revisions =
|
|
455
|
+
await this.storage.getSynchronizationUnitsRevision(syncUnitsQuery);
|
|
456
|
+
|
|
457
|
+
const synchronizationUnits: SynchronizationUnit[] = syncUnitsQuery.map(
|
|
458
|
+
s => ({
|
|
414
459
|
...s,
|
|
415
460
|
lastUpdated: drive.created,
|
|
416
461
|
revision: -1
|
|
417
|
-
})
|
|
462
|
+
})
|
|
463
|
+
);
|
|
418
464
|
for (const revision of revisions) {
|
|
419
465
|
const syncUnit = synchronizationUnits.find(
|
|
420
466
|
s =>
|
|
@@ -160,6 +160,15 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
160
160
|
) {
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
const transmitter = await this.getTransmitter(
|
|
165
|
+
driveId,
|
|
166
|
+
listener.listener.listenerId
|
|
167
|
+
);
|
|
168
|
+
if (!transmitter?.transmit) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
for (const syncUnit of syncUnits) {
|
|
164
173
|
if (!this._checkFilter(listener.listener.filter, syncUnit)) {
|
|
165
174
|
continue;
|
|
@@ -229,7 +238,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
229
238
|
for (const [driveId, drive] of this.listenerState) {
|
|
230
239
|
for (const [id, listener] of drive) {
|
|
231
240
|
const transmitter = await this.getTransmitter(driveId, id);
|
|
232
|
-
if (!transmitter) {
|
|
241
|
+
if (!transmitter?.transmit) {
|
|
233
242
|
continue;
|
|
234
243
|
}
|
|
235
244
|
|
|
@@ -239,6 +248,8 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
239
248
|
);
|
|
240
249
|
|
|
241
250
|
const strandUpdates: StrandUpdate[] = [];
|
|
251
|
+
|
|
252
|
+
// TODO change to push one after the other, reusing operation data
|
|
242
253
|
await Promise.all(
|
|
243
254
|
syncUnits.map(async syncUnit => {
|
|
244
255
|
const unitState = listener.syncUnits.get(
|
|
@@ -292,7 +303,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
292
303
|
|
|
293
304
|
// TODO update listeners in parallel, blocking for listeners with block=true
|
|
294
305
|
try {
|
|
295
|
-
const listenerRevisions = await transmitter
|
|
306
|
+
const listenerRevisions = await transmitter.transmit(
|
|
296
307
|
strandUpdates,
|
|
297
308
|
source
|
|
298
309
|
);
|
|
@@ -321,22 +332,31 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
321
332
|
);
|
|
322
333
|
}
|
|
323
334
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
335
|
+
|
|
336
|
+
for (const revision of listenerRevisions) {
|
|
337
|
+
const error = revision.status === 'ERROR';
|
|
338
|
+
if (revision.error?.includes('Missing operations')) {
|
|
339
|
+
const updates = await this._triggerUpdate(
|
|
340
|
+
source,
|
|
341
|
+
onError
|
|
342
|
+
);
|
|
343
|
+
listenerUpdates.push(...updates);
|
|
344
|
+
} else {
|
|
345
|
+
listenerUpdates.push({
|
|
346
|
+
listenerId: listener.listener.listenerId,
|
|
347
|
+
listenerRevisions
|
|
348
|
+
});
|
|
349
|
+
if (error) {
|
|
350
|
+
throw new OperationError(
|
|
351
|
+
revision.status as ErrorStatus,
|
|
352
|
+
undefined,
|
|
353
|
+
revision.error,
|
|
354
|
+
revision.error
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
334
358
|
}
|
|
335
359
|
listener.listenerStatus = 'SUCCESS';
|
|
336
|
-
listenerUpdates.push({
|
|
337
|
-
listenerId: listener.listener.listenerId,
|
|
338
|
-
listenerRevisions
|
|
339
|
-
});
|
|
340
360
|
} catch (e) {
|
|
341
361
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
342
362
|
onError?.(e as Error, driveId, listener);
|
|
@@ -59,10 +59,6 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
59
59
|
this.manager = manager;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
async transmit(): Promise<ListenerRevision[]> {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
62
|
getStrands(since?: string | undefined): Promise<StrandUpdate[]> {
|
|
67
63
|
return this.manager.getStrands(
|
|
68
64
|
this.listener.driveId,
|
package/src/storage/prisma.ts
CHANGED
|
@@ -80,9 +80,13 @@ function getRetryTransactionsClient<T extends PrismaClient>(
|
|
|
80
80
|
// eslint-disable-next-line prefer-spread
|
|
81
81
|
return backOff(() => prisma.$transaction.apply(prisma, args), {
|
|
82
82
|
retry: e => {
|
|
83
|
+
const code = (e as { code: string }).code;
|
|
83
84
|
// Retry the transaction only if the error was due to a write conflict or deadlock
|
|
84
85
|
// See: https://www.prisma.io/docs/reference/api-reference/error-reference#p2034
|
|
85
|
-
|
|
86
|
+
if (code !== 'P2034') {
|
|
87
|
+
logger.error('TRANSACTION ERROR', e);
|
|
88
|
+
}
|
|
89
|
+
return code === 'P2034';
|
|
86
90
|
},
|
|
87
91
|
...backOffOptions
|
|
88
92
|
});
|
|
@@ -502,12 +506,13 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
502
506
|
id: id
|
|
503
507
|
}
|
|
504
508
|
});
|
|
505
|
-
} catch (e:
|
|
509
|
+
} catch (e: unknown) {
|
|
510
|
+
const prismaError = e as { code?: string; message?: string };
|
|
506
511
|
// Ignore Error: P2025: An operation failed because it depends on one or more records that were required but not found.
|
|
507
512
|
if (
|
|
508
|
-
(
|
|
509
|
-
(
|
|
510
|
-
|
|
513
|
+
(prismaError.code && prismaError.code === 'P2025') ||
|
|
514
|
+
(prismaError.message &&
|
|
515
|
+
prismaError.message.includes(
|
|
511
516
|
'An operation failed because it depends on one or more records that were required but not found.'
|
|
512
517
|
))
|
|
513
518
|
) {
|