document-drive 1.0.0-experimental.1 → 1.0.0-experimental.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.0.0-experimental.1",
3
+ "version": "1.0.0-experimental.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -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>;
@@ -68,6 +68,8 @@ import {
68
68
  type SynchronizationUnit
69
69
  } from './types';
70
70
  import { filterOperationsByRevision } from './utils';
71
+ import { BaseQueueManager } from '../queue/base';
72
+ import { IQueueManager } from '../queue/types';
71
73
 
72
74
  export * from './listener';
73
75
  export type * from './types';
@@ -86,16 +88,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
86
88
  >();
87
89
  private syncStatus = new Map<DocumentDriveState['id'], SyncStatus>();
88
90
 
91
+ private queueManager: IQueueManager;
92
+
89
93
  constructor(
90
94
  documentModels: DocumentModel[],
91
95
  storage: IDriveStorage = new MemoryStorage(),
92
- cache: ICache = new InMemoryCache()
96
+ cache: ICache = new InMemoryCache(),
97
+ queueManager: IQueueManager = new BaseQueueManager(),
93
98
  ) {
94
99
  super();
95
100
  this.listenerStateManager = new ListenerManager(this);
96
101
  this.documentModels = documentModels;
97
102
  this.storage = storage;
98
103
  this.cache = cache;
104
+ this.queueManager = queueManager;
99
105
  }
100
106
 
101
107
  private updateSyncStatus(
@@ -223,6 +229,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
223
229
  });
224
230
  }
225
231
 
232
+ await this.queueManager.init({
233
+ checkDocumentExists: (driveId: string, documentId: string): Promise<boolean> => this.storage.checkDocumentExists(driveId, documentId),
234
+ processOperationJob: ({ driveId, documentId, operations, forceSync }) => documentId ?
235
+ this.addOperations(driveId, documentId, operations, forceSync)
236
+ : this.addDriveOperations(driveId, operations as Operation<DocumentDriveAction | BaseAction>[], forceSync)
237
+
238
+ }, error => {
239
+ logger.error(`Error initializing queue manager`, error);
240
+ errors.push(error);
241
+ })
242
+
226
243
  // if network connect comes online then
227
244
  // triggers the listeners update
228
245
  if (typeof window !== 'undefined') {
@@ -438,6 +455,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
438
455
 
439
456
  await this.storage.createDrive(id, document);
440
457
  await this._initializeDrive(id);
458
+
441
459
  return document;
442
460
  }
443
461
 
@@ -805,6 +823,44 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
805
823
  }
806
824
  }
807
825
 
826
+
827
+ async queueOperations(drive: string,
828
+ id: string,
829
+ operations: Operation[],
830
+ forceSync = true) {
831
+ // try {
832
+ // await this.getDocument(drive, id);
833
+ // } catch (error) {
834
+ // logger.error('Error getting document', error);
835
+ // throw error;
836
+ // }
837
+
838
+ try {
839
+ const jobId = await this.queueManager.addJob({ driveId: drive, documentId: id, operations, forceSync });
840
+
841
+ return new Promise((resolve, reject) => {
842
+ const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
843
+ if (job.jobId === jobId) {
844
+ unsubscribe();
845
+ unsubscribeError();
846
+ resolve(result);
847
+ }
848
+ });
849
+ const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
850
+ console.log("test")
851
+ if (job.jobId === jobId) {
852
+ unsubscribe();
853
+ unsubscribeError();
854
+ reject(error);
855
+ }
856
+ });
857
+ })
858
+ } catch (error) {
859
+ logger.error('Error adding job', error);
860
+ throw error;
861
+ }
862
+ }
863
+
808
864
  async addOperations(
809
865
  drive: string,
810
866
  id: string,
@@ -958,6 +1014,31 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
958
1014
  }
959
1015
  }
960
1016
 
1017
+ async queueDriveOperations(
1018
+ drive: string,
1019
+ operations: Operation<DocumentDriveAction | BaseAction>[],
1020
+ forceSync = true
1021
+ ): Promise<IOperationResult> {
1022
+ const jobId = await this.queueManager.addJob({ driveId: drive, operations, forceSync });
1023
+ return new Promise((resolve, reject) => {
1024
+ const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1025
+ if (job.jobId === jobId) {
1026
+ unsubscribe();
1027
+ unsubscribeError();
1028
+ resolve(result);
1029
+ }
1030
+ });
1031
+ const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1032
+ if (job.jobId === jobId) {
1033
+ unsubscribe();
1034
+ unsubscribeError();
1035
+ reject(error);
1036
+ }
1037
+ });
1038
+
1039
+ })
1040
+ }
1041
+
961
1042
  async addDriveOperations(
962
1043
  drive: string,
963
1044
  operations: Operation<DocumentDriveAction | BaseAction>[],
@@ -32,3 +32,4 @@ export function filterOperationsByRevision(
32
32
  return acc;
33
33
  }, operations);
34
34
  }
35
+
@@ -27,6 +27,13 @@ export class BrowserStorage implements IDriveStorage {
27
27
  return args.join(BrowserStorage.SEP);
28
28
  }
29
29
 
30
+ async checkDocumentExists(drive: string, id: string): Promise<boolean> {
31
+ const document = await (
32
+ await this.db
33
+ ).getItem<Document>(this.buildKey(drive, id));
34
+ return document !== undefined;
35
+ }
36
+
30
37
  async getDocuments(drive: string) {
31
38
  const keys = await (await this.db).keys();
32
39
  const driveKey = `${drive}${BrowserStorage.SEP}`;
@@ -77,6 +77,11 @@ export class FilesystemStorage implements IDriveStorage {
77
77
  return documents;
78
78
  }
79
79
 
80
+ checkDocumentExists(drive: string, id: string): Promise<boolean> {
81
+ const documentExists = existsSync(this._buildDocumentPath(drive, id));
82
+ return Promise.resolve(documentExists);
83
+ }
84
+
80
85
  async getDocument(drive: string, id: string) {
81
86
  try {
82
87
  const content = readFileSync(this._buildDocumentPath(drive, id), {
@@ -18,6 +18,10 @@ export class MemoryStorage implements IDriveStorage {
18
18
  this.drives = {};
19
19
  }
20
20
 
21
+ checkDocumentExists(drive: string, id: string): Promise<boolean> {
22
+ return Promise.resolve(this.documents[drive]?.[id] !== undefined)
23
+ }
24
+
21
25
  async getDocuments(drive: string) {
22
26
  return Object.keys(this.documents[drive] ?? {});
23
27
  }
@@ -121,6 +125,7 @@ export class MemoryStorage implements IDriveStorage {
121
125
 
122
126
  async createDrive(id: string, drive: DocumentDriveStorage) {
123
127
  this.drives[id] = drive;
128
+ this.documents[id] = {};
124
129
  const { slug } = drive.initialState.state.global;
125
130
  if (slug) {
126
131
  this.slugToDriveId[slug] = id;
@@ -285,6 +285,16 @@ export class PrismaStorage implements IDriveStorage {
285
285
  return docs.map(doc => doc.id);
286
286
  }
287
287
 
288
+ async checkDocumentExists(driveId: string, id: string) {
289
+ const count = await this.db.document.count({
290
+ where: {
291
+ id: id,
292
+ driveId: driveId
293
+ },
294
+ });
295
+ return count > 0;
296
+ }
297
+
288
298
  async getDocument(driveId: string, id: string, tx?: Transaction) {
289
299
  const result = await (tx ?? this.db).document.findFirst({
290
300
  where: {
@@ -275,6 +275,21 @@ export class SequelizeStorage implements IDriveStorage {
275
275
  return ids;
276
276
  }
277
277
 
278
+ async checkDocumentExists(driveId: string, id: string): Promise<boolean> {
279
+ const Document = this.db.models.document;
280
+ if (!Document) {
281
+ throw new Error('Document model not found');
282
+ }
283
+ const count = await Document.count({
284
+ where: {
285
+ id: id,
286
+ driveId: driveId
287
+ },
288
+ })
289
+
290
+ return count > 0;
291
+ }
292
+
278
293
  async getDocument(driveId: string, id: string) {
279
294
  const Document = this.db.models.document;
280
295
  if (!Document) {
@@ -17,6 +17,7 @@ export type DocumentStorage<D extends Document = Document> = Omit<
17
17
  export type DocumentDriveStorage = DocumentStorage<DocumentDriveDocument>;
18
18
 
19
19
  export interface IStorage {
20
+ checkDocumentExists(drive: string, id: string): Promise<boolean>;
20
21
  getDocuments: (drive: string) => Promise<string[]>;
21
22
  getDocument(drive: string, id: string): Promise<DocumentStorage>;
22
23
  createDocument(