document-drive 1.0.0-alpha.9 → 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/LICENSE CHANGED
@@ -56,6 +56,7 @@ this license.
56
56
  The precise terms and conditions for copying, distribution and
57
57
  modification follow.
58
58
 
59
+
59
60
  TERMS AND CONDITIONS
60
61
 
61
62
  0. Definitions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.0.0-alpha.9",
3
+ "version": "1.0.0-experimental.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -13,8 +13,11 @@
13
13
  "./storage/filesystem": "./src/storage/filesystem.ts",
14
14
  "./storage/memory": "./src/storage/memory.ts",
15
15
  "./storage/prisma": "./src/storage/prisma.ts",
16
+ "./cache/redis": "./src/cache/redis.ts",
17
+ "./cache/memory": "./src/cache/memory.ts",
16
18
  "./utils": "./src/utils/index.ts",
17
- "./utils/graphql": "./src/utils/graphql.ts"
19
+ "./utils/graphql": "./src/utils/graphql.ts",
20
+ "./logger": "./src/utils/logger.ts"
18
21
  },
19
22
  "files": [
20
23
  "./src"
@@ -29,14 +32,18 @@
29
32
  "test:watch": "vitest watch"
30
33
  },
31
34
  "peerDependencies": {
32
- "@prisma/client": "5.8.1",
33
- "document-model": "^1.0.29",
34
- "document-model-libs": "^1.1.44",
35
+ "document-model": "1.1.0-experimental.1",
36
+ "document-model-libs": "^1.37.0"
37
+ },
38
+ "optionalDependencies": {
39
+ "@prisma/client": "5.11.0",
35
40
  "localforage": "^1.10.0",
41
+ "redis": "^4.6.13",
36
42
  "sequelize": "^6.35.2",
37
43
  "sqlite3": "^5.1.7"
38
44
  },
39
45
  "dependencies": {
46
+ "exponential-backoff": "^3.1.1",
40
47
  "graphql": "^16.8.1",
41
48
  "graphql-request": "^6.1.0",
42
49
  "json-stringify-deterministic": "^1.0.12",
@@ -45,28 +52,29 @@
45
52
  },
46
53
  "devDependencies": {
47
54
  "@commitlint/cli": "^18.6.1",
48
- "@commitlint/config-conventional": "^18.6.2",
49
- "@prisma/client": "5.8.1",
55
+ "@commitlint/config-conventional": "^18.6.3",
56
+ "@prisma/client": "5.11.0",
50
57
  "@semantic-release/changelog": "^6.0.3",
51
58
  "@semantic-release/git": "^10.0.1",
52
59
  "@total-typescript/ts-reset": "^0.5.1",
53
- "@types/node": "^20.11.16",
54
- "@typescript-eslint/eslint-plugin": "^6.18.1",
55
- "@typescript-eslint/parser": "^6.18.1",
56
- "@vitest/coverage-v8": "^0.34.6",
57
- "document-model": "^1.0.30",
58
- "document-model-libs": "^1.1.51",
59
- "eslint": "^8.56.0",
60
+ "@types/node": "^20.12.7",
61
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
62
+ "@typescript-eslint/parser": "^6.21.0",
63
+ "@vitest/coverage-v8": "^1.4.0",
64
+ "document-model": "1.1.0-experimental.1",
65
+ "document-model-libs": "^1.37.0",
66
+ "eslint": "^8.57.0",
60
67
  "eslint-config-prettier": "^9.1.0",
61
- "fake-indexeddb": "^5.0.1",
68
+ "fake-indexeddb": "^5.0.2",
62
69
  "localforage": "^1.10.0",
63
- "msw": "^2.1.2",
64
- "prettier": "^3.1.1",
70
+ "msw": "^2.2.13",
71
+ "prettier": "^3.2.5",
65
72
  "prettier-plugin-organize-imports": "^3.2.4",
66
- "semantic-release": "^23.0.2",
67
- "sequelize": "^6.35.2",
73
+ "prisma": "^5.12.1",
74
+ "semantic-release": "^23.0.8",
75
+ "sequelize": "^6.37.2",
68
76
  "sqlite3": "^5.1.7",
69
- "typescript": "^5.3.2",
70
- "vitest": "^1.2.2"
77
+ "typescript": "^5.4.4",
78
+ "vitest": "^1.5.0"
71
79
  }
72
80
  }
@@ -0,0 +1,2 @@
1
+ export * from './memory';
2
+ export * from './types';
@@ -0,0 +1,24 @@
1
+ import { Document } from "document-model/document";
2
+ import { ICache } from "./types";
3
+
4
+ class InMemoryCache implements ICache {
5
+ private cache = new Map<string, Map<string, Document>>();
6
+
7
+ async setDocument(drive: string, id: string, document: Document) {
8
+ if (!this.cache.has(drive)) {
9
+ this.cache.set(drive, new Map());
10
+ }
11
+ this.cache.get(drive)?.set(id, document);
12
+ return true;
13
+ }
14
+
15
+ async deleteDocument(drive: string, id: string) {
16
+ return this.cache.get(drive)?.delete(id) ?? false;
17
+ }
18
+
19
+ async getDocument(drive: string, id: string) {
20
+ return this.cache.get(drive)?.get(id);
21
+ }
22
+ }
23
+
24
+ export default InMemoryCache;
@@ -0,0 +1,29 @@
1
+ import { Document } from "document-model/document";
2
+ import { ICache } from "./types";
3
+ import type { RedisClientType } from "redis";
4
+ import { logger } from "../utils/logger";
5
+
6
+ class RedisCache implements ICache {
7
+ private redis: RedisClientType;
8
+
9
+ constructor(redis: RedisClientType) {
10
+ this.redis = redis;
11
+ this.redis.flushAll().catch(logger.error);
12
+ }
13
+
14
+ async setDocument(drive: string, id: string, document: Document) {
15
+ return (await this.redis.hSet(drive, id, JSON.stringify(document))) > 0;
16
+ }
17
+
18
+ async getDocument(drive: string, id: string) {
19
+ const doc = await this.redis.hGet(drive, id);
20
+
21
+ return doc ? JSON.parse(doc) as Document : undefined;
22
+ }
23
+
24
+ async deleteDocument(drive: string, id: string) {
25
+ return (await this.redis.hDel(drive, id)) > 0;
26
+ }
27
+ }
28
+
29
+ export default RedisCache;
@@ -0,0 +1,9 @@
1
+ import type { Document } from "document-model/document";
2
+
3
+ export interface ICache {
4
+ setDocument(drive: string, id: string, document: Document): Promise<boolean>
5
+ getDocument(drive: string, id: string): Promise<Document | undefined>
6
+
7
+ // @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>
9
+ }
@@ -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>;
@@ -16,3 +16,20 @@ export class OperationError extends Error {
16
16
  this.operation = operation;
17
17
  }
18
18
  }
19
+
20
+ export class ConflictOperationError extends OperationError {
21
+ constructor(existingOperation: Operation, newOperation: Operation) {
22
+ super(
23
+ 'CONFLICT',
24
+ newOperation,
25
+ `Conflicting operation on index ${newOperation.index}`,
26
+ { existingOperation, newOperation }
27
+ );
28
+ }
29
+ }
30
+
31
+ export class MissingOperationError extends OperationError {
32
+ constructor(index: number, operation: Operation) {
33
+ super('MISSING', operation, `Missing operation on index ${index}`);
34
+ }
35
+ }