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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.0.0-alpha.84",
3
+ "version": "1.0.0-alpha.85",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
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
- this.timeout === 0 && typeof setImmediate !== 'undefined'
215
+ _timeout === 0 && typeof setImmediate !== 'undefined'
215
216
  ? setImmediate
216
- : (fn: () => void) => setTimeout(fn, this.timeout);
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.retryNextJob();
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
- await this.processNextJob();
307
+ this.retryNextJob(0);
281
308
  }
282
309
  }
283
310
 
@@ -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
- const revisions = await this.storage.getSynchronizationUnitsRevision(
409
- synchronizationUnitsQuery
440
+ return this.getSynchronizationUnitsRevision(
441
+ driveId,
442
+ synchronizationUnitsQuery,
443
+ drive
410
444
  );
445
+ }
411
446
 
412
- const synchronizationUnits: SynchronizationUnit[] =
413
- synchronizationUnitsQuery.map(s => ({
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?.transmit(
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
- const revisionError = listenerRevisions.find(
325
- l => l.status !== 'SUCCESS'
326
- );
327
- if (revisionError) {
328
- throw new OperationError(
329
- revisionError.status as ErrorStatus,
330
- undefined,
331
- revisionError.error,
332
- revisionError.error
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,
@@ -11,7 +11,7 @@ export type StrandUpdateSource =
11
11
  | { type: 'trigger'; trigger: Trigger };
12
12
 
13
13
  export interface ITransmitter {
14
- transmit(
14
+ transmit?(
15
15
  strands: StrandUpdate[],
16
16
  source: StrandUpdateSource
17
17
  ): Promise<ListenerRevision[]>;
@@ -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
- return (e as { code: string }).code === 'P2034';
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: any) {
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
- (e.code && e.code === 'P2025') ||
509
- (e.message &&
510
- e.message.includes(
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
  ) {