document-drive 1.0.0-websockets → 1.0.0
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 -0
- package/package.json +74 -88
- package/src/cache/index.ts +2 -2
- package/src/cache/memory.ts +22 -13
- package/src/cache/redis.ts +43 -16
- package/src/cache/types.ts +4 -4
- package/src/index.ts +6 -3
- package/src/queue/base.ts +276 -214
- package/src/queue/index.ts +2 -2
- package/src/queue/redis.ts +138 -127
- package/src/queue/types.ts +44 -38
- package/src/read-mode/errors.ts +19 -0
- package/src/read-mode/index.ts +125 -0
- package/src/read-mode/service.ts +207 -0
- package/src/read-mode/types.ts +108 -0
- package/src/server/error.ts +61 -26
- package/src/server/index.ts +2160 -1785
- package/src/server/listener/index.ts +2 -2
- package/src/server/listener/manager.ts +475 -437
- package/src/server/listener/transmitter/index.ts +4 -5
- package/src/server/listener/transmitter/internal.ts +77 -79
- package/src/server/listener/transmitter/pull-responder.ts +363 -329
- package/src/server/listener/transmitter/switchboard-push.ts +72 -55
- package/src/server/listener/transmitter/types.ts +19 -25
- package/src/server/types.ts +536 -349
- package/src/server/utils.ts +26 -27
- package/src/storage/base.ts +81 -0
- package/src/storage/browser.ts +233 -216
- package/src/storage/filesystem.ts +257 -256
- package/src/storage/index.ts +2 -1
- package/src/storage/memory.ts +206 -214
- package/src/storage/prisma.ts +575 -568
- package/src/storage/sequelize.ts +460 -471
- package/src/storage/types.ts +83 -67
- package/src/utils/default-drives-manager.ts +341 -0
- package/src/utils/document-helpers.ts +19 -18
- package/src/utils/graphql.ts +288 -34
- package/src/utils/index.ts +61 -59
- package/src/utils/logger.ts +39 -37
- package/src/utils/migrations.ts +58 -0
- package/src/utils/run-asap.ts +156 -0
- package/CHANGELOG.md +0 -818
- package/src/server/listener/transmitter/subscription.ts +0 -364
|
@@ -1,296 +1,297 @@
|
|
|
1
|
-
import { DocumentDriveAction } from
|
|
2
|
-
import { BaseAction, DocumentHeader, Operation, OperationScope } from 'document-model/document';
|
|
3
|
-
import type { Dirent } from 'fs';
|
|
1
|
+
import { DocumentDriveAction } from "document-model-libs/document-drive";
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
BaseAction,
|
|
4
|
+
DocumentHeader,
|
|
5
|
+
Operation,
|
|
6
|
+
OperationScope,
|
|
7
|
+
} from "document-model/document";
|
|
8
|
+
import type { Dirent } from "fs";
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "fs";
|
|
16
|
+
import fs from "fs/promises";
|
|
17
|
+
import stringify from "json-stringify-deterministic";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import sanitize from "sanitize-filename";
|
|
20
|
+
import { DriveNotFoundError } from "../server/error";
|
|
21
|
+
import type { SynchronizationUnitQuery } from "../server/types";
|
|
22
|
+
import { mergeOperations } from "../utils";
|
|
23
|
+
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from "./types";
|
|
18
24
|
|
|
19
25
|
type FSError = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
errno: number;
|
|
27
|
+
code: string;
|
|
28
|
+
syscall: string;
|
|
29
|
+
path: string;
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
function ensureDir(dir: string) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export class FilesystemStorage implements IDriveStorage {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
private basePath: string;
|
|
40
|
+
private drivesPath: string;
|
|
41
|
+
private static DRIVES_DIR = "drives";
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
ensureDir(this.drivesPath);
|
|
45
|
-
}
|
|
43
|
+
constructor(basePath: string) {
|
|
44
|
+
this.basePath = basePath;
|
|
45
|
+
ensureDir(this.basePath);
|
|
46
|
+
this.drivesPath = path.join(this.basePath, FilesystemStorage.DRIVES_DIR);
|
|
47
|
+
ensureDir(this.drivesPath);
|
|
48
|
+
}
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
private _buildDocumentPath(...args: string[]) {
|
|
51
|
+
return `${path.join(
|
|
52
|
+
this.basePath,
|
|
53
|
+
...args.map((arg) => sanitize(arg)),
|
|
54
|
+
)}.json`;
|
|
55
|
+
}
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
const documents: string[] = [];
|
|
67
|
-
for (const file of files.filter(file => file.isFile())) {
|
|
68
|
-
try {
|
|
69
|
-
const documentId = path.parse(file.name).name;
|
|
70
|
-
|
|
71
|
-
// checks if file is document
|
|
72
|
-
await this.getDocument(drive, documentId);
|
|
73
|
-
documents.push(documentId);
|
|
74
|
-
} catch {
|
|
75
|
-
/* Ignore invalid document*/
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return documents;
|
|
57
|
+
async getDocuments(drive: string) {
|
|
58
|
+
let files: Dirent[] = [];
|
|
59
|
+
try {
|
|
60
|
+
files = readdirSync(path.join(this.basePath, drive), {
|
|
61
|
+
withFileTypes: true,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// if folder is not found then drive has no documents
|
|
65
|
+
if ((error as FSError).code !== "ENOENT") {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
79
68
|
}
|
|
69
|
+
const documents: string[] = [];
|
|
70
|
+
for (const file of files.filter((file) => file.isFile())) {
|
|
71
|
+
try {
|
|
72
|
+
const documentId = path.parse(file.name).name;
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
// checks if file is document
|
|
75
|
+
await this.getDocument(drive, documentId);
|
|
76
|
+
documents.push(documentId);
|
|
77
|
+
} catch {
|
|
78
|
+
/* Ignore invalid document*/
|
|
79
|
+
}
|
|
84
80
|
}
|
|
81
|
+
return documents;
|
|
82
|
+
}
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
return JSON.parse(content) as Promise<DocumentStorage>;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
throw new Error(`Document with id ${id} not found`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
84
|
+
checkDocumentExists(drive: string, id: string): Promise<boolean> {
|
|
85
|
+
const documentExists = existsSync(this._buildDocumentPath(drive, id));
|
|
86
|
+
return Promise.resolve(documentExists);
|
|
87
|
+
}
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
async getDocument(drive: string, id: string) {
|
|
90
|
+
try {
|
|
91
|
+
const content = readFileSync(this._buildDocumentPath(drive, id), {
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
});
|
|
94
|
+
return JSON.parse(content) as Promise<DocumentStorage>;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(`Document with id ${id} not found`);
|
|
104
97
|
}
|
|
98
|
+
}
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
async createDocument(drive: string, id: string, document: DocumentStorage) {
|
|
101
|
+
const documentPath = this._buildDocumentPath(drive, id);
|
|
102
|
+
ensureDir(path.dirname(documentPath));
|
|
103
|
+
writeFileSync(documentPath, stringify(document), {
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
});
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
}
|
|
111
108
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await fs.readdir(drivesPath, {
|
|
115
|
-
withFileTypes: true,
|
|
116
|
-
recursive: true
|
|
117
|
-
})
|
|
118
|
-
).filter(dirent => !!dirent.name);
|
|
109
|
+
async clearStorage() {
|
|
110
|
+
const drivesPath = path.join(this.basePath, FilesystemStorage.DRIVES_DIR);
|
|
119
111
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
112
|
+
// delete content of drives directory
|
|
113
|
+
const drives = (
|
|
114
|
+
await fs.readdir(drivesPath, {
|
|
115
|
+
withFileTypes: true,
|
|
116
|
+
recursive: true,
|
|
117
|
+
})
|
|
118
|
+
).filter((dirent) => !!dirent.name);
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await Promise.all(
|
|
136
|
-
files.map(async dirent => {
|
|
137
|
-
await fs.rm(path.join(this.basePath, dirent.name), {
|
|
138
|
-
recursive: true
|
|
139
|
-
});
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
-
}
|
|
120
|
+
await Promise.all(
|
|
121
|
+
drives.map(async (dirent) => {
|
|
122
|
+
await fs.rm(path.join(drivesPath, dirent.name), {
|
|
123
|
+
recursive: true,
|
|
124
|
+
});
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
143
127
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
// delete files in basePath
|
|
129
|
+
const files = (
|
|
130
|
+
await fs.readdir(this.basePath, { withFileTypes: true })
|
|
131
|
+
).filter(
|
|
132
|
+
(file) => file.name !== FilesystemStorage.DRIVES_DIR && !!file.name,
|
|
133
|
+
);
|
|
147
134
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
throw new Error(`Document with id ${id} not found`);
|
|
157
|
-
}
|
|
135
|
+
await Promise.all(
|
|
136
|
+
files.map(async (dirent) => {
|
|
137
|
+
await fs.rm(path.join(this.basePath, dirent.name), {
|
|
138
|
+
recursive: true,
|
|
139
|
+
});
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
158
143
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
144
|
+
async deleteDocument(drive: string, id: string) {
|
|
145
|
+
return fs.rm(this._buildDocumentPath(drive, id));
|
|
146
|
+
}
|
|
163
147
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
148
|
+
async addDocumentOperations(
|
|
149
|
+
drive: string,
|
|
150
|
+
id: string,
|
|
151
|
+
operations: Operation[],
|
|
152
|
+
header: DocumentHeader,
|
|
153
|
+
) {
|
|
154
|
+
const document = await this.getDocument(drive, id);
|
|
155
|
+
if (!document) {
|
|
156
|
+
throw new Error(`Document with id ${id} not found`);
|
|
169
157
|
}
|
|
170
158
|
|
|
171
|
-
|
|
172
|
-
const files = readdirSync(this.drivesPath, {
|
|
173
|
-
withFileTypes: true
|
|
174
|
-
});
|
|
175
|
-
const drives: string[] = [];
|
|
176
|
-
for (const file of files.filter(file => file.isFile())) {
|
|
177
|
-
try {
|
|
178
|
-
const driveId = path.parse(file.name).name;
|
|
159
|
+
const mergedOperations = mergeOperations(document.operations, operations);
|
|
179
160
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
return drives;
|
|
188
|
-
}
|
|
161
|
+
await this.createDocument(drive, id, {
|
|
162
|
+
...document,
|
|
163
|
+
...header,
|
|
164
|
+
operations: mergedOperations,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
189
167
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
168
|
+
async getDrives() {
|
|
169
|
+
const files = readdirSync(this.drivesPath, {
|
|
170
|
+
withFileTypes: true,
|
|
171
|
+
});
|
|
172
|
+
const drives: string[] = [];
|
|
173
|
+
for (const file of files.filter((file) => file.isFile())) {
|
|
174
|
+
try {
|
|
175
|
+
const driveId = path.parse(file.name).name;
|
|
200
176
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return this.getDrive(drive);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
throw new Error(`Drive with slug ${slug} not found`);
|
|
177
|
+
// checks if file is drive
|
|
178
|
+
await this.getDrive(driveId);
|
|
179
|
+
drives.push(driveId);
|
|
180
|
+
} catch {
|
|
181
|
+
/* Ignore invalid drive document found on drives dir */
|
|
182
|
+
}
|
|
211
183
|
}
|
|
184
|
+
return drives;
|
|
185
|
+
}
|
|
212
186
|
|
|
213
|
-
|
|
214
|
-
|
|
187
|
+
async getDrive(id: string) {
|
|
188
|
+
try {
|
|
189
|
+
return (await this.getDocument(
|
|
190
|
+
FilesystemStorage.DRIVES_DIR,
|
|
191
|
+
id,
|
|
192
|
+
)) as DocumentDriveStorage;
|
|
193
|
+
} catch {
|
|
194
|
+
throw new DriveNotFoundError(id);
|
|
215
195
|
}
|
|
196
|
+
}
|
|
216
197
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
198
|
+
async getDriveBySlug(slug: string) {
|
|
199
|
+
// get oldes drives first
|
|
200
|
+
const drives = (await this.getDrives()).reverse();
|
|
201
|
+
for (const drive of drives) {
|
|
202
|
+
const {
|
|
203
|
+
initialState: {
|
|
204
|
+
state: {
|
|
205
|
+
global: { slug: driveSlug },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
} = await this.getDrive(drive);
|
|
209
|
+
if (driveSlug === slug) {
|
|
210
|
+
return this.getDrive(drive);
|
|
211
|
+
}
|
|
223
212
|
}
|
|
213
|
+
throw new Error(`Drive with slug ${slug} not found`);
|
|
214
|
+
}
|
|
224
215
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
header: DocumentHeader
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
const drive = await this.getDrive(id);
|
|
231
|
-
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
216
|
+
createDrive(id: string, drive: DocumentDriveStorage) {
|
|
217
|
+
return this.createDocument(FilesystemStorage.DRIVES_DIR, id, drive);
|
|
218
|
+
}
|
|
232
219
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
220
|
+
async deleteDrive(id: string) {
|
|
221
|
+
const documents = await this.getDocuments(id);
|
|
222
|
+
await this.deleteDocument(FilesystemStorage.DRIVES_DIR, id);
|
|
223
|
+
await Promise.all(
|
|
224
|
+
documents.map((document) => this.deleteDocument(id, document)),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
239
227
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
228
|
+
async addDriveOperations(
|
|
229
|
+
id: string,
|
|
230
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
231
|
+
header: DocumentHeader,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
const drive = await this.getDrive(id);
|
|
234
|
+
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
235
|
+
|
|
236
|
+
await this.createDrive(id, {
|
|
237
|
+
...drive,
|
|
238
|
+
...header,
|
|
239
|
+
operations: mergedOperations,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async getSynchronizationUnitsRevision(
|
|
244
|
+
units: SynchronizationUnitQuery[],
|
|
245
|
+
): Promise<
|
|
246
|
+
{
|
|
247
|
+
driveId: string;
|
|
248
|
+
documentId: string;
|
|
249
|
+
scope: string;
|
|
250
|
+
branch: string;
|
|
251
|
+
lastUpdated: string;
|
|
252
|
+
revision: number;
|
|
253
|
+
}[]
|
|
254
|
+
> {
|
|
255
|
+
const results = await Promise.allSettled(
|
|
256
|
+
units.map(async (unit) => {
|
|
257
|
+
try {
|
|
258
|
+
const document = await (unit.documentId
|
|
259
|
+
? this.getDocument(unit.driveId, unit.documentId)
|
|
260
|
+
: this.getDrive(unit.driveId));
|
|
261
|
+
if (!document) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
const operation =
|
|
265
|
+
document.operations[unit.scope as OperationScope].at(-1);
|
|
266
|
+
if (operation) {
|
|
267
|
+
return {
|
|
268
|
+
driveId: unit.driveId,
|
|
269
|
+
documentId: unit.documentId,
|
|
270
|
+
scope: unit.scope,
|
|
271
|
+
branch: unit.branch,
|
|
272
|
+
lastUpdated: operation.timestamp,
|
|
273
|
+
revision: operation.index,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
return results.reduce<
|
|
282
|
+
{
|
|
283
|
+
driveId: string;
|
|
284
|
+
documentId: string;
|
|
285
|
+
scope: string;
|
|
286
|
+
branch: string;
|
|
287
|
+
lastUpdated: string;
|
|
288
|
+
revision: number;
|
|
289
|
+
}[]
|
|
290
|
+
>((acc, curr) => {
|
|
291
|
+
if (curr.status === "fulfilled" && curr.value !== undefined) {
|
|
292
|
+
acc.push(curr.value);
|
|
293
|
+
}
|
|
294
|
+
return acc;
|
|
295
|
+
}, []);
|
|
296
|
+
}
|
|
296
297
|
}
|
package/src/storage/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from "./base";
|
|
2
|
+
export type * from "./types";
|