document-drive 1.0.0-alpha.83 → 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/README.md CHANGED
@@ -1 +1 @@
1
- # Document Drive
1
+ # Document Drive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.0.0-alpha.83",
3
+ "version": "1.0.0-alpha.85",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -1,5 +1,5 @@
1
- import { Document } from "document-model/document";
2
- import { ICache } from "./types";
1
+ import { Document } from 'document-model/document';
2
+ import { ICache } from './types';
3
3
 
4
4
  class InMemoryCache implements ICache {
5
5
  private cache = new Map<string, Map<string, Document>>();
@@ -13,7 +13,7 @@ class InMemoryCache implements ICache {
13
13
  delete e.resultingState;
14
14
  return e;
15
15
  });
16
- const doc = { ...document, operations: { global, local } }
16
+ const doc = { ...document, operations: { global, local } };
17
17
  if (!this.cache.has(drive)) {
18
18
  this.cache.set(drive, new Map());
19
19
  }
@@ -1,12 +1,15 @@
1
- import { Document } from "document-model/document";
2
- import { ICache } from "./types";
3
- import type { RedisClientType } from "redis";
1
+ import { Document } from 'document-model/document';
2
+ import type { RedisClientType } from 'redis';
3
+ import { ICache } from './types';
4
4
 
5
5
  class RedisCache implements ICache {
6
6
  private redis: RedisClientType;
7
7
  private timeoutInSeconds: number;
8
8
 
9
- constructor(redis: RedisClientType, timeoutInSeconds: number | undefined = 5 * 60) {
9
+ constructor(
10
+ redis: RedisClientType,
11
+ timeoutInSeconds: number | undefined = 5 * 60
12
+ ) {
10
13
  this.redis = redis;
11
14
  this.timeoutInSeconds = timeoutInSeconds;
12
15
  }
@@ -24,7 +27,7 @@ class RedisCache implements ICache {
24
27
  delete e.resultingState;
25
28
  return e;
26
29
  });
27
- const doc = { ...document, operations: { global, local } }
30
+ const doc = { ...document, operations: { global, local } };
28
31
  const redisId = RedisCache._getId(drive, id);
29
32
  const result = await this.redis.set(redisId, JSON.stringify(doc), {
30
33
  EX: this.timeoutInSeconds ? this.timeoutInSeconds : undefined
@@ -41,7 +44,7 @@ class RedisCache implements ICache {
41
44
  const redisId = RedisCache._getId(drive, id);
42
45
  const doc = await this.redis.get(redisId);
43
46
 
44
- return doc ? JSON.parse(doc) as Document : undefined;
47
+ return doc ? (JSON.parse(doc) as Document) : undefined;
45
48
  }
46
49
 
47
50
  async deleteDocument(drive: string, id: string) {
@@ -1,9 +1,13 @@
1
- import type { Document } from "document-model/document";
1
+ import type { Document } from 'document-model/document';
2
2
 
3
3
  export interface ICache {
4
- setDocument(drive: string, id: string, document: Document): Promise<boolean>
5
- getDocument(drive: string, id: string): Promise<Document | undefined>
4
+ setDocument(
5
+ drive: string,
6
+ id: string,
7
+ document: Document
8
+ ): Promise<boolean>;
9
+ getDocument(drive: string, id: string): Promise<Document | undefined>;
6
10
 
7
11
  // @returns — true if a document existed and has been removed, or false if the document is not cached.
8
- deleteDocument(drive: string, id: string): Promise<boolean>
12
+ deleteDocument(drive: string, id: string): Promise<boolean>;
9
13
  }
package/src/queue/base.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { IJob, IJobQueue, IQueue, IQueueManager, IServerDelegate, JobId, Job, QueueEvents, isOperationJob } from "./types";
2
- import { generateUUID } from "../utils";
3
- import { createNanoEvents, Unsubscribe } from 'nanoevents';
4
- import { Action } from "document-model/document";
5
- import { AddFileInput, DeleteNodeInput } from "document-model-libs/document-drive";
1
+ import {
2
+ AddFileInput,
3
+ DeleteNodeInput
4
+ } from 'document-model-libs/document-drive';
5
+ import { Action } from 'document-model/document';
6
+ import { Unsubscribe, createNanoEvents } from 'nanoevents';
7
+ import { generateUUID } from '../utils';
8
+ import { logger } from '../utils/logger';
9
+ import {
10
+ IJob,
11
+ IJobQueue,
12
+ IQueue,
13
+ IQueueManager,
14
+ IServerDelegate,
15
+ Job,
16
+ JobId,
17
+ QueueEvents,
18
+ isOperationJob
19
+ } from './types';
6
20
 
7
21
  export class MemoryQueue<T, R> implements IQueue<T, R> {
8
22
  private id: string;
@@ -63,7 +77,9 @@ export class MemoryQueue<T, R> implements IQueue<T, R> {
63
77
  }
64
78
 
65
79
  async removeDependencies(job: IJob<Job>) {
66
- this.dependencies = this.dependencies.filter((j) => j.jobId !== job.jobId && j.driveId !== job.driveId);
80
+ this.dependencies = this.dependencies.filter(
81
+ j => j.jobId !== job.jobId && j.driveId !== job.driveId
82
+ );
67
83
  if (this.dependencies.length === 0) {
68
84
  await this.setBlocked(false);
69
85
  }
@@ -83,29 +99,40 @@ export class BaseQueueManager implements IQueueManager {
83
99
  this.timeout = timeout;
84
100
  }
85
101
 
86
- async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
102
+ async init(
103
+ delegate: IServerDelegate,
104
+ onError: (error: Error) => void
105
+ ): Promise<void> {
87
106
  this.delegate = delegate;
88
107
  for (let i = 0; i < this.workers; i++) {
89
- setTimeout(() => this.processNextJob.bind(this)().catch(onError), 100 * i);
108
+ setTimeout(
109
+ () => this.processNextJob.bind(this)().catch(onError),
110
+ 100 * i
111
+ );
90
112
  }
91
- return Promise.resolve()
113
+ return Promise.resolve();
92
114
  }
93
115
 
94
116
  async addJob(job: Job): Promise<JobId> {
95
117
  if (!this.delegate) {
96
- throw new Error("No server delegate defined");
118
+ throw new Error('No server delegate defined');
97
119
  }
98
120
 
99
121
  const jobId = generateUUID();
100
122
  const queue = this.getQueue(job.driveId, job.documentId);
101
123
 
102
124
  if (await queue.isDeleted()) {
103
- throw new Error("Queue is deleted")
125
+ throw new Error('Queue is deleted');
104
126
  }
105
127
 
106
128
  // checks if the job is for a document that doesn't exist in storage yet
107
- const newDocument = job.documentId && !(await this.delegate.checkDocumentExists(job.driveId, job.documentId));
108
- // if it is a new document and queue is not yet blocked then
129
+ const newDocument =
130
+ job.documentId &&
131
+ !(await this.delegate.checkDocumentExists(
132
+ job.driveId,
133
+ job.documentId
134
+ ));
135
+ // if it is a new document and queue is not yet blocked then
109
136
  // blocks it so the jobs are not processed until it's ready
110
137
  if (newDocument && !(await queue.isBlocked())) {
111
138
  await queue.setBlocked(true);
@@ -114,11 +141,13 @@ export class BaseQueueManager implements IQueueManager {
114
141
  const driveQueue = this.getQueue(job.driveId);
115
142
  const jobs = await driveQueue.getJobs();
116
143
  for (const driveJob of jobs) {
117
- const actions = isOperationJob(driveJob) ? driveJob.operations : driveJob.actions;
144
+ const actions = isOperationJob(driveJob)
145
+ ? driveJob.operations
146
+ : driveJob.actions;
118
147
  const op = actions.find((j: Action) => {
119
148
  const input = j.input as AddFileInput;
120
- return j.type === "ADD_FILE" && input.id === job.documentId
121
- })
149
+ return j.type === 'ADD_FILE' && input.id === job.documentId;
150
+ });
122
151
  if (op) {
123
152
  await queue.addDependencies(driveJob);
124
153
  }
@@ -128,21 +157,22 @@ export class BaseQueueManager implements IQueueManager {
128
157
  // if it has ADD_FILE operations then adds the job as
129
158
  // a dependency to the corresponding document queues
130
159
  const actions = isOperationJob(job) ? job.operations : job.actions;
131
- const addFileOps = actions.filter((j: Action) => j.type === "ADD_FILE");
160
+ const addFileOps = actions.filter((j: Action) => j.type === 'ADD_FILE');
132
161
  for (const addFileOp of addFileOps) {
133
162
  const input = addFileOp.input as AddFileInput;
134
- const q = this.getQueue(job.driveId, input.id)
163
+ const q = this.getQueue(job.driveId, input.id);
135
164
  await q.addDependencies({ jobId, ...job });
136
165
  }
137
166
 
138
167
  // remove document if operations contains delete_node
139
- const removeFileOps = actions.filter((j: Action) => j.type === "DELETE_NODE");
168
+ const removeFileOps = actions.filter(
169
+ (j: Action) => j.type === 'DELETE_NODE'
170
+ );
140
171
  for (const removeFileOp of removeFileOps) {
141
172
  const input = removeFileOp.input as DeleteNodeInput;
142
173
  const queue = this.getQueue(job.driveId, input.id);
143
174
  await queue.setDeleted(true);
144
175
  }
145
-
146
176
  await queue.addJob({ jobId, ...job });
147
177
 
148
178
  return jobId;
@@ -150,7 +180,7 @@ export class BaseQueueManager implements IQueueManager {
150
180
 
151
181
  getQueue(driveId: string, documentId?: string) {
152
182
  const queueId = this.getQueueId(driveId, documentId);
153
- let queue = this.queues.find((q) => q.getId() === queueId);
183
+ let queue = this.queues.find(q => q.getId() === queueId);
154
184
 
155
185
  if (!queue) {
156
186
  queue = new MemoryQueue(queueId);
@@ -162,8 +192,8 @@ export class BaseQueueManager implements IQueueManager {
162
192
 
163
193
  removeQueue(driveId: string, documentId?: string) {
164
194
  const queueId = this.getQueueId(driveId, documentId);
165
- this.queues = this.queues.filter((q) => q.getId() !== queueId);
166
- this.emit("queueRemoved", queueId)
195
+ this.queues = this.queues.filter(q => q.getId() !== queueId);
196
+ this.emit('queueRemoved', queueId);
167
197
  }
168
198
 
169
199
  getQueueByIndex(index: number) {
@@ -176,17 +206,36 @@ export class BaseQueueManager implements IQueueManager {
176
206
  }
177
207
 
178
208
  getQueues() {
179
- return Object.keys(new Array(this.queues));
209
+ return this.queues.map(q => q.getId());
180
210
  }
181
211
 
182
- private retryNextJob() {
183
- const retry = this.timeout === 0 && typeof setImmediate !== "undefined" ? setImmediate : (fn: () => void) => setTimeout(fn, this.timeout);
212
+ private retryNextJob(timeout?: number) {
213
+ const _timeout = timeout !== undefined ? timeout : this.timeout;
214
+ const retry =
215
+ _timeout === 0 && typeof setImmediate !== 'undefined'
216
+ ? setImmediate
217
+ : (fn: () => void) => setTimeout(fn, _timeout);
184
218
  return retry(() => this.processNextJob());
185
219
  }
186
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
+
187
236
  private async processNextJob() {
188
237
  if (!this.delegate) {
189
- throw new Error("No server delegate defined");
238
+ throw new Error('No server delegate defined');
190
239
  }
191
240
 
192
241
  if (this.queues.length === 0) {
@@ -195,19 +244,30 @@ export class BaseQueueManager implements IQueueManager {
195
244
  }
196
245
 
197
246
  const queue = this.queues[this.ticker];
198
- this.ticker = this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
199
247
  if (!queue) {
200
248
  this.ticker = 0;
201
249
  this.retryNextJob();
202
250
  return;
203
251
  }
204
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
205
256
  const amountOfJobs = await queue.amountOfJobs();
206
257
  if (amountOfJobs === 0) {
207
- 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
+ }
208
265
  return;
209
266
  }
210
267
 
268
+ this.ticker =
269
+ this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
270
+
211
271
  const isBlocked = await queue.isBlocked();
212
272
  if (isBlocked) {
213
273
  this.retryNextJob();
@@ -225,20 +285,26 @@ export class BaseQueueManager implements IQueueManager {
225
285
  const result = await this.delegate.processJob(nextJob);
226
286
 
227
287
  // unblock the document queues of each add_file operation
228
- const actions = isOperationJob(nextJob) ? nextJob.operations : nextJob.actions;
229
- const addFileActions = actions.filter((op) => op.type === "ADD_FILE");
288
+ const actions = isOperationJob(nextJob)
289
+ ? nextJob.operations
290
+ : nextJob.actions;
291
+ const addFileActions = actions.filter(op => op.type === 'ADD_FILE');
230
292
  if (addFileActions.length > 0) {
231
293
  for (const addFile of addFileActions) {
232
- const documentQueue = this.getQueue(nextJob.driveId, (addFile.input as AddFileInput).id);
294
+ const documentQueue = this.getQueue(
295
+ nextJob.driveId,
296
+ (addFile.input as AddFileInput).id
297
+ );
233
298
  await documentQueue.removeDependencies(nextJob);
234
- };
299
+ }
235
300
  }
236
- this.emit("jobCompleted", nextJob, result);
301
+ this.emit('jobCompleted', nextJob, result);
237
302
  } catch (e) {
238
- this.emit("jobFailed", nextJob, e as Error);
303
+ logger.error(`job failed`, e);
304
+ this.emit('jobFailed', nextJob, e as Error);
239
305
  } finally {
240
306
  await queue.setBlocked(false);
241
- await this.processNextJob();
307
+ this.retryNextJob(0);
242
308
  }
243
309
  }
244
310
 
@@ -248,11 +314,15 @@ export class BaseQueueManager implements IQueueManager {
248
314
  ) {
249
315
  this.emitter.emit(event, ...args);
250
316
  }
251
- on<K extends keyof QueueEvents>(this: this, event: K, cb: QueueEvents[K]): Unsubscribe {
317
+ on<K extends keyof QueueEvents>(
318
+ this: this,
319
+ event: K,
320
+ cb: QueueEvents[K]
321
+ ): Unsubscribe {
252
322
  return this.emitter.on(event, cb);
253
323
  }
254
324
 
255
325
  protected getQueueId(driveId: string, documentId?: string) {
256
326
  return `queue:${driveId}${documentId ? `:${documentId}` : ''}`;
257
327
  }
258
- }
328
+ }
@@ -1,2 +1,2 @@
1
1
  export * from './base';
2
- export * from './types';
2
+ export * from './types';
@@ -1,6 +1,12 @@
1
- import { RedisClientType } from "redis";
2
- import { IJob, IQueue, IQueueManager, IServerDelegate, OperationJob } from "./types";
3
- import { BaseQueueManager } from "./base";
1
+ import { RedisClientType } from 'redis';
2
+ import { BaseQueueManager } from './base';
3
+ import {
4
+ IJob,
5
+ IQueue,
6
+ IQueueManager,
7
+ IServerDelegate,
8
+ OperationJob
9
+ } from './types';
4
10
 
5
11
  export class RedisQueue<T, R> implements IQueue<T, R> {
6
12
  private id: string;
@@ -9,16 +15,16 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
9
15
  constructor(id: string, client: RedisClientType) {
10
16
  this.client = client;
11
17
  this.id = id;
12
- this.client.hSet("queues", id, "true");
13
- this.client.hSet(this.id, "blocked", "false");
18
+ this.client.hSet('queues', id, 'true');
19
+ this.client.hSet(this.id, 'blocked', 'false');
14
20
  }
15
21
 
16
22
  async addJob(data: any) {
17
- await this.client.lPush(this.id + "-jobs", JSON.stringify(data));
23
+ await this.client.lPush(this.id + '-jobs', JSON.stringify(data));
18
24
  }
19
25
 
20
26
  async getNextJob() {
21
- const job = await this.client.rPop(this.id + "-jobs");
27
+ const job = await this.client.rPop(this.id + '-jobs');
22
28
  if (!job) {
23
29
  return undefined;
24
30
  }
@@ -26,20 +32,20 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
26
32
  }
27
33
 
28
34
  async amountOfJobs() {
29
- return this.client.lLen(this.id + "-jobs");
35
+ return this.client.lLen(this.id + '-jobs');
30
36
  }
31
37
 
32
38
  async setBlocked(blocked: boolean) {
33
39
  if (blocked) {
34
- await this.client.hSet(this.id, "blocked", "true");
40
+ await this.client.hSet(this.id, 'blocked', 'true');
35
41
  } else {
36
- await this.client.hSet(this.id, "blocked", "false");
42
+ await this.client.hSet(this.id, 'blocked', 'false');
37
43
  }
38
44
  }
39
45
 
40
46
  async isBlocked() {
41
- const blockedResult = await this.client.hGet(this.id, "blocked");
42
- if (blockedResult === "true") {
47
+ const blockedResult = await this.client.hGet(this.id, 'blocked');
48
+ if (blockedResult === 'true') {
43
49
  return true;
44
50
  }
45
51
 
@@ -51,7 +57,7 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
51
57
  }
52
58
 
53
59
  async getJobs() {
54
- const entries = await this.client.lRange(this.id + "-jobs", 0, -1)
60
+ const entries = await this.client.lRange(this.id + '-jobs', 0, -1);
55
61
  return entries.map(e => JSON.parse(e) as IJob<T>);
56
62
  }
57
63
 
@@ -59,18 +65,18 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
59
65
  if (await this.hasDependency(job)) {
60
66
  return;
61
67
  }
62
- await this.client.lPush(this.id + "-deps", JSON.stringify(job));
68
+ await this.client.lPush(this.id + '-deps', JSON.stringify(job));
63
69
  await this.setBlocked(true);
64
70
  }
65
71
 
66
72
  async hasDependency(job: IJob<OperationJob>) {
67
- const deps = await this.client.lRange(this.id + "-deps", 0, -1);
73
+ const deps = await this.client.lRange(this.id + '-deps', 0, -1);
68
74
  return deps.some(d => d === JSON.stringify(job));
69
75
  }
70
76
 
71
77
  async removeDependencies(job: IJob<OperationJob>) {
72
- await this.client.lRem(this.id + "-deps", 1, JSON.stringify(job));
73
- const allDeps = await this.client.lLen(this.id + "-deps");
78
+ await this.client.lRem(this.id + '-deps', 1, JSON.stringify(job));
79
+ const allDeps = await this.client.lLen(this.id + '-deps');
74
80
  if (allDeps > 0) {
75
81
  await this.setBlocked(true);
76
82
  } else {
@@ -79,21 +85,23 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
79
85
  }
80
86
 
81
87
  async isDeleted() {
82
- const active = await this.client.hGet("queues", this.id);
83
- return active === "false";
88
+ const active = await this.client.hGet('queues', this.id);
89
+ return active === 'false';
84
90
  }
85
91
 
86
92
  async setDeleted(deleted: boolean) {
87
93
  if (deleted) {
88
- await this.client.hSet("queues", this.id, "false");
94
+ await this.client.hSet('queues', this.id, 'false');
89
95
  } else {
90
- await this.client.hSet("queues", this.id, "true");
96
+ await this.client.hSet('queues', this.id, 'true');
91
97
  }
92
98
  }
93
99
  }
94
100
 
95
- export class RedisQueueManager extends BaseQueueManager implements IQueueManager {
96
-
101
+ export class RedisQueueManager
102
+ extends BaseQueueManager
103
+ implements IQueueManager
104
+ {
97
105
  private client: RedisClientType;
98
106
 
99
107
  constructor(workers = 3, timeout = 0, client: RedisClientType) {
@@ -101,12 +109,15 @@ export class RedisQueueManager extends BaseQueueManager implements IQueueManager
101
109
  this.client = client;
102
110
  }
103
111
 
104
- async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
112
+ async init(
113
+ delegate: IServerDelegate,
114
+ onError: (error: Error) => void
115
+ ): Promise<void> {
105
116
  await super.init(delegate, onError);
106
- const queues = await this.client.hGetAll("queues");
117
+ const queues = await this.client.hGetAll('queues');
107
118
  for (const queueId in queues) {
108
- const active = await this.client.hGet("queues", queueId);
109
- if (active === "true") {
119
+ const active = await this.client.hGet('queues', queueId);
120
+ if (active === 'true') {
110
121
  this.queues.push(new RedisQueue(queueId, this.client));
111
122
  }
112
123
  }
@@ -114,7 +125,7 @@ export class RedisQueueManager extends BaseQueueManager implements IQueueManager
114
125
 
115
126
  getQueue(driveId: string, documentId?: string) {
116
127
  const queueId = this.getQueueId(driveId, documentId);
117
- let queue = this.queues.find((q) => q.getId() === queueId);
128
+ let queue = this.queues.find(q => q.getId() === queueId);
118
129
 
119
130
  if (!queue) {
120
131
  queue = new RedisQueue(queueId, this.client);
@@ -128,6 +139,6 @@ export class RedisQueueManager extends BaseQueueManager implements IQueueManager
128
139
  super.removeQueue(driveId, documentId);
129
140
 
130
141
  const queueId = this.getQueueId(driveId, documentId);
131
- this.client.hDel("queues", queueId);
142
+ this.client.hDel('queues', queueId);
132
143
  }
133
- }
144
+ }
@@ -1,20 +1,20 @@
1
- import { Action, Operation } from "document-model/document";
2
- import { AddOperationOptions, IOperationResult } from "../server";
3
- import type { Unsubscribe } from "nanoevents";
1
+ import { Action, Operation } from 'document-model/document';
2
+ import type { Unsubscribe } from 'nanoevents';
3
+ import { AddOperationOptions, IOperationResult } from '../server';
4
4
 
5
5
  export interface BaseJob {
6
6
  driveId: string;
7
- documentId?: string
8
- actions?: Action[]
9
- options?: AddOperationOptions;
7
+ documentId?: string;
8
+ actions?: Action[];
9
+ options?: AddOperationOptions;
10
10
  }
11
11
 
12
12
  export interface OperationJob extends BaseJob {
13
- operations: Operation[]
13
+ operations: Operation[];
14
14
  }
15
15
 
16
16
  export interface ActionJob extends BaseJob {
17
- actions: Action[]
17
+ actions: Action[];
18
18
  }
19
19
 
20
20
  export type Job = OperationJob | ActionJob;
@@ -28,9 +28,12 @@ export interface QueueEvents {
28
28
  }
29
29
 
30
30
  export interface IServerDelegate {
31
- checkDocumentExists: (driveId: string, documentId: string) => Promise<boolean>;
31
+ checkDocumentExists: (
32
+ driveId: string,
33
+ documentId: string
34
+ ) => Promise<boolean>;
32
35
  processJob: (job: Job) => Promise<IOperationResult>;
33
- };
36
+ }
34
37
 
35
38
  export interface IQueueManager {
36
39
  addJob(job: Job): Promise<JobId>;
@@ -38,7 +41,10 @@ export interface IQueueManager {
38
41
  removeQueue(driveId: string, documentId?: string): void;
39
42
  getQueueByIndex(index: number): IQueue<Job, IOperationResult> | null;
40
43
  getQueues(): string[];
41
- init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void>;
44
+ init(
45
+ delegate: IServerDelegate,
46
+ onError: (error: Error) => void
47
+ ): Promise<void>;
42
48
  on<K extends keyof QueueEvents>(
43
49
  this: this,
44
50
  event: K,
@@ -65,9 +71,9 @@ export interface IQueue<T, R> {
65
71
  export type IJobQueue = IQueue<Job, IOperationResult>;
66
72
 
67
73
  export function isOperationJob(job: Job): job is OperationJob {
68
- return "operations" in job;
74
+ return 'operations' in job;
69
75
  }
70
76
 
71
77
  export function isActionJob(job: Job): job is ActionJob {
72
- return "actions" in job;
73
- }
78
+ return 'actions' in job;
79
+ }
@@ -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
+ }
446
+
447
+ public async getSynchronizationUnitsRevision(
448
+ driveId: string,
449
+ syncUnitsQuery: SynchronizationUnitQuery[],
450
+ loadedDrive?: DocumentDriveDocument
451
+ ): Promise<SynchronizationUnit[]> {
452
+ const drive = loadedDrive || (await this.getDrive(driveId));
411
453
 
412
- const synchronizationUnits: SynchronizationUnit[] =
413
- synchronizationUnitsQuery.map(s => ({
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 =>
@@ -1368,37 +1414,42 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1368
1414
  actions: (DocumentDriveAction | BaseAction)[],
1369
1415
  options?: AddOperationOptions
1370
1416
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1371
- const jobId = await this.queueManager.addJob({
1372
- driveId: drive,
1373
- actions,
1374
- options
1375
- });
1376
- return new Promise<IOperationResult<DocumentDriveDocument>>(
1377
- (resolve, reject) => {
1378
- const unsubscribe = this.queueManager.on(
1379
- 'jobCompleted',
1380
- (job, result) => {
1381
- if (job.jobId === jobId) {
1382
- unsubscribe();
1383
- unsubscribeError();
1384
- resolve(
1385
- result as IOperationResult<DocumentDriveDocument>
1386
- );
1417
+ try {
1418
+ const jobId = await this.queueManager.addJob({
1419
+ driveId: drive,
1420
+ actions,
1421
+ options
1422
+ });
1423
+ return new Promise<IOperationResult<DocumentDriveDocument>>(
1424
+ (resolve, reject) => {
1425
+ const unsubscribe = this.queueManager.on(
1426
+ 'jobCompleted',
1427
+ (job, result) => {
1428
+ if (job.jobId === jobId) {
1429
+ unsubscribe();
1430
+ unsubscribeError();
1431
+ resolve(
1432
+ result as IOperationResult<DocumentDriveDocument>
1433
+ );
1434
+ }
1387
1435
  }
1388
- }
1389
- );
1390
- const unsubscribeError = this.queueManager.on(
1391
- 'jobFailed',
1392
- (job, error) => {
1393
- if (job.jobId === jobId) {
1394
- unsubscribe();
1395
- unsubscribeError();
1396
- reject(error);
1436
+ );
1437
+ const unsubscribeError = this.queueManager.on(
1438
+ 'jobFailed',
1439
+ (job, error) => {
1440
+ if (job.jobId === jobId) {
1441
+ unsubscribe();
1442
+ unsubscribeError();
1443
+ reject(error);
1444
+ }
1397
1445
  }
1398
- }
1399
- );
1400
- }
1401
- );
1446
+ );
1447
+ }
1448
+ );
1449
+ } catch (error) {
1450
+ logger.error('Error adding drive job', error);
1451
+ throw error;
1452
+ }
1402
1453
  }
1403
1454
 
1404
1455
  async addOperations(
@@ -1657,38 +1708,42 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1657
1708
  if (result) {
1658
1709
  return result;
1659
1710
  }
1660
-
1661
- const jobId = await this.queueManager.addJob({
1662
- driveId: drive,
1663
- operations,
1664
- options
1665
- });
1666
- return new Promise<IOperationResult<DocumentDriveDocument>>(
1667
- (resolve, reject) => {
1668
- const unsubscribe = this.queueManager.on(
1669
- 'jobCompleted',
1670
- (job, result) => {
1671
- if (job.jobId === jobId) {
1672
- unsubscribe();
1673
- unsubscribeError();
1674
- resolve(
1675
- result as IOperationResult<DocumentDriveDocument>
1676
- );
1711
+ try {
1712
+ const jobId = await this.queueManager.addJob({
1713
+ driveId: drive,
1714
+ operations,
1715
+ options
1716
+ });
1717
+ return new Promise<IOperationResult<DocumentDriveDocument>>(
1718
+ (resolve, reject) => {
1719
+ const unsubscribe = this.queueManager.on(
1720
+ 'jobCompleted',
1721
+ (job, result) => {
1722
+ if (job.jobId === jobId) {
1723
+ unsubscribe();
1724
+ unsubscribeError();
1725
+ resolve(
1726
+ result as IOperationResult<DocumentDriveDocument>
1727
+ );
1728
+ }
1677
1729
  }
1678
- }
1679
- );
1680
- const unsubscribeError = this.queueManager.on(
1681
- 'jobFailed',
1682
- (job, error) => {
1683
- if (job.jobId === jobId) {
1684
- unsubscribe();
1685
- unsubscribeError();
1686
- reject(error);
1730
+ );
1731
+ const unsubscribeError = this.queueManager.on(
1732
+ 'jobFailed',
1733
+ (job, error) => {
1734
+ if (job.jobId === jobId) {
1735
+ unsubscribe();
1736
+ unsubscribeError();
1737
+ reject(error);
1738
+ }
1687
1739
  }
1688
- }
1689
- );
1690
- }
1691
- );
1740
+ );
1741
+ }
1742
+ );
1743
+ } catch (error) {
1744
+ logger.error('Error adding drive job', error);
1745
+ throw error;
1746
+ }
1692
1747
  }
1693
1748
 
1694
1749
  async addDriveOperations(
@@ -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);
@@ -1,4 +1,4 @@
1
+ export * from './internal';
1
2
  export * from './pull-responder';
2
3
  export * from './switchboard-push';
3
- export * from './internal';
4
4
  export * from './types';
@@ -1,4 +1,5 @@
1
1
  import { Document, OperationScope } from 'document-model/document';
2
+ import { logger } from '../../../utils/logger';
2
3
  import {
3
4
  BaseDocumentDriveServer,
4
5
  Listener,
@@ -8,7 +9,6 @@ import {
8
9
  } from '../../types';
9
10
  import { buildRevisionsFilter } from '../../utils';
10
11
  import { ITransmitter } from './types';
11
- import { logger } from '../../../utils/logger';
12
12
 
13
13
  export interface IReceiver {
14
14
  transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
@@ -56,10 +56,10 @@ export class InternalTransmitter implements ITransmitter {
56
56
  );
57
57
  document = await (strand.documentId
58
58
  ? this.drive.getDocument(
59
- strand.driveId,
60
- strand.documentId,
61
- { revisions }
62
- )
59
+ strand.driveId,
60
+ strand.documentId,
61
+ { revisions }
62
+ )
63
63
  : this.drive.getDrive(strand.driveId, { revisions }));
64
64
  retrievedDocuments.set(
65
65
  `${strand.driveId}:${strand.documentId}`,
@@ -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[]>;
@@ -32,4 +32,3 @@ export function filterOperationsByRevision(
32
32
  return acc;
33
33
  }, operations);
34
34
  }
35
-
@@ -7,11 +7,7 @@ import {
7
7
  OperationScope
8
8
  } from 'document-model/document';
9
9
  import { mergeOperations, type SynchronizationUnitQuery } from '..';
10
- import {
11
- DocumentDriveStorage,
12
- DocumentStorage,
13
- IDriveStorage,
14
- } from './types';
10
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
15
11
 
16
12
  export class BrowserStorage implements IDriveStorage {
17
13
  private db: Promise<LocalForage>;
@@ -1,5 +1,10 @@
1
1
  import { DocumentDriveAction } from 'document-model-libs/document-drive';
2
- import { BaseAction, DocumentHeader, Operation, OperationScope } from 'document-model/document';
2
+ import {
3
+ BaseAction,
4
+ DocumentHeader,
5
+ Operation,
6
+ OperationScope
7
+ } from 'document-model/document';
3
8
  import type { Dirent } from 'fs';
4
9
  import {
5
10
  existsSync,
@@ -202,7 +207,13 @@ export class FilesystemStorage implements IDriveStorage {
202
207
  // get oldes drives first
203
208
  const drives = (await this.getDrives()).reverse();
204
209
  for (const drive of drives) {
205
- const { initialState: { state: { global: { slug: driveSlug } } } } = await this.getDrive(drive);
210
+ const {
211
+ initialState: {
212
+ state: {
213
+ global: { slug: driveSlug }
214
+ }
215
+ }
216
+ } = await this.getDrive(drive);
206
217
  if (driveSlug === slug) {
207
218
  return this.getDrive(drive);
208
219
  }
@@ -6,13 +6,9 @@ import {
6
6
  Operation,
7
7
  OperationScope
8
8
  } from 'document-model/document';
9
- import {
10
- DocumentDriveStorage,
11
- DocumentStorage,
12
- IDriveStorage,
13
- } from './types';
14
9
  import type { SynchronizationUnitQuery } from '../server/types';
15
10
  import { mergeOperations } from '../utils';
11
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
16
12
 
17
13
  export class MemoryStorage implements IDriveStorage {
18
14
  private documents: Record<string, Record<string, DocumentStorage>>;
@@ -31,14 +31,14 @@ import {
31
31
 
32
32
  type Transaction =
33
33
  | Omit<
34
- PrismaClient<Prisma.PrismaClientOptions, never>,
35
- | '$connect'
36
- | '$disconnect'
37
- | '$on'
38
- | '$transaction'
39
- | '$use'
40
- | '$extends'
41
- >
34
+ PrismaClient<Prisma.PrismaClientOptions, never>,
35
+ | '$connect'
36
+ | '$disconnect'
37
+ | '$on'
38
+ | '$transaction'
39
+ | '$use'
40
+ | '$extends'
41
+ >
42
42
  | ExtendedPrismaClient;
43
43
 
44
44
  function storageToOperation(
@@ -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,9 +506,16 @@ 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
- if ((e.code && e.code === "P2025") || (e.message && e.message.includes("An operation failed because it depends on one or more records that were required but not found."))) {
512
+ if (
513
+ (prismaError.code && prismaError.code === 'P2025') ||
514
+ (prismaError.message &&
515
+ prismaError.message.includes(
516
+ 'An operation failed because it depends on one or more records that were required but not found.'
517
+ ))
518
+ ) {
508
519
  return;
509
520
  }
510
521
 
@@ -10,8 +10,8 @@ import {
10
10
  OperationScope
11
11
  } from 'document-model/document';
12
12
  import { DataTypes, Options, Sequelize } from 'sequelize';
13
- import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
14
13
  import type { SynchronizationUnitQuery } from '../server/types';
14
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
15
15
 
16
16
  export class SequelizeStorage implements IDriveStorage {
17
17
  private db: Sequelize;
@@ -1,6 +1,6 @@
1
1
  import type {
2
2
  DocumentDriveAction,
3
- DocumentDriveDocument,
3
+ DocumentDriveDocument
4
4
  } from 'document-model-libs/document-drive';
5
5
  import type {
6
6
  Action,
@@ -8,7 +8,7 @@ import type {
8
8
  Document,
9
9
  DocumentHeader,
10
10
  DocumentOperations,
11
- Operation,
11
+ Operation
12
12
  } from 'document-model/document';
13
13
  import type { SynchronizationUnitQuery } from '../server/types';
14
14
 
@@ -20,7 +20,10 @@ export type DocumentStorage<D extends Document = Document> = Omit<
20
20
  export type DocumentDriveStorage = DocumentStorage<DocumentDriveDocument>;
21
21
 
22
22
  export interface IStorageDelegate {
23
- getCachedOperations(drive: string, id: string): Promise<DocumentOperations<Action> | undefined>;
23
+ getCachedOperations(
24
+ drive: string,
25
+ id: string
26
+ ): Promise<DocumentOperations<Action> | undefined>;
24
27
  }
25
28
 
26
29
  export interface IStorage {
@@ -36,7 +39,7 @@ export interface IStorage {
36
39
  drive: string,
37
40
  id: string,
38
41
  operations: Operation[],
39
- header: DocumentHeader,
42
+ header: DocumentHeader
40
43
  ): Promise<void>;
41
44
  addDocumentOperationsWithTransaction?(
42
45
  drive: string,
@@ -47,16 +50,24 @@ export interface IStorage {
47
50
  }>
48
51
  ): Promise<void>;
49
52
  deleteDocument(drive: string, id: string): Promise<void>;
50
- getOperationResultingState?(drive: string, id: string, index: number, scope: string, branch: string): Promise<unknown>;
53
+ getOperationResultingState?(
54
+ drive: string,
55
+ id: string,
56
+ index: number,
57
+ scope: string,
58
+ branch: string
59
+ ): Promise<unknown>;
51
60
  setStorageDelegate?(delegate: IStorageDelegate): void;
52
- getSynchronizationUnitsRevision(units: SynchronizationUnitQuery[]): Promise<{
53
- driveId: string;
54
- documentId: string;
55
- scope: string;
56
- branch: string;
57
- lastUpdated: string;
58
- revision: number;
59
- }[]>
61
+ getSynchronizationUnitsRevision(units: SynchronizationUnitQuery[]): Promise<
62
+ {
63
+ driveId: string;
64
+ documentId: string;
65
+ scope: string;
66
+ branch: string;
67
+ lastUpdated: string;
68
+ revision: number;
69
+ }[]
70
+ >;
60
71
  }
61
72
  export interface IDriveStorage extends IStorage {
62
73
  getDrives(): Promise<string[]>;
@@ -77,5 +88,10 @@ export interface IDriveStorage extends IStorage {
77
88
  header: DocumentHeader;
78
89
  }>
79
90
  ): Promise<void>;
80
- getDriveOperationResultingState?(drive: string, index: number, scope: string, branch: string): Promise<unknown>;
81
- }
91
+ getDriveOperationResultingState?(
92
+ drive: string,
93
+ index: number,
94
+ scope: string,
95
+ branch: string
96
+ ): Promise<unknown>;
97
+ }
@@ -1,6 +1,7 @@
1
1
  import { utils } from 'document-model/document';
2
2
 
3
- export const { attachBranch,
3
+ export const {
4
+ attachBranch,
4
5
  garbageCollect,
5
6
  groupOperationsByScope,
6
7
  merge,
@@ -1,8 +1,6 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
1
  import {
3
2
  DocumentDriveDocument,
4
- documentModel as DocumentDriveModel,
5
- z
3
+ documentModel as DocumentDriveModel
6
4
  } from 'document-model-libs/document-drive';
7
5
  import {
8
6
  Action,
@@ -12,49 +10,51 @@ import {
12
10
  Operation,
13
11
  OperationScope
14
12
  } from 'document-model/document';
13
+ import { v4 as uuidv4 } from 'uuid';
15
14
  import { OperationError } from '../server/error';
16
15
  import { DocumentDriveStorage, DocumentStorage } from '../storage';
17
16
 
18
17
  export function isDocumentDriveStorage(
19
18
  document: DocumentStorage
20
19
  ): document is DocumentDriveStorage {
21
- return (
22
- document.documentType === DocumentDriveModel.id
23
- );
20
+ return document.documentType === DocumentDriveModel.id;
24
21
  }
25
22
 
26
23
  export function isDocumentDrive(
27
24
  document: Document
28
25
  ): document is DocumentDriveDocument {
29
- return (
30
- document.documentType === DocumentDriveModel.id
31
- );
26
+ return document.documentType === DocumentDriveModel.id;
32
27
  }
33
28
 
34
29
  export function mergeOperations<A extends Action = Action>(
35
30
  currentOperations: DocumentOperations<A>,
36
31
  newOperations: Operation<A | BaseAction>[]
37
32
  ): DocumentOperations<A> {
38
- const minIndexByScope = Object.keys(currentOperations).reduce<Partial<Record<OperationScope, number>>>((acc, curr) => {
33
+ const minIndexByScope = Object.keys(currentOperations).reduce<
34
+ Partial<Record<OperationScope, number>>
35
+ >((acc, curr) => {
39
36
  const scope = curr as OperationScope;
40
- acc[scope] = currentOperations[scope].at(-1)?.index ?? 0
37
+ acc[scope] = currentOperations[scope].at(-1)?.index ?? 0;
41
38
  return acc;
42
39
  }, {});
43
40
 
44
- const conflictOp = newOperations.find(op => op.index < (minIndexByScope[op.scope] ?? 0));
41
+ const conflictOp = newOperations.find(
42
+ op => op.index < (minIndexByScope[op.scope] ?? 0)
43
+ );
45
44
  if (conflictOp) {
46
45
  throw new OperationError(
47
- "ERROR",
46
+ 'ERROR',
48
47
  conflictOp,
49
48
  `Tried to add operation with index ${conflictOp.index} and document is at index ${minIndexByScope[conflictOp.scope]}`
50
49
  );
51
50
  }
52
51
 
53
- return newOperations.sort((a, b) => a.index - b.index
54
- ).reduce<DocumentOperations<A>>((acc, curr) => {
55
- const existingOperations = acc[curr.scope] || [];
56
- return { ...acc, [curr.scope]: [...existingOperations, curr] };
57
- }, currentOperations);
52
+ return newOperations
53
+ .sort((a, b) => a.index - b.index)
54
+ .reduce<DocumentOperations<A>>((acc, curr) => {
55
+ const existingOperations = acc[curr.scope] || [];
56
+ return { ...acc, [curr.scope]: [...existingOperations, curr] };
57
+ }, currentOperations);
58
58
  }
59
59
 
60
60
  export function generateUUID(): string {
@@ -86,4 +86,3 @@ export function isNoopUpdate(
86
86
  export function isBefore(dateA: Date | string, dateB: Date | string) {
87
87
  return new Date(dateA) < new Date(dateB);
88
88
  }
89
-
@@ -1,5 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- export type ILogger = Pick<Console, 'log' | 'info' | 'warn' | 'error' | 'debug' | 'trace'>;
2
+ export type ILogger = Pick<
3
+ Console,
4
+ 'log' | 'info' | 'warn' | 'error' | 'debug' | 'trace'
5
+ >;
3
6
  class Logger implements ILogger {
4
7
  #logger: ILogger = console;
5
8