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 +1 -1
- package/package.json +1 -1
- package/src/cache/memory.ts +3 -3
- package/src/cache/redis.ts +9 -6
- package/src/cache/types.ts +8 -4
- package/src/queue/base.ts +108 -38
- package/src/queue/index.ts +1 -1
- package/src/queue/redis.ts +41 -30
- package/src/queue/types.ts +20 -14
- package/src/server/index.ts +119 -64
- package/src/server/listener/manager.ts +36 -16
- package/src/server/listener/transmitter/index.ts +1 -1
- package/src/server/listener/transmitter/internal.ts +5 -5
- package/src/server/listener/transmitter/pull-responder.ts +0 -4
- package/src/server/listener/transmitter/types.ts +1 -1
- package/src/server/utils.ts +0 -1
- package/src/storage/browser.ts +1 -5
- package/src/storage/filesystem.ts +13 -2
- package/src/storage/memory.ts +1 -5
- package/src/storage/prisma.ts +22 -11
- package/src/storage/sequelize.ts +1 -1
- package/src/storage/types.ts +31 -15
- package/src/utils/document-helpers.ts +2 -1
- package/src/utils/index.ts +18 -19
- package/src/utils/logger.ts +4 -1
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# Document Drive
|
|
1
|
+
# Document Drive
|
package/package.json
CHANGED
package/src/cache/memory.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Document } from
|
|
2
|
-
import { ICache } from
|
|
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
|
}
|
package/src/cache/redis.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { Document } from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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(
|
|
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) {
|
package/src/cache/types.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { Document } from
|
|
1
|
+
import type { Document } from 'document-model/document';
|
|
2
2
|
|
|
3
3
|
export interface ICache {
|
|
4
|
-
setDocument(
|
|
5
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
108
|
-
|
|
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)
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
166
|
-
this.emit(
|
|
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
|
|
209
|
+
return this.queues.map(q => q.getId());
|
|
180
210
|
}
|
|
181
211
|
|
|
182
|
-
private retryNextJob() {
|
|
183
|
-
const
|
|
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(
|
|
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.
|
|
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)
|
|
229
|
-
|
|
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(
|
|
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(
|
|
301
|
+
this.emit('jobCompleted', nextJob, result);
|
|
237
302
|
} catch (e) {
|
|
238
|
-
|
|
303
|
+
logger.error(`job failed`, e);
|
|
304
|
+
this.emit('jobFailed', nextJob, e as Error);
|
|
239
305
|
} finally {
|
|
240
306
|
await queue.setBlocked(false);
|
|
241
|
-
|
|
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>(
|
|
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
|
+
}
|
package/src/queue/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './base';
|
|
2
|
-
export * from './types';
|
|
2
|
+
export * from './types';
|
package/src/queue/redis.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { RedisClientType } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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(
|
|
13
|
-
this.client.hSet(this.id,
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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,
|
|
40
|
+
await this.client.hSet(this.id, 'blocked', 'true');
|
|
35
41
|
} else {
|
|
36
|
-
await this.client.hSet(this.id,
|
|
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,
|
|
42
|
-
if (blockedResult ===
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
73
|
-
const allDeps = await this.client.lLen(this.id +
|
|
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(
|
|
83
|
-
return active ===
|
|
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(
|
|
94
|
+
await this.client.hSet('queues', this.id, 'false');
|
|
89
95
|
} else {
|
|
90
|
-
await this.client.hSet(
|
|
96
|
+
await this.client.hSet('queues', this.id, 'true');
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
export class RedisQueueManager
|
|
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(
|
|
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(
|
|
117
|
+
const queues = await this.client.hGetAll('queues');
|
|
107
118
|
for (const queueId in queues) {
|
|
108
|
-
const active = await this.client.hGet(
|
|
109
|
-
if (active ===
|
|
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(
|
|
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(
|
|
142
|
+
this.client.hDel('queues', queueId);
|
|
132
143
|
}
|
|
133
|
-
}
|
|
144
|
+
}
|
package/src/queue/types.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { Action, Operation } from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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: (
|
|
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(
|
|
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
|
|
74
|
+
return 'operations' in job;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export function isActionJob(job: Job): job is ActionJob {
|
|
72
|
-
return
|
|
73
|
-
}
|
|
78
|
+
return 'actions' in job;
|
|
79
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -220,6 +220,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
223
|
+
let firstPull = true;
|
|
223
224
|
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
224
225
|
driveId,
|
|
225
226
|
trigger,
|
|
@@ -269,6 +270,37 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
269
270
|
);
|
|
270
271
|
}
|
|
271
272
|
}
|
|
273
|
+
|
|
274
|
+
// if it is the first pull and returns empty
|
|
275
|
+
// then updates corresponding push transmitter
|
|
276
|
+
if (firstPull) {
|
|
277
|
+
firstPull = false;
|
|
278
|
+
const pushListener =
|
|
279
|
+
drive.state.local.listeners.find(
|
|
280
|
+
listener =>
|
|
281
|
+
trigger.data.url ===
|
|
282
|
+
listener.callInfo?.data
|
|
283
|
+
);
|
|
284
|
+
if (pushListener) {
|
|
285
|
+
this.getSynchronizationUnitsRevision(
|
|
286
|
+
driveId,
|
|
287
|
+
syncUnits
|
|
288
|
+
)
|
|
289
|
+
.then(syncUnitRevisions => {
|
|
290
|
+
for (const revision of syncUnitRevisions) {
|
|
291
|
+
this.listenerStateManager
|
|
292
|
+
.updateListenerRevision(
|
|
293
|
+
pushListener.listenerId,
|
|
294
|
+
driveId,
|
|
295
|
+
revision.syncId,
|
|
296
|
+
revision.revision
|
|
297
|
+
)
|
|
298
|
+
.catch(logger.error);
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.catch(logger.error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
272
304
|
}
|
|
273
305
|
);
|
|
274
306
|
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
@@ -405,16 +437,30 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
405
437
|
documentType,
|
|
406
438
|
drive
|
|
407
439
|
);
|
|
408
|
-
|
|
409
|
-
|
|
440
|
+
return this.getSynchronizationUnitsRevision(
|
|
441
|
+
driveId,
|
|
442
|
+
synchronizationUnitsQuery,
|
|
443
|
+
drive
|
|
410
444
|
);
|
|
445
|
+
}
|
|
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
|
|
413
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
|
306
|
+
const listenerRevisions = await transmitter.transmit(
|
|
296
307
|
strandUpdates,
|
|
297
308
|
source
|
|
298
309
|
);
|
|
@@ -321,22 +332,31 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
321
332
|
);
|
|
322
333
|
}
|
|
323
334
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
335
|
+
|
|
336
|
+
for (const revision of listenerRevisions) {
|
|
337
|
+
const error = revision.status === 'ERROR';
|
|
338
|
+
if (revision.error?.includes('Missing operations')) {
|
|
339
|
+
const updates = await this._triggerUpdate(
|
|
340
|
+
source,
|
|
341
|
+
onError
|
|
342
|
+
);
|
|
343
|
+
listenerUpdates.push(...updates);
|
|
344
|
+
} else {
|
|
345
|
+
listenerUpdates.push({
|
|
346
|
+
listenerId: listener.listener.listenerId,
|
|
347
|
+
listenerRevisions
|
|
348
|
+
});
|
|
349
|
+
if (error) {
|
|
350
|
+
throw new OperationError(
|
|
351
|
+
revision.status as ErrorStatus,
|
|
352
|
+
undefined,
|
|
353
|
+
revision.error,
|
|
354
|
+
revision.error
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
334
358
|
}
|
|
335
359
|
listener.listenerStatus = 'SUCCESS';
|
|
336
|
-
listenerUpdates.push({
|
|
337
|
-
listenerId: listener.listener.listenerId,
|
|
338
|
-
listenerRevisions
|
|
339
|
-
});
|
|
340
360
|
} catch (e) {
|
|
341
361
|
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
342
362
|
onError?.(e as Error, driveId, listener);
|
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
package/src/server/utils.ts
CHANGED
package/src/storage/browser.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
}
|
package/src/storage/memory.ts
CHANGED
|
@@ -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>>;
|
package/src/storage/prisma.ts
CHANGED
|
@@ -31,14 +31,14 @@ import {
|
|
|
31
31
|
|
|
32
32
|
type Transaction =
|
|
33
33
|
| Omit<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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
|
|
package/src/storage/sequelize.ts
CHANGED
|
@@ -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;
|
package/src/storage/types.ts
CHANGED
|
@@ -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(
|
|
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?(
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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?(
|
|
81
|
-
|
|
91
|
+
getDriveOperationResultingState?(
|
|
92
|
+
drive: string,
|
|
93
|
+
index: number,
|
|
94
|
+
scope: string,
|
|
95
|
+
branch: string
|
|
96
|
+
): Promise<unknown>;
|
|
97
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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<
|
|
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(
|
|
41
|
+
const conflictOp = newOperations.find(
|
|
42
|
+
op => op.index < (minIndexByScope[op.scope] ?? 0)
|
|
43
|
+
);
|
|
45
44
|
if (conflictOp) {
|
|
46
45
|
throw new OperationError(
|
|
47
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
export type ILogger = Pick<
|
|
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
|
|