document-drive 1.0.0-experimental.1 → 1.0.0-experimental.2
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 +1 -1
- package/src/queue/base.ts +273 -0
- package/src/queue/redis.ts +143 -0
- package/src/queue/types.ts +57 -0
- package/src/server/index.ts +82 -1
- package/src/server/utils.ts +1 -0
- package/src/storage/browser.ts +7 -0
- package/src/storage/filesystem.ts +5 -0
- package/src/storage/memory.ts +5 -0
- package/src/storage/prisma.ts +10 -0
- package/src/storage/sequelize.ts +15 -0
- package/src/storage/types.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { IJob, IJobQueue, IQueue, IQueueManager, IServerDelegate, JobId, OperationJob, QueueEvents } from "./types";
|
|
2
|
+
import { generateUUID } from "../utils";
|
|
3
|
+
import { IOperationResult } from "../server";
|
|
4
|
+
import { createNanoEvents, Unsubscribe } from 'nanoevents';
|
|
5
|
+
import { Operation } from "document-model/document";
|
|
6
|
+
import { AddFileInput, DeleteNodeInput } from "document-model-libs/document-drive";
|
|
7
|
+
|
|
8
|
+
export class MemoryQueue<T, R> implements IQueue<T, R> {
|
|
9
|
+
private id: string;
|
|
10
|
+
private blocked = false;
|
|
11
|
+
private deleted = false;
|
|
12
|
+
private items: IJob<T>[] = [];
|
|
13
|
+
private results = new Map<JobId, R>();
|
|
14
|
+
private dependencies = new Array<IJob<OperationJob>>();
|
|
15
|
+
|
|
16
|
+
constructor(id: string) {
|
|
17
|
+
this.id = id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async setDeleted(deleted: boolean) {
|
|
21
|
+
this.deleted = deleted;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async isDeleted() {
|
|
25
|
+
return this.deleted;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async setResult(jobId: string, result: R): Promise<void> {
|
|
29
|
+
this.results.set(jobId, result);
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getResult(jobId: string): Promise<R | undefined> {
|
|
34
|
+
return Promise.resolve(this.results.get(jobId));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async addJob(data: IJob<T>) {
|
|
38
|
+
this.items.push(data);
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getNextJob() {
|
|
43
|
+
const job = this.items.shift();
|
|
44
|
+
return Promise.resolve(job);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async amountOfJobs() {
|
|
48
|
+
return Promise.resolve(this.items.length);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getId() {
|
|
52
|
+
return this.id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async setBlocked(blocked: boolean) {
|
|
56
|
+
this.blocked = blocked;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async isBlocked() {
|
|
60
|
+
return this.blocked;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getJobs() {
|
|
64
|
+
return this.items;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async addDependencies(job: IJob<OperationJob>) {
|
|
68
|
+
if (!this.dependencies.find(j => j.jobId === job.jobId)) {
|
|
69
|
+
this.dependencies.push(job);
|
|
70
|
+
}
|
|
71
|
+
if (!this.isBlocked()) {
|
|
72
|
+
this.setBlocked(true);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async removeDependencies(job: IJob<OperationJob>) {
|
|
77
|
+
this.dependencies = this.dependencies.filter((j) => j.jobId !== job.jobId && j.driveId !== job.driveId);
|
|
78
|
+
if (this.dependencies.length === 0) {
|
|
79
|
+
await this.setBlocked(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class BaseQueueManager implements IQueueManager {
|
|
85
|
+
protected emitter = createNanoEvents<QueueEvents>();
|
|
86
|
+
protected ticker = 0;
|
|
87
|
+
protected queues: IJobQueue[] = [];
|
|
88
|
+
protected workers: number;
|
|
89
|
+
protected timeout: number;
|
|
90
|
+
private delegate: IServerDelegate | undefined;
|
|
91
|
+
|
|
92
|
+
constructor(workers = 3, timeout = 0) {
|
|
93
|
+
this.workers = workers;
|
|
94
|
+
this.timeout = timeout;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
|
|
98
|
+
this.delegate = delegate;
|
|
99
|
+
for (let i = 0; i < this.workers; i++) {
|
|
100
|
+
setTimeout(() => this.processNextJob.bind(this)().catch(onError), 100 * i);
|
|
101
|
+
}
|
|
102
|
+
return Promise.resolve()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async addJob(job: OperationJob): Promise<JobId> {
|
|
106
|
+
if (!this.delegate) {
|
|
107
|
+
throw new Error("No server delegate defined");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const jobId = generateUUID();
|
|
111
|
+
const queue = this.getQueue(job.driveId, job.documentId);
|
|
112
|
+
|
|
113
|
+
if (await queue.isDeleted()) {
|
|
114
|
+
throw new Error("Queue is deleted")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// checks if the job is for a document that doesn't exist in storage yet
|
|
118
|
+
const newDocument = job.documentId && !(await this.delegate.checkDocumentExists(job.driveId, job.documentId));
|
|
119
|
+
// if it is a new document and queue is not yet blocked then
|
|
120
|
+
// blocks it so the jobs are not processed until it's ready
|
|
121
|
+
if (newDocument && !(await queue.isBlocked())) {
|
|
122
|
+
await queue.setBlocked(true);
|
|
123
|
+
|
|
124
|
+
// checks if there any job in the queue adding the file and adds as dependency
|
|
125
|
+
const driveQueue = this.getQueue(job.driveId);
|
|
126
|
+
const jobs = await driveQueue.getJobs();
|
|
127
|
+
for (const driveJob of jobs) {
|
|
128
|
+
const op = driveJob.operations.find((j: Operation) => {
|
|
129
|
+
const input = j.input as AddFileInput;
|
|
130
|
+
return j.type === "ADD_FILE" && input.id === job.documentId
|
|
131
|
+
})
|
|
132
|
+
if (op) {
|
|
133
|
+
await queue.addDependencies(driveJob);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// if it has ADD_FILE operations then adds the job as
|
|
139
|
+
// a dependency to the corresponding document queues
|
|
140
|
+
const addFileOps = job.operations.filter((j: Operation) => j.type === "ADD_FILE");
|
|
141
|
+
for (const addFileOp of addFileOps) {
|
|
142
|
+
const input = addFileOp.input as AddFileInput;
|
|
143
|
+
const q = this.getQueue(job.driveId, input.id)
|
|
144
|
+
await q.addDependencies({ jobId, ...job });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// remove document if operations contains delete_node
|
|
148
|
+
const removeFileOps = job.operations.filter((j: Operation) => j.type === "DELETE_NODE");
|
|
149
|
+
for (const removeFileOp of removeFileOps) {
|
|
150
|
+
const input = removeFileOp.input as DeleteNodeInput;
|
|
151
|
+
const queue = this.getQueue(job.driveId, input.id);
|
|
152
|
+
await queue.setDeleted(true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await queue.addJob({ jobId, ...job });
|
|
156
|
+
|
|
157
|
+
return jobId;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getResult(driveId: string, documentId: string, jobId: JobId): Promise<IOperationResult | undefined> {
|
|
161
|
+
const queue = this.getQueue(driveId, documentId);
|
|
162
|
+
return queue.getResult(jobId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getQueue(driveId: string, documentId?: string) {
|
|
166
|
+
const queueId = this.getQueueId(driveId, documentId);
|
|
167
|
+
let queue = this.queues.find((q) => q.getId() === queueId);
|
|
168
|
+
|
|
169
|
+
if (!queue) {
|
|
170
|
+
queue = new MemoryQueue(queueId);
|
|
171
|
+
this.queues.push(queue);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return queue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
removeQueue(driveId: string, documentId?: string) {
|
|
178
|
+
const queueId = this.getQueueId(driveId, documentId);
|
|
179
|
+
this.queues = this.queues.filter((q) => q.getId() !== queueId);
|
|
180
|
+
this.emit("queueRemoved", queueId)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getQueueByIndex(index: number) {
|
|
184
|
+
const queue = this.queues[index];
|
|
185
|
+
if (queue) {
|
|
186
|
+
return queue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getQueues() {
|
|
193
|
+
return Object.keys(new Array(this.queues));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private retryNextJob() {
|
|
197
|
+
const retry = this.timeout === 0 && typeof setImmediate !== "undefined" ? setImmediate : (fn: () => void) => setTimeout(fn, this.timeout);
|
|
198
|
+
return retry(() => this.processNextJob());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async processNextJob() {
|
|
202
|
+
if (!this.delegate) {
|
|
203
|
+
throw new Error("No server delegate defined");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.queues.length === 0) {
|
|
207
|
+
this.retryNextJob();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const queue = this.queues[this.ticker];
|
|
212
|
+
this.ticker = this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
|
|
213
|
+
if (!queue) {
|
|
214
|
+
this.ticker = 0;
|
|
215
|
+
this.retryNextJob();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const amountOfJobs = await queue.amountOfJobs();
|
|
220
|
+
if (amountOfJobs === 0) {
|
|
221
|
+
this.retryNextJob();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isBlocked = await queue.isBlocked();
|
|
226
|
+
if (isBlocked) {
|
|
227
|
+
this.retryNextJob();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await queue.setBlocked(true);
|
|
232
|
+
const nextJob = await queue.getNextJob();
|
|
233
|
+
if (!nextJob) {
|
|
234
|
+
this.retryNextJob();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const result = await this.delegate.processOperationJob(nextJob);
|
|
240
|
+
await queue.setResult(nextJob.jobId, result);
|
|
241
|
+
|
|
242
|
+
// unblock the document queues of each add_file operation
|
|
243
|
+
const addFileOperations = nextJob.operations.filter((op) => op.type === "ADD_FILE");
|
|
244
|
+
if (addFileOperations.length > 0) {
|
|
245
|
+
addFileOperations.map(async (addFileOp) => {
|
|
246
|
+
const documentQueue = this.getQueue(nextJob.driveId, (addFileOp.input as AddFileInput).id);
|
|
247
|
+
await documentQueue.removeDependencies(nextJob);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.emit("jobCompleted", nextJob, result);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
this.emit("jobFailed", nextJob, e as Error);
|
|
254
|
+
} finally {
|
|
255
|
+
queue.setBlocked(false);
|
|
256
|
+
await this.processNextJob();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected emit<K extends keyof QueueEvents>(
|
|
261
|
+
event: K,
|
|
262
|
+
...args: Parameters<QueueEvents[K]>
|
|
263
|
+
) {
|
|
264
|
+
this.emitter.emit(event, ...args);
|
|
265
|
+
}
|
|
266
|
+
on<K extends keyof QueueEvents>(this: this, event: K, cb: QueueEvents[K]): Unsubscribe {
|
|
267
|
+
return this.emitter.on(event, cb);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
protected getQueueId(driveId: string, documentId?: string) {
|
|
271
|
+
return `${driveId}${documentId ? `:${documentId}` : ''}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { RedisClientType } from "redis";
|
|
2
|
+
import { IJob, IQueue, IQueueManager, OperationJob } from "./types";
|
|
3
|
+
import { BaseQueueManager } from "./base";
|
|
4
|
+
|
|
5
|
+
export class RedisQueue<T, R> implements IQueue<T, R> {
|
|
6
|
+
private id: string;
|
|
7
|
+
private client: RedisClientType;
|
|
8
|
+
|
|
9
|
+
constructor(id: string, client: RedisClientType) {
|
|
10
|
+
this.client = client;
|
|
11
|
+
this.id = id;
|
|
12
|
+
this.client.hSet("queues", id, "true");
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async setResult(jobId: string, result: any): Promise<void> {
|
|
17
|
+
await this.client.hSet(this.id + "-results", jobId, JSON.stringify(result));
|
|
18
|
+
}
|
|
19
|
+
async getResult(jobId: string): Promise<any> {
|
|
20
|
+
const results = await this.client.hGet(this.id + "-results", jobId);
|
|
21
|
+
if (!results) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(results);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async addJob(data: any) {
|
|
28
|
+
await this.client.lPush(this.id + "-jobs", JSON.stringify(data));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getNextJob() {
|
|
32
|
+
const job = await this.client.rPop(this.id + "-jobs");
|
|
33
|
+
if (!job) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return JSON.parse(job);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async amountOfJobs() {
|
|
40
|
+
return this.client.lLen(this.id + "-jobs");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async setBlocked(blocked: boolean) {
|
|
44
|
+
if (blocked) {
|
|
45
|
+
await this.client.hSet(this.id, "blocked", "true");
|
|
46
|
+
} else {
|
|
47
|
+
await this.client.hDel(this.id, "blocked");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async isBlocked() {
|
|
52
|
+
const blockedResult = await this.client.hGet(this.id, "blocked");
|
|
53
|
+
if (blockedResult) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getId() {
|
|
61
|
+
return this.id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getJobs() {
|
|
65
|
+
const entries = await this.client.lRange(this.id + "-jobs", 0, -1)
|
|
66
|
+
return entries.map(e => JSON.parse(e));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async addDependencies(job: IJob<OperationJob>) {
|
|
70
|
+
if (await this.hasDependency(job)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await this.client.lPush(this.id + "-deps", JSON.stringify(job));
|
|
74
|
+
await this.setBlocked(true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async hasDependency(job: IJob<OperationJob>) {
|
|
78
|
+
const deps = await this.client.lRange(this.id + "-deps", 0, -1);
|
|
79
|
+
return deps.some(d => d === JSON.stringify(job));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async removeDependencies(job: IJob<OperationJob>) {
|
|
83
|
+
const allDeps1 = await this.client.lLen(this.id + "-deps");
|
|
84
|
+
await this.client.lRem(this.id + "-deps", 1, JSON.stringify(job));
|
|
85
|
+
const allDeps = await this.client.lLen(this.id + "-deps");
|
|
86
|
+
if (allDeps > 0) {
|
|
87
|
+
await this.setBlocked(true);
|
|
88
|
+
} else {
|
|
89
|
+
await this.setBlocked(false);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async isDeleted() {
|
|
94
|
+
const deleted = await this.client.hGet(this.id, "deleted");
|
|
95
|
+
return deleted === "true";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async setDeleted(deleted: boolean) {
|
|
99
|
+
if (deleted) {
|
|
100
|
+
await this.client.hSet(this.id, "deleted", "true");
|
|
101
|
+
} else {
|
|
102
|
+
await this.client.hDel(this.id, "deleted");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class RedisQueueManager extends BaseQueueManager implements IQueueManager {
|
|
108
|
+
|
|
109
|
+
private client: RedisClientType;
|
|
110
|
+
|
|
111
|
+
constructor(workers = 3, timeout = 0, client: RedisClientType) {
|
|
112
|
+
super(workers, timeout);
|
|
113
|
+
this.client = client;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
|
|
117
|
+
await super.init(delegate, onError);
|
|
118
|
+
// load all queues
|
|
119
|
+
const queues = await this.client.hGetAll("queues");
|
|
120
|
+
for (const queueId in queues) {
|
|
121
|
+
this.queues.push(new RedisQueue(queueId, this.client));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getQueue(driveId: string, documentId?: string) {
|
|
126
|
+
const queueId = this.getQueueId(driveId, documentId);
|
|
127
|
+
let queue = this.queues.find((q) => q.getId() === queueId);
|
|
128
|
+
|
|
129
|
+
if (!queue) {
|
|
130
|
+
queue = new RedisQueue(queueId, this.client);
|
|
131
|
+
this.queues.push(queue);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return queue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
removeQueue(driveId: string, documentId?: string | undefined): void {
|
|
138
|
+
super.removeQueue(driveId, documentId);
|
|
139
|
+
|
|
140
|
+
const queueId = this.getQueueId(driveId, documentId);
|
|
141
|
+
this.client.hDel("queues", queueId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Operation } from "document-model/document";
|
|
2
|
+
import { IOperationResult } from "../server";
|
|
3
|
+
import type { Unsubscribe } from "nanoevents";
|
|
4
|
+
|
|
5
|
+
export type OperationJob = {
|
|
6
|
+
driveId: string;
|
|
7
|
+
documentId?: string
|
|
8
|
+
operations: Operation[]
|
|
9
|
+
forceSync?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type JobId = string;
|
|
13
|
+
|
|
14
|
+
export interface QueueEvents {
|
|
15
|
+
jobCompleted: (job: IJob<OperationJob>, result: IOperationResult) => void;
|
|
16
|
+
jobFailed: (job: IJob<OperationJob>, error: Error) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IServerDelegate {
|
|
20
|
+
checkDocumentExists: (driveId: string, documentId: string) => Promise<boolean>;
|
|
21
|
+
processOperationJob: (job: OperationJob) => Promise<IOperationResult>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IQueueManager {
|
|
25
|
+
addJob(job: OperationJob): Promise<JobId>;
|
|
26
|
+
getResult(driveId: string, documentId: string, jobId: JobId): Promise<IOperationResult | undefined>;
|
|
27
|
+
getQueue(driveId: string, document?: string): IQueue<OperationJob, IOperationResult>;
|
|
28
|
+
removeQueue(driveId: string, documentId?: string): void;
|
|
29
|
+
getQueueByIndex(index: number): IQueue<OperationJob, IOperationResult> | null;
|
|
30
|
+
getQueues(): string[];
|
|
31
|
+
init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void>;
|
|
32
|
+
on<K extends keyof QueueEvents>(
|
|
33
|
+
this: this,
|
|
34
|
+
event: K,
|
|
35
|
+
cb: QueueEvents[K]
|
|
36
|
+
): Unsubscribe;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type IJob<T> = { jobId: JobId } & T;
|
|
40
|
+
|
|
41
|
+
export interface IQueue<T, R> {
|
|
42
|
+
addJob(data: IJob<T>): Promise<void>;
|
|
43
|
+
getNextJob(): Promise<IJob<T> | undefined>;
|
|
44
|
+
amountOfJobs(): Promise<number>;
|
|
45
|
+
getId(): string;
|
|
46
|
+
setBlocked(blocked: boolean): Promise<void>;
|
|
47
|
+
isBlocked(): Promise<boolean>;
|
|
48
|
+
isDeleted(): Promise<boolean>;
|
|
49
|
+
setDeleted(deleted: boolean): Promise<void>;
|
|
50
|
+
setResult(jobId: JobId, result: R): Promise<void>;
|
|
51
|
+
getResult(jobId: JobId): Promise<R | undefined>;
|
|
52
|
+
getJobs(): Promise<IJob<T>[]>;
|
|
53
|
+
addDependencies(job: IJob<OperationJob>): Promise<void>;
|
|
54
|
+
removeDependencies(job: IJob<OperationJob>): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type IJobQueue = IQueue<OperationJob, IOperationResult>;
|
package/src/server/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
type SynchronizationUnit
|
|
69
69
|
} from './types';
|
|
70
70
|
import { filterOperationsByRevision } from './utils';
|
|
71
|
+
import { BaseQueueManager } from '../queue/base';
|
|
72
|
+
import { IQueueManager } from '../queue/types';
|
|
71
73
|
|
|
72
74
|
export * from './listener';
|
|
73
75
|
export type * from './types';
|
|
@@ -86,16 +88,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
86
88
|
>();
|
|
87
89
|
private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
|
|
88
90
|
|
|
91
|
+
private queueManager: IQueueManager;
|
|
92
|
+
|
|
89
93
|
constructor(
|
|
90
94
|
documentModels: DocumentModel[],
|
|
91
95
|
storage: IDriveStorage = new MemoryStorage(),
|
|
92
|
-
cache: ICache = new InMemoryCache()
|
|
96
|
+
cache: ICache = new InMemoryCache(),
|
|
97
|
+
queueManager: IQueueManager = new BaseQueueManager(),
|
|
93
98
|
) {
|
|
94
99
|
super();
|
|
95
100
|
this.listenerStateManager = new ListenerManager(this);
|
|
96
101
|
this.documentModels = documentModels;
|
|
97
102
|
this.storage = storage;
|
|
98
103
|
this.cache = cache;
|
|
104
|
+
this.queueManager = queueManager;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
private updateSyncStatus(
|
|
@@ -223,6 +229,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
223
229
|
});
|
|
224
230
|
}
|
|
225
231
|
|
|
232
|
+
await this.queueManager.init({
|
|
233
|
+
checkDocumentExists: (driveId: string, documentId: string): Promise<boolean> => this.storage.checkDocumentExists(driveId, documentId),
|
|
234
|
+
processOperationJob: ({ driveId, documentId, operations, forceSync }) => documentId ?
|
|
235
|
+
this.addOperations(driveId, documentId, operations, forceSync)
|
|
236
|
+
: this.addDriveOperations(driveId, operations as Operation<DocumentDriveAction | BaseAction>[], forceSync)
|
|
237
|
+
|
|
238
|
+
}, error => {
|
|
239
|
+
logger.error(`Error initializing queue manager`, error);
|
|
240
|
+
errors.push(error);
|
|
241
|
+
})
|
|
242
|
+
|
|
226
243
|
// if network connect comes online then
|
|
227
244
|
// triggers the listeners update
|
|
228
245
|
if (typeof window !== 'undefined') {
|
|
@@ -438,6 +455,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
438
455
|
|
|
439
456
|
await this.storage.createDrive(id, document);
|
|
440
457
|
await this._initializeDrive(id);
|
|
458
|
+
|
|
441
459
|
return document;
|
|
442
460
|
}
|
|
443
461
|
|
|
@@ -805,6 +823,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
805
823
|
}
|
|
806
824
|
}
|
|
807
825
|
|
|
826
|
+
|
|
827
|
+
async queueOperations(drive: string,
|
|
828
|
+
id: string,
|
|
829
|
+
operations: Operation[],
|
|
830
|
+
forceSync = true) {
|
|
831
|
+
// try {
|
|
832
|
+
// await this.getDocument(drive, id);
|
|
833
|
+
// } catch (error) {
|
|
834
|
+
// logger.error('Error getting document', error);
|
|
835
|
+
// throw error;
|
|
836
|
+
// }
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const jobId = await this.queueManager.addJob({ driveId: drive, documentId: id, operations, forceSync });
|
|
840
|
+
|
|
841
|
+
return new Promise((resolve, reject) => {
|
|
842
|
+
const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
|
|
843
|
+
if (job.jobId === jobId) {
|
|
844
|
+
unsubscribe();
|
|
845
|
+
unsubscribeError();
|
|
846
|
+
resolve(result);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
|
|
850
|
+
console.log("test")
|
|
851
|
+
if (job.jobId === jobId) {
|
|
852
|
+
unsubscribe();
|
|
853
|
+
unsubscribeError();
|
|
854
|
+
reject(error);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
})
|
|
858
|
+
} catch (error) {
|
|
859
|
+
logger.error('Error adding job', error);
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
808
864
|
async addOperations(
|
|
809
865
|
drive: string,
|
|
810
866
|
id: string,
|
|
@@ -958,6 +1014,31 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
958
1014
|
}
|
|
959
1015
|
}
|
|
960
1016
|
|
|
1017
|
+
async queueDriveOperations(
|
|
1018
|
+
drive: string,
|
|
1019
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
1020
|
+
forceSync = true
|
|
1021
|
+
): Promise<IOperationResult> {
|
|
1022
|
+
const jobId = await this.queueManager.addJob({ driveId: drive, operations, forceSync });
|
|
1023
|
+
return new Promise((resolve, reject) => {
|
|
1024
|
+
const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
|
|
1025
|
+
if (job.jobId === jobId) {
|
|
1026
|
+
unsubscribe();
|
|
1027
|
+
unsubscribeError();
|
|
1028
|
+
resolve(result);
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
|
|
1032
|
+
if (job.jobId === jobId) {
|
|
1033
|
+
unsubscribe();
|
|
1034
|
+
unsubscribeError();
|
|
1035
|
+
reject(error);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
})
|
|
1040
|
+
}
|
|
1041
|
+
|
|
961
1042
|
async addDriveOperations(
|
|
962
1043
|
drive: string,
|
|
963
1044
|
operations: Operation<DocumentDriveAction | BaseAction>[],
|
package/src/server/utils.ts
CHANGED
package/src/storage/browser.ts
CHANGED
|
@@ -27,6 +27,13 @@ export class BrowserStorage implements IDriveStorage {
|
|
|
27
27
|
return args.join(BrowserStorage.SEP);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async checkDocumentExists(drive: string, id: string): Promise<boolean> {
|
|
31
|
+
const document = await (
|
|
32
|
+
await this.db
|
|
33
|
+
).getItem<Document>(this.buildKey(drive, id));
|
|
34
|
+
return document !== undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
async getDocuments(drive: string) {
|
|
31
38
|
const keys = await (await this.db).keys();
|
|
32
39
|
const driveKey = `${drive}${BrowserStorage.SEP}`;
|
|
@@ -77,6 +77,11 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
77
77
|
return documents;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
checkDocumentExists(drive: string, id: string): Promise<boolean> {
|
|
81
|
+
const documentExists = existsSync(this._buildDocumentPath(drive, id));
|
|
82
|
+
return Promise.resolve(documentExists);
|
|
83
|
+
}
|
|
84
|
+
|
|
80
85
|
async getDocument(drive: string, id: string) {
|
|
81
86
|
try {
|
|
82
87
|
const content = readFileSync(this._buildDocumentPath(drive, id), {
|
package/src/storage/memory.ts
CHANGED
|
@@ -18,6 +18,10 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
18
18
|
this.drives = {};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
checkDocumentExists(drive: string, id: string): Promise<boolean> {
|
|
22
|
+
return Promise.resolve(this.documents[drive]?.[id] !== undefined)
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
async getDocuments(drive: string) {
|
|
22
26
|
return Object.keys(this.documents[drive] ?? {});
|
|
23
27
|
}
|
|
@@ -121,6 +125,7 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
121
125
|
|
|
122
126
|
async createDrive(id: string, drive: DocumentDriveStorage) {
|
|
123
127
|
this.drives[id] = drive;
|
|
128
|
+
this.documents[id] = {};
|
|
124
129
|
const { slug } = drive.initialState.state.global;
|
|
125
130
|
if (slug) {
|
|
126
131
|
this.slugToDriveId[slug] = id;
|
package/src/storage/prisma.ts
CHANGED
|
@@ -285,6 +285,16 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
285
285
|
return docs.map(doc => doc.id);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
async checkDocumentExists(driveId: string, id: string) {
|
|
289
|
+
const count = await this.db.document.count({
|
|
290
|
+
where: {
|
|
291
|
+
id: id,
|
|
292
|
+
driveId: driveId
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
return count > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
288
298
|
async getDocument(driveId: string, id: string, tx?: Transaction) {
|
|
289
299
|
const result = await (tx ?? this.db).document.findFirst({
|
|
290
300
|
where: {
|
package/src/storage/sequelize.ts
CHANGED
|
@@ -275,6 +275,21 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
275
275
|
return ids;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
async checkDocumentExists(driveId: string, id: string): Promise<boolean> {
|
|
279
|
+
const Document = this.db.models.document;
|
|
280
|
+
if (!Document) {
|
|
281
|
+
throw new Error('Document model not found');
|
|
282
|
+
}
|
|
283
|
+
const count = await Document.count({
|
|
284
|
+
where: {
|
|
285
|
+
id: id,
|
|
286
|
+
driveId: driveId
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return count > 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
278
293
|
async getDocument(driveId: string, id: string) {
|
|
279
294
|
const Document = this.db.models.document;
|
|
280
295
|
if (!Document) {
|
package/src/storage/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type DocumentStorage<D extends Document = Document> = Omit<
|
|
|
17
17
|
export type DocumentDriveStorage = DocumentStorage<DocumentDriveDocument>;
|
|
18
18
|
|
|
19
19
|
export interface IStorage {
|
|
20
|
+
checkDocumentExists(drive: string, id: string): Promise<boolean>;
|
|
20
21
|
getDocuments: (drive: string) => Promise<string[]>;
|
|
21
22
|
getDocument(drive: string, id: string): Promise<DocumentStorage>;
|
|
22
23
|
createDocument(
|