document-drive 1.0.0-alpha.82 → 1.0.0-alpha.84
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 +29 -11
- package/src/cache/types.ts +8 -4
- package/src/queue/base.ts +78 -35
- 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 +68 -59
- package/src/server/listener/transmitter/index.ts +1 -1
- package/src/server/listener/transmitter/internal.ts +5 -5
- 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 +18 -19
- 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,14 +1,21 @@
|
|
|
1
|
-
import { Document } from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { logger } from "../utils/logger";
|
|
1
|
+
import { Document } from 'document-model/document';
|
|
2
|
+
import type { RedisClientType } from 'redis';
|
|
3
|
+
import { ICache } from './types';
|
|
5
4
|
|
|
6
5
|
class RedisCache implements ICache {
|
|
7
6
|
private redis: RedisClientType;
|
|
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
|
-
this.
|
|
14
|
+
this.timeoutInSeconds = timeoutInSeconds;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private static _getId(drive: string, id: string) {
|
|
18
|
+
return `cache:${drive}:${id}`;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
async setDocument(drive: string, id: string, document: Document) {
|
|
@@ -20,18 +27,29 @@ class RedisCache implements ICache {
|
|
|
20
27
|
delete e.resultingState;
|
|
21
28
|
return e;
|
|
22
29
|
});
|
|
23
|
-
const doc = { ...document, operations: { global, local } }
|
|
24
|
-
|
|
30
|
+
const doc = { ...document, operations: { global, local } };
|
|
31
|
+
const redisId = RedisCache._getId(drive, id);
|
|
32
|
+
const result = await this.redis.set(redisId, JSON.stringify(doc), {
|
|
33
|
+
EX: this.timeoutInSeconds ? this.timeoutInSeconds : undefined
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result === 'OK') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
async getDocument(drive: string, id: string) {
|
|
28
|
-
const
|
|
44
|
+
const redisId = RedisCache._getId(drive, id);
|
|
45
|
+
const doc = await this.redis.get(redisId);
|
|
29
46
|
|
|
30
|
-
return doc ? JSON.parse(doc) as Document : undefined;
|
|
47
|
+
return doc ? (JSON.parse(doc) as Document) : undefined;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
async deleteDocument(drive: string, id: string) {
|
|
34
|
-
|
|
51
|
+
const redisId = RedisCache._getId(drive, id);
|
|
52
|
+
return (await this.redis.del(redisId)) > 0;
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
|
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,21 @@
|
|
|
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 {
|
|
9
|
+
IJob,
|
|
10
|
+
IJobQueue,
|
|
11
|
+
IQueue,
|
|
12
|
+
IQueueManager,
|
|
13
|
+
IServerDelegate,
|
|
14
|
+
Job,
|
|
15
|
+
JobId,
|
|
16
|
+
QueueEvents,
|
|
17
|
+
isOperationJob
|
|
18
|
+
} from './types';
|
|
6
19
|
|
|
7
20
|
export class MemoryQueue<T, R> implements IQueue<T, R> {
|
|
8
21
|
private id: string;
|
|
@@ -63,7 +76,9 @@ export class MemoryQueue<T, R> implements IQueue<T, R> {
|
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
async removeDependencies(job: IJob<Job>) {
|
|
66
|
-
this.dependencies = this.dependencies.filter(
|
|
79
|
+
this.dependencies = this.dependencies.filter(
|
|
80
|
+
j => j.jobId !== job.jobId && j.driveId !== job.driveId
|
|
81
|
+
);
|
|
67
82
|
if (this.dependencies.length === 0) {
|
|
68
83
|
await this.setBlocked(false);
|
|
69
84
|
}
|
|
@@ -83,29 +98,40 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
83
98
|
this.timeout = timeout;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
async init(
|
|
101
|
+
async init(
|
|
102
|
+
delegate: IServerDelegate,
|
|
103
|
+
onError: (error: Error) => void
|
|
104
|
+
): Promise<void> {
|
|
87
105
|
this.delegate = delegate;
|
|
88
106
|
for (let i = 0; i < this.workers; i++) {
|
|
89
|
-
setTimeout(
|
|
107
|
+
setTimeout(
|
|
108
|
+
() => this.processNextJob.bind(this)().catch(onError),
|
|
109
|
+
100 * i
|
|
110
|
+
);
|
|
90
111
|
}
|
|
91
|
-
return Promise.resolve()
|
|
112
|
+
return Promise.resolve();
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
async addJob(job: Job): Promise<JobId> {
|
|
95
116
|
if (!this.delegate) {
|
|
96
|
-
throw new Error(
|
|
117
|
+
throw new Error('No server delegate defined');
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
const jobId = generateUUID();
|
|
100
121
|
const queue = this.getQueue(job.driveId, job.documentId);
|
|
101
122
|
|
|
102
123
|
if (await queue.isDeleted()) {
|
|
103
|
-
throw new Error(
|
|
124
|
+
throw new Error('Queue is deleted');
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
// checks if the job is for a document that doesn't exist in storage yet
|
|
107
|
-
const newDocument =
|
|
108
|
-
|
|
128
|
+
const newDocument =
|
|
129
|
+
job.documentId &&
|
|
130
|
+
!(await this.delegate.checkDocumentExists(
|
|
131
|
+
job.driveId,
|
|
132
|
+
job.documentId
|
|
133
|
+
));
|
|
134
|
+
// if it is a new document and queue is not yet blocked then
|
|
109
135
|
// blocks it so the jobs are not processed until it's ready
|
|
110
136
|
if (newDocument && !(await queue.isBlocked())) {
|
|
111
137
|
await queue.setBlocked(true);
|
|
@@ -114,11 +140,13 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
114
140
|
const driveQueue = this.getQueue(job.driveId);
|
|
115
141
|
const jobs = await driveQueue.getJobs();
|
|
116
142
|
for (const driveJob of jobs) {
|
|
117
|
-
const actions = isOperationJob(driveJob)
|
|
143
|
+
const actions = isOperationJob(driveJob)
|
|
144
|
+
? driveJob.operations
|
|
145
|
+
: driveJob.actions;
|
|
118
146
|
const op = actions.find((j: Action) => {
|
|
119
147
|
const input = j.input as AddFileInput;
|
|
120
|
-
return j.type ===
|
|
121
|
-
})
|
|
148
|
+
return j.type === 'ADD_FILE' && input.id === job.documentId;
|
|
149
|
+
});
|
|
122
150
|
if (op) {
|
|
123
151
|
await queue.addDependencies(driveJob);
|
|
124
152
|
}
|
|
@@ -128,15 +156,17 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
128
156
|
// if it has ADD_FILE operations then adds the job as
|
|
129
157
|
// a dependency to the corresponding document queues
|
|
130
158
|
const actions = isOperationJob(job) ? job.operations : job.actions;
|
|
131
|
-
const addFileOps = actions.filter((j: Action) => j.type ===
|
|
159
|
+
const addFileOps = actions.filter((j: Action) => j.type === 'ADD_FILE');
|
|
132
160
|
for (const addFileOp of addFileOps) {
|
|
133
161
|
const input = addFileOp.input as AddFileInput;
|
|
134
|
-
const q = this.getQueue(job.driveId, input.id)
|
|
162
|
+
const q = this.getQueue(job.driveId, input.id);
|
|
135
163
|
await q.addDependencies({ jobId, ...job });
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
// remove document if operations contains delete_node
|
|
139
|
-
const removeFileOps = actions.filter(
|
|
167
|
+
const removeFileOps = actions.filter(
|
|
168
|
+
(j: Action) => j.type === 'DELETE_NODE'
|
|
169
|
+
);
|
|
140
170
|
for (const removeFileOp of removeFileOps) {
|
|
141
171
|
const input = removeFileOp.input as DeleteNodeInput;
|
|
142
172
|
const queue = this.getQueue(job.driveId, input.id);
|
|
@@ -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,20 @@ 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
212
|
private retryNextJob() {
|
|
183
|
-
const retry =
|
|
213
|
+
const retry =
|
|
214
|
+
this.timeout === 0 && typeof setImmediate !== 'undefined'
|
|
215
|
+
? setImmediate
|
|
216
|
+
: (fn: () => void) => setTimeout(fn, this.timeout);
|
|
184
217
|
return retry(() => this.processNextJob());
|
|
185
218
|
}
|
|
186
219
|
|
|
187
220
|
private async processNextJob() {
|
|
188
221
|
if (!this.delegate) {
|
|
189
|
-
throw new Error(
|
|
222
|
+
throw new Error('No server delegate defined');
|
|
190
223
|
}
|
|
191
224
|
|
|
192
225
|
if (this.queues.length === 0) {
|
|
@@ -195,7 +228,8 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
195
228
|
}
|
|
196
229
|
|
|
197
230
|
const queue = this.queues[this.ticker];
|
|
198
|
-
this.ticker =
|
|
231
|
+
this.ticker =
|
|
232
|
+
this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
|
|
199
233
|
if (!queue) {
|
|
200
234
|
this.ticker = 0;
|
|
201
235
|
this.retryNextJob();
|
|
@@ -225,17 +259,22 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
225
259
|
const result = await this.delegate.processJob(nextJob);
|
|
226
260
|
|
|
227
261
|
// unblock the document queues of each add_file operation
|
|
228
|
-
const actions = isOperationJob(nextJob)
|
|
229
|
-
|
|
262
|
+
const actions = isOperationJob(nextJob)
|
|
263
|
+
? nextJob.operations
|
|
264
|
+
: nextJob.actions;
|
|
265
|
+
const addFileActions = actions.filter(op => op.type === 'ADD_FILE');
|
|
230
266
|
if (addFileActions.length > 0) {
|
|
231
267
|
for (const addFile of addFileActions) {
|
|
232
|
-
const documentQueue = this.getQueue(
|
|
268
|
+
const documentQueue = this.getQueue(
|
|
269
|
+
nextJob.driveId,
|
|
270
|
+
(addFile.input as AddFileInput).id
|
|
271
|
+
);
|
|
233
272
|
await documentQueue.removeDependencies(nextJob);
|
|
234
|
-
}
|
|
273
|
+
}
|
|
235
274
|
}
|
|
236
|
-
this.emit(
|
|
275
|
+
this.emit('jobCompleted', nextJob, result);
|
|
237
276
|
} catch (e) {
|
|
238
|
-
this.emit(
|
|
277
|
+
this.emit('jobFailed', nextJob, e as Error);
|
|
239
278
|
} finally {
|
|
240
279
|
await queue.setBlocked(false);
|
|
241
280
|
await this.processNextJob();
|
|
@@ -248,11 +287,15 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
248
287
|
) {
|
|
249
288
|
this.emitter.emit(event, ...args);
|
|
250
289
|
}
|
|
251
|
-
on<K extends keyof QueueEvents>(
|
|
290
|
+
on<K extends keyof QueueEvents>(
|
|
291
|
+
this: this,
|
|
292
|
+
event: K,
|
|
293
|
+
cb: QueueEvents[K]
|
|
294
|
+
): Unsubscribe {
|
|
252
295
|
return this.emitter.on(event, cb);
|
|
253
296
|
}
|
|
254
297
|
|
|
255
298
|
protected getQueueId(driveId: string, documentId?: string) {
|
|
256
|
-
return
|
|
299
|
+
return `queue:${driveId}${documentId ? `:${documentId}` : ''}`;
|
|
257
300
|
}
|
|
258
|
-
}
|
|
301
|
+
}
|
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
|
@@ -1368,37 +1368,42 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1368
1368
|
actions: (DocumentDriveAction | BaseAction)[],
|
|
1369
1369
|
options?: AddOperationOptions
|
|
1370
1370
|
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1371
|
+
try {
|
|
1372
|
+
const jobId = await this.queueManager.addJob({
|
|
1373
|
+
driveId: drive,
|
|
1374
|
+
actions,
|
|
1375
|
+
options
|
|
1376
|
+
});
|
|
1377
|
+
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1378
|
+
(resolve, reject) => {
|
|
1379
|
+
const unsubscribe = this.queueManager.on(
|
|
1380
|
+
'jobCompleted',
|
|
1381
|
+
(job, result) => {
|
|
1382
|
+
if (job.jobId === jobId) {
|
|
1383
|
+
unsubscribe();
|
|
1384
|
+
unsubscribeError();
|
|
1385
|
+
resolve(
|
|
1386
|
+
result as IOperationResult<DocumentDriveDocument>
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1387
1389
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1390
|
+
);
|
|
1391
|
+
const unsubscribeError = this.queueManager.on(
|
|
1392
|
+
'jobFailed',
|
|
1393
|
+
(job, error) => {
|
|
1394
|
+
if (job.jobId === jobId) {
|
|
1395
|
+
unsubscribe();
|
|
1396
|
+
unsubscribeError();
|
|
1397
|
+
reject(error);
|
|
1398
|
+
}
|
|
1397
1399
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
logger.error('Error adding drive job', error);
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1402
1407
|
}
|
|
1403
1408
|
|
|
1404
1409
|
async addOperations(
|
|
@@ -1657,38 +1662,42 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1657
1662
|
if (result) {
|
|
1658
1663
|
return result;
|
|
1659
1664
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1665
|
+
try {
|
|
1666
|
+
const jobId = await this.queueManager.addJob({
|
|
1667
|
+
driveId: drive,
|
|
1668
|
+
operations,
|
|
1669
|
+
options
|
|
1670
|
+
});
|
|
1671
|
+
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1672
|
+
(resolve, reject) => {
|
|
1673
|
+
const unsubscribe = this.queueManager.on(
|
|
1674
|
+
'jobCompleted',
|
|
1675
|
+
(job, result) => {
|
|
1676
|
+
if (job.jobId === jobId) {
|
|
1677
|
+
unsubscribe();
|
|
1678
|
+
unsubscribeError();
|
|
1679
|
+
resolve(
|
|
1680
|
+
result as IOperationResult<DocumentDriveDocument>
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1677
1683
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1684
|
+
);
|
|
1685
|
+
const unsubscribeError = this.queueManager.on(
|
|
1686
|
+
'jobFailed',
|
|
1687
|
+
(job, error) => {
|
|
1688
|
+
if (job.jobId === jobId) {
|
|
1689
|
+
unsubscribe();
|
|
1690
|
+
unsubscribeError();
|
|
1691
|
+
reject(error);
|
|
1692
|
+
}
|
|
1687
1693
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
)
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
logger.error('Error adding drive job', error);
|
|
1699
|
+
throw error;
|
|
1700
|
+
}
|
|
1692
1701
|
}
|
|
1693
1702
|
|
|
1694
1703
|
async addDriveOperations(
|
|
@@ -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}`,
|
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
|
@@ -495,21 +495,27 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
async deleteDocument(drive: string, id: string) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
try {
|
|
499
|
+
await this.db.document.deleteMany({
|
|
500
|
+
where: {
|
|
501
501
|
driveId: drive,
|
|
502
502
|
id: id
|
|
503
503
|
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
504
|
+
});
|
|
505
|
+
} catch (e: any) {
|
|
506
|
+
// Ignore Error: P2025: An operation failed because it depends on one or more records that were required but not found.
|
|
507
|
+
if (
|
|
508
|
+
(e.code && e.code === 'P2025') ||
|
|
509
|
+
(e.message &&
|
|
510
|
+
e.message.includes(
|
|
511
|
+
'An operation failed because it depends on one or more records that were required but not found.'
|
|
512
|
+
))
|
|
513
|
+
) {
|
|
514
|
+
return;
|
|
511
515
|
}
|
|
512
|
-
|
|
516
|
+
|
|
517
|
+
throw e;
|
|
518
|
+
}
|
|
513
519
|
}
|
|
514
520
|
|
|
515
521
|
async getDrives() {
|
|
@@ -541,13 +547,6 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
541
547
|
}
|
|
542
548
|
|
|
543
549
|
async deleteDrive(id: string) {
|
|
544
|
-
// delete drive documents and operations
|
|
545
|
-
await this.db.document.deleteMany({
|
|
546
|
-
where: {
|
|
547
|
-
driveId: id
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
550
|
// delete drive and associated slug
|
|
552
551
|
await this.db.drive.deleteMany({
|
|
553
552
|
where: {
|
|
@@ -555,7 +554,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
555
554
|
}
|
|
556
555
|
});
|
|
557
556
|
|
|
558
|
-
// delete drive
|
|
557
|
+
// delete drive document and its operations
|
|
559
558
|
await this.deleteDocument('drives', id);
|
|
560
559
|
}
|
|
561
560
|
|
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
|
|