document-drive 1.0.0-websockets.1 → 1.0.0

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.
Files changed (43) hide show
  1. package/README.md +1 -0
  2. package/package.json +74 -88
  3. package/src/cache/index.ts +2 -2
  4. package/src/cache/memory.ts +22 -13
  5. package/src/cache/redis.ts +43 -16
  6. package/src/cache/types.ts +4 -4
  7. package/src/index.ts +6 -3
  8. package/src/queue/base.ts +276 -214
  9. package/src/queue/index.ts +2 -2
  10. package/src/queue/redis.ts +138 -127
  11. package/src/queue/types.ts +44 -38
  12. package/src/read-mode/errors.ts +19 -0
  13. package/src/read-mode/index.ts +125 -0
  14. package/src/read-mode/service.ts +207 -0
  15. package/src/read-mode/types.ts +108 -0
  16. package/src/server/error.ts +61 -26
  17. package/src/server/index.ts +2160 -1785
  18. package/src/server/listener/index.ts +2 -2
  19. package/src/server/listener/manager.ts +475 -437
  20. package/src/server/listener/transmitter/index.ts +4 -5
  21. package/src/server/listener/transmitter/internal.ts +77 -79
  22. package/src/server/listener/transmitter/pull-responder.ts +363 -329
  23. package/src/server/listener/transmitter/switchboard-push.ts +72 -55
  24. package/src/server/listener/transmitter/types.ts +19 -25
  25. package/src/server/types.ts +536 -349
  26. package/src/server/utils.ts +26 -27
  27. package/src/storage/base.ts +81 -0
  28. package/src/storage/browser.ts +233 -216
  29. package/src/storage/filesystem.ts +257 -256
  30. package/src/storage/index.ts +2 -1
  31. package/src/storage/memory.ts +206 -214
  32. package/src/storage/prisma.ts +575 -568
  33. package/src/storage/sequelize.ts +460 -471
  34. package/src/storage/types.ts +83 -67
  35. package/src/utils/default-drives-manager.ts +341 -0
  36. package/src/utils/document-helpers.ts +19 -18
  37. package/src/utils/graphql.ts +288 -34
  38. package/src/utils/index.ts +61 -59
  39. package/src/utils/logger.ts +39 -37
  40. package/src/utils/migrations.ts +58 -0
  41. package/src/utils/run-asap.ts +156 -0
  42. package/CHANGELOG.md +0 -818
  43. package/src/server/listener/transmitter/subscription.ts +0 -364
package/src/queue/base.ts CHANGED
@@ -1,258 +1,320 @@
1
- import { IJob, IJobQueue, IQueue, IQueueManager, IServerDelegate, JobId, Job, QueueEvents, isOperationJob } from "./types";
2
- import { generateUUID } from "../utils";
3
- import { createNanoEvents, Unsubscribe } from 'nanoevents';
1
+ import {
2
+ AddFileInput,
3
+ DeleteNodeInput,
4
+ } from "document-model-libs/document-drive";
4
5
  import { Action } from "document-model/document";
5
- import { AddFileInput, DeleteNodeInput } from "document-model-libs/document-drive";
6
+ import { Unsubscribe, createNanoEvents } from "nanoevents";
7
+ import { generateUUID, runAsap } 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
- private id: string;
9
- private blocked = false;
10
- private deleted = false;
11
- private items: IJob<T>[] = [];
12
- private dependencies = new Array<IJob<Job>>();
13
-
14
- constructor(id: string) {
15
- this.id = id;
22
+ private id: string;
23
+ private blocked = false;
24
+ private deleted = false;
25
+ private items: IJob<T>[] = [];
26
+ private dependencies = new Array<IJob<Job>>();
27
+
28
+ constructor(id: string) {
29
+ this.id = id;
30
+ }
31
+
32
+ async setDeleted(deleted: boolean) {
33
+ this.deleted = deleted;
34
+ }
35
+
36
+ async isDeleted() {
37
+ return this.deleted;
38
+ }
39
+
40
+ async addJob(data: IJob<T>) {
41
+ this.items.push(data);
42
+ return Promise.resolve();
43
+ }
44
+
45
+ async getNextJob() {
46
+ const job = this.items.shift();
47
+ return Promise.resolve(job);
48
+ }
49
+
50
+ async amountOfJobs() {
51
+ return Promise.resolve(this.items.length);
52
+ }
53
+
54
+ getId() {
55
+ return this.id;
56
+ }
57
+
58
+ async setBlocked(blocked: boolean) {
59
+ this.blocked = blocked;
60
+ }
61
+
62
+ async isBlocked() {
63
+ return this.blocked;
64
+ }
65
+
66
+ async getJobs() {
67
+ return this.items;
68
+ }
69
+
70
+ async addDependencies(job: IJob<Job>) {
71
+ if (!this.dependencies.find((j) => j.jobId === job.jobId)) {
72
+ this.dependencies.push(job);
16
73
  }
17
-
18
- async setDeleted(deleted: boolean) {
19
- this.deleted = deleted;
20
- }
21
-
22
- async isDeleted() {
23
- return this.deleted;
24
- }
25
-
26
- async addJob(data: IJob<T>) {
27
- this.items.push(data);
28
- return Promise.resolve();
29
- }
30
-
31
- async getNextJob() {
32
- const job = this.items.shift();
33
- return Promise.resolve(job);
74
+ if (!this.isBlocked()) {
75
+ this.setBlocked(true);
34
76
  }
35
-
36
- async amountOfJobs() {
37
- return Promise.resolve(this.items.length);
77
+ }
78
+
79
+ async removeDependencies(job: IJob<Job>) {
80
+ this.dependencies = this.dependencies.filter(
81
+ (j) => j.jobId !== job.jobId && j.driveId !== job.driveId,
82
+ );
83
+ if (this.dependencies.length === 0) {
84
+ await this.setBlocked(false);
38
85
  }
86
+ }
87
+ }
39
88
 
40
- getId() {
41
- return this.id;
89
+ export class BaseQueueManager implements IQueueManager {
90
+ protected emitter = createNanoEvents<QueueEvents>();
91
+ protected ticker = 0;
92
+ protected queues: IJobQueue[] = [];
93
+ protected workers: number;
94
+ protected timeout: number;
95
+ private delegate: IServerDelegate | undefined;
96
+
97
+ constructor(workers = 3, timeout = 0) {
98
+ this.workers = workers;
99
+ this.timeout = timeout;
100
+ }
101
+
102
+ async init(
103
+ delegate: IServerDelegate,
104
+ onError: (error: Error) => void,
105
+ ): Promise<void> {
106
+ this.delegate = delegate;
107
+ for (let i = 0; i < this.workers; i++) {
108
+ setTimeout(
109
+ () => this.processNextJob.bind(this)().catch(onError),
110
+ 100 * i,
111
+ );
42
112
  }
113
+ return Promise.resolve();
114
+ }
43
115
 
44
- async setBlocked(blocked: boolean) {
45
- this.blocked = blocked;
116
+ async addJob(job: Job): Promise<JobId> {
117
+ if (!this.delegate) {
118
+ throw new Error("No server delegate defined");
46
119
  }
47
120
 
48
- async isBlocked() {
49
- return this.blocked;
50
- }
121
+ const jobId = generateUUID();
122
+ const queue = this.getQueue(job.driveId, job.documentId);
51
123
 
52
- async getJobs() {
53
- return this.items;
124
+ if (await queue.isDeleted()) {
125
+ throw new Error("Queue is deleted");
54
126
  }
55
127
 
56
- async addDependencies(job: IJob<Job>) {
57
- if (!this.dependencies.find(j => j.jobId === job.jobId)) {
58
- this.dependencies.push(job);
59
- }
60
- if (!this.isBlocked()) {
61
- this.setBlocked(true);
128
+ // checks if the job is for a document that doesn't exist in storage yet
129
+ const newDocument =
130
+ job.documentId &&
131
+ !(await this.delegate.checkDocumentExists(job.driveId, job.documentId));
132
+ // if it is a new document and queue is not yet blocked then
133
+ // blocks it so the jobs are not processed until it's ready
134
+ if (newDocument && !(await queue.isBlocked())) {
135
+ await queue.setBlocked(true);
136
+
137
+ // checks if there any job in the queue adding the file and adds as dependency
138
+ const driveQueue = this.getQueue(job.driveId);
139
+ const jobs = await driveQueue.getJobs();
140
+ for (const driveJob of jobs) {
141
+ const actions = isOperationJob(driveJob)
142
+ ? driveJob.operations
143
+ : driveJob.actions;
144
+ const op = actions.find((j: Action) => {
145
+ const input = j.input as AddFileInput;
146
+ return j.type === "ADD_FILE" && input.id === job.documentId;
147
+ });
148
+ if (op) {
149
+ await queue.addDependencies(driveJob);
62
150
  }
151
+ }
63
152
  }
64
153
 
65
- async removeDependencies(job: IJob<Job>) {
66
- this.dependencies = this.dependencies.filter((j) => j.jobId !== job.jobId && j.driveId !== job.driveId);
67
- if (this.dependencies.length === 0) {
68
- await this.setBlocked(false);
69
- }
70
- }
71
- }
72
-
73
- export class BaseQueueManager implements IQueueManager {
74
- protected emitter = createNanoEvents<QueueEvents>();
75
- protected ticker = 0;
76
- protected queues: IJobQueue[] = [];
77
- protected workers: number;
78
- protected timeout: number;
79
- private delegate: IServerDelegate | undefined;
80
-
81
- constructor(workers = 3, timeout = 0) {
82
- this.workers = workers;
83
- this.timeout = timeout;
154
+ // if it has ADD_FILE operations then adds the job as
155
+ // a dependency to the corresponding document queues
156
+ const actions = isOperationJob(job) ? job.operations : job.actions;
157
+ const addFileOps = actions.filter((j: Action) => j.type === "ADD_FILE");
158
+ for (const addFileOp of addFileOps) {
159
+ const input = addFileOp.input as AddFileInput;
160
+ const q = this.getQueue(job.driveId, input.id);
161
+ await q.addDependencies({ jobId, ...job });
84
162
  }
85
163
 
86
- async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
87
- this.delegate = delegate;
88
- for (let i = 0; i < this.workers; i++) {
89
- setTimeout(() => this.processNextJob.bind(this)().catch(onError), 100 * i);
90
- }
91
- return Promise.resolve()
164
+ // remove document if operations contains delete_node
165
+ const removeFileOps = actions.filter(
166
+ (j: Action) => j.type === "DELETE_NODE",
167
+ );
168
+ for (const removeFileOp of removeFileOps) {
169
+ const input = removeFileOp.input as DeleteNodeInput;
170
+ const queue = this.getQueue(job.driveId, input.id);
171
+ await queue.setDeleted(true);
92
172
  }
173
+ await queue.addJob({ jobId, ...job });
93
174
 
94
- async addJob(job: Job): Promise<JobId> {
95
- if (!this.delegate) {
96
- throw new Error("No server delegate defined");
97
- }
98
-
99
- const jobId = generateUUID();
100
- const queue = this.getQueue(job.driveId, job.documentId);
101
-
102
- if (await queue.isDeleted()) {
103
- throw new Error("Queue is deleted")
104
- }
105
-
106
- // 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
109
- // blocks it so the jobs are not processed until it's ready
110
- if (newDocument && !(await queue.isBlocked())) {
111
- await queue.setBlocked(true);
112
-
113
- // checks if there any job in the queue adding the file and adds as dependency
114
- const driveQueue = this.getQueue(job.driveId);
115
- const jobs = await driveQueue.getJobs();
116
- for (const driveJob of jobs) {
117
- const actions = isOperationJob(driveJob) ? driveJob.operations : driveJob.actions;
118
- const op = actions.find((j: Action) => {
119
- const input = j.input as AddFileInput;
120
- return j.type === "ADD_FILE" && input.id === job.documentId
121
- })
122
- if (op) {
123
- await queue.addDependencies(driveJob);
124
- }
125
- }
126
- }
127
-
128
- // if it has ADD_FILE operations then adds the job as
129
- // a dependency to the corresponding document queues
130
- const actions = isOperationJob(job) ? job.operations : job.actions;
131
- const addFileOps = actions.filter((j: Action) => j.type === "ADD_FILE");
132
- for (const addFileOp of addFileOps) {
133
- const input = addFileOp.input as AddFileInput;
134
- const q = this.getQueue(job.driveId, input.id)
135
- await q.addDependencies({ jobId, ...job });
136
- }
175
+ return jobId;
176
+ }
137
177
 
138
- // remove document if operations contains delete_node
139
- const removeFileOps = actions.filter((j: Action) => j.type === "DELETE_NODE");
140
- for (const removeFileOp of removeFileOps) {
141
- const input = removeFileOp.input as DeleteNodeInput;
142
- const queue = this.getQueue(job.driveId, input.id);
143
- await queue.setDeleted(true);
144
- }
145
-
146
- await queue.addJob({ jobId, ...job });
178
+ getQueue(driveId: string, documentId?: string) {
179
+ const queueId = this.getQueueId(driveId, documentId);
180
+ let queue = this.queues.find((q) => q.getId() === queueId);
147
181
 
148
- return jobId;
182
+ if (!queue) {
183
+ queue = new MemoryQueue(queueId);
184
+ this.queues.push(queue);
149
185
  }
150
186
 
151
- getQueue(driveId: string, documentId?: string) {
152
- const queueId = this.getQueueId(driveId, documentId);
153
- let queue = this.queues.find((q) => q.getId() === queueId);
187
+ return queue;
188
+ }
154
189
 
155
- if (!queue) {
156
- queue = new MemoryQueue(queueId);
157
- this.queues.push(queue);
158
- }
190
+ removeQueue(driveId: string, documentId?: string) {
191
+ const queueId = this.getQueueId(driveId, documentId);
192
+ this.queues = this.queues.filter((q) => q.getId() !== queueId);
193
+ this.emit("queueRemoved", queueId);
194
+ }
159
195
 
160
- return queue;
196
+ getQueueByIndex(index: number) {
197
+ const queue = this.queues[index];
198
+ if (queue) {
199
+ return queue;
161
200
  }
162
201
 
163
- removeQueue(driveId: string, documentId?: string) {
164
- const queueId = this.getQueueId(driveId, documentId);
165
- this.queues = this.queues.filter((q) => q.getId() !== queueId);
166
- this.emit("queueRemoved", queueId)
202
+ return null;
203
+ }
204
+
205
+ getQueues() {
206
+ return this.queues.map((q) => q.getId());
207
+ }
208
+
209
+ private retryNextJob(timeout?: number) {
210
+ const _timeout = timeout !== undefined ? timeout : this.timeout;
211
+ const retry =
212
+ _timeout > 0 ? (fn: () => void) => setTimeout(fn, _timeout) : runAsap;
213
+ retry(() => this.processNextJob());
214
+ }
215
+
216
+ private async findFirstNonEmptyQueue(ticker: number): Promise<number | null> {
217
+ const numQueues = this.queues.length;
218
+
219
+ for (let i = 0; i < numQueues; i++) {
220
+ const index = (ticker + i) % numQueues;
221
+ const queue = this.queues[index];
222
+ if (queue && (await queue.amountOfJobs()) > 0) {
223
+ return index;
224
+ }
167
225
  }
226
+ return null;
227
+ }
168
228
 
169
- getQueueByIndex(index: number) {
170
- const queue = this.queues[index];
171
- if (queue) {
172
- return queue;
173
- }
174
-
175
- return null;
229
+ private async processNextJob() {
230
+ if (!this.delegate) {
231
+ throw new Error("No server delegate defined");
176
232
  }
177
233
 
178
- getQueues() {
179
- return Object.keys(new Array(this.queues));
234
+ if (this.queues.length === 0) {
235
+ this.retryNextJob();
236
+ return;
180
237
  }
181
238
 
182
- private retryNextJob() {
183
- const retry = this.timeout === 0 && typeof setImmediate !== "undefined" ? setImmediate : (fn: () => void) => setTimeout(fn, this.timeout);
184
- return retry(() => this.processNextJob());
239
+ const queue = this.queues[this.ticker];
240
+ if (!queue) {
241
+ this.ticker = 0;
242
+ this.retryNextJob();
243
+ return;
185
244
  }
186
245
 
187
- private async processNextJob() {
188
- if (!this.delegate) {
189
- throw new Error("No server delegate defined");
190
- }
191
-
192
- if (this.queues.length === 0) {
193
- this.retryNextJob();
194
- return;
195
- }
196
-
197
- const queue = this.queues[this.ticker];
198
- this.ticker = this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
199
- if (!queue) {
200
- this.ticker = 0;
201
- this.retryNextJob();
202
- return;
203
- }
204
-
205
- const amountOfJobs = await queue.amountOfJobs();
206
- if (amountOfJobs === 0) {
207
- this.retryNextJob();
208
- return;
209
- }
246
+ // if no jobs in the current queue then looks for the
247
+ // next queue with jobs. If no jobs in any queue then
248
+ // retries after a timeout
249
+ const amountOfJobs = await queue.amountOfJobs();
250
+ if (amountOfJobs === 0) {
251
+ const nextTicker = await this.findFirstNonEmptyQueue(this.ticker);
252
+ if (nextTicker !== null) {
253
+ this.ticker = nextTicker;
254
+ this.retryNextJob(0);
255
+ } else {
256
+ this.retryNextJob();
257
+ }
258
+ return;
259
+ }
210
260
 
211
- const isBlocked = await queue.isBlocked();
212
- if (isBlocked) {
213
- this.retryNextJob();
214
- return;
215
- }
261
+ this.ticker = this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
216
262
 
217
- await queue.setBlocked(true);
218
- const nextJob = await queue.getNextJob();
219
- if (!nextJob) {
220
- this.retryNextJob();
221
- return;
222
- }
223
-
224
- try {
225
- const result = await this.delegate.processJob(nextJob);
226
-
227
- // 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");
230
- if (addFileActions.length > 0) {
231
- for (const addFile of addFileActions) {
232
- const documentQueue = this.getQueue(nextJob.driveId, (addFile.input as AddFileInput).id);
233
- await documentQueue.removeDependencies(nextJob);
234
- };
235
- }
236
- this.emit("jobCompleted", nextJob, result);
237
- } catch (e) {
238
- this.emit("jobFailed", nextJob, e as Error);
239
- } finally {
240
- await queue.setBlocked(false);
241
- await this.processNextJob();
242
- }
263
+ const isBlocked = await queue.isBlocked();
264
+ if (isBlocked) {
265
+ this.retryNextJob();
266
+ return;
243
267
  }
244
268
 
245
- protected emit<K extends keyof QueueEvents>(
246
- event: K,
247
- ...args: Parameters<QueueEvents[K]>
248
- ) {
249
- this.emitter.emit(event, ...args);
250
- }
251
- on<K extends keyof QueueEvents>(this: this, event: K, cb: QueueEvents[K]): Unsubscribe {
252
- return this.emitter.on(event, cb);
269
+ await queue.setBlocked(true);
270
+ const nextJob = await queue.getNextJob();
271
+ if (!nextJob) {
272
+ this.retryNextJob();
273
+ return;
253
274
  }
254
275
 
255
- protected getQueueId(driveId: string, documentId?: string) {
256
- return `${driveId}${documentId ? `:${documentId}` : ''}`;
276
+ try {
277
+ const result = await this.delegate.processJob(nextJob);
278
+
279
+ // unblock the document queues of each add_file operation
280
+ const actions = isOperationJob(nextJob)
281
+ ? nextJob.operations
282
+ : nextJob.actions;
283
+ const addFileActions = actions.filter((op) => op.type === "ADD_FILE");
284
+ if (addFileActions.length > 0) {
285
+ for (const addFile of addFileActions) {
286
+ const documentQueue = this.getQueue(
287
+ nextJob.driveId,
288
+ (addFile.input as AddFileInput).id,
289
+ );
290
+ await documentQueue.removeDependencies(nextJob);
291
+ }
292
+ }
293
+ this.emit("jobCompleted", nextJob, result);
294
+ } catch (e) {
295
+ logger.error(`job failed`, e);
296
+ this.emit("jobFailed", nextJob, e as Error);
297
+ } finally {
298
+ await queue.setBlocked(false);
299
+ this.retryNextJob(0);
257
300
  }
258
- }
301
+ }
302
+
303
+ protected emit<K extends keyof QueueEvents>(
304
+ event: K,
305
+ ...args: Parameters<QueueEvents[K]>
306
+ ) {
307
+ this.emitter.emit(event, ...args);
308
+ }
309
+ on<K extends keyof QueueEvents>(
310
+ this: this,
311
+ event: K,
312
+ cb: QueueEvents[K],
313
+ ): Unsubscribe {
314
+ return this.emitter.on(event, cb);
315
+ }
316
+
317
+ protected getQueueId(driveId: string, documentId?: string) {
318
+ return `queue:${driveId}${documentId ? `:${documentId}` : ""}`;
319
+ }
320
+ }
@@ -1,2 +1,2 @@
1
- export * from './base';
2
- export * from './types';
1
+ export * from "./base";
2
+ export * from "./types";