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.
Files changed (43) hide show
  1. package/README.md +1 -0
  2. package/package.json +74 -88
  3. package/src/cache/index.ts +2 -2
  4. package/src/cache/memory.ts +22 -13
  5. package/src/cache/redis.ts +43 -16
  6. package/src/cache/types.ts +4 -4
  7. package/src/index.ts +6 -3
  8. package/src/queue/base.ts +276 -214
  9. package/src/queue/index.ts +2 -2
  10. package/src/queue/redis.ts +138 -127
  11. package/src/queue/types.ts +44 -38
  12. package/src/read-mode/errors.ts +19 -0
  13. package/src/read-mode/index.ts +125 -0
  14. package/src/read-mode/service.ts +207 -0
  15. package/src/read-mode/types.ts +108 -0
  16. package/src/server/error.ts +61 -26
  17. package/src/server/index.ts +2160 -1785
  18. package/src/server/listener/index.ts +2 -2
  19. package/src/server/listener/manager.ts +475 -437
  20. package/src/server/listener/transmitter/index.ts +4 -5
  21. package/src/server/listener/transmitter/internal.ts +77 -79
  22. package/src/server/listener/transmitter/pull-responder.ts +363 -329
  23. package/src/server/listener/transmitter/switchboard-push.ts +72 -55
  24. package/src/server/listener/transmitter/types.ts +19 -25
  25. package/src/server/types.ts +536 -349
  26. package/src/server/utils.ts +26 -27
  27. package/src/storage/base.ts +81 -0
  28. package/src/storage/browser.ts +233 -216
  29. package/src/storage/filesystem.ts +257 -256
  30. package/src/storage/index.ts +2 -1
  31. package/src/storage/memory.ts +206 -214
  32. package/src/storage/prisma.ts +575 -568
  33. package/src/storage/sequelize.ts +460 -471
  34. package/src/storage/types.ts +83 -67
  35. package/src/utils/default-drives-manager.ts +341 -0
  36. package/src/utils/document-helpers.ts +19 -18
  37. package/src/utils/graphql.ts +288 -34
  38. package/src/utils/index.ts +61 -59
  39. package/src/utils/logger.ts +39 -37
  40. package/src/utils/migrations.ts +58 -0
  41. package/src/utils/run-asap.ts +156 -0
  42. package/CHANGELOG.md +0 -818
  43. package/src/server/listener/transmitter/subscription.ts +0 -364
@@ -1,81 +1,97 @@
1
1
  import type {
2
- DocumentDriveAction,
3
- DocumentDriveDocument,
4
- } from 'document-model-libs/document-drive';
2
+ DocumentDriveAction,
3
+ DocumentDriveDocument,
4
+ } from "document-model-libs/document-drive";
5
5
  import type {
6
- Action,
7
- BaseAction,
8
- Document,
9
- DocumentHeader,
10
- DocumentOperations,
11
- Operation,
12
- } from 'document-model/document';
13
- import type { SynchronizationUnitQuery } from '../server/types';
6
+ Action,
7
+ BaseAction,
8
+ Document,
9
+ DocumentHeader,
10
+ DocumentOperations,
11
+ Operation,
12
+ } from "document-model/document";
13
+ import type { SynchronizationUnitQuery } from "../server/types";
14
14
 
15
15
  export type DocumentStorage<D extends Document = Document> = Omit<
16
- D,
17
- 'attachments'
16
+ D,
17
+ "attachments"
18
18
  >;
19
19
 
20
20
  export type DocumentDriveStorage = DocumentStorage<DocumentDriveDocument>;
21
21
 
22
22
  export interface IStorageDelegate {
23
- getCachedOperations(drive: string, id: string): Promise<DocumentOperations<Action> | undefined>;
23
+ getCachedOperations(
24
+ drive: string,
25
+ id: string,
26
+ ): Promise<DocumentOperations<Action> | undefined>;
24
27
  }
25
28
 
26
29
  export interface IStorage {
27
- checkDocumentExists(drive: string, id: string): Promise<boolean>;
28
- getDocuments: (drive: string) => Promise<string[]>;
29
- getDocument(drive: string, id: string): Promise<DocumentStorage>;
30
- createDocument(
31
- drive: string,
32
- id: string,
33
- document: DocumentStorage
34
- ): Promise<void>;
35
- addDocumentOperations(
36
- drive: string,
37
- id: string,
38
- operations: Operation[],
39
- header: DocumentHeader,
40
- ): Promise<void>;
41
- addDocumentOperationsWithTransaction?(
42
- drive: string,
43
- id: string,
44
- callback: (document: DocumentStorage) => Promise<{
45
- operations: Operation[];
46
- header: DocumentHeader;
47
- }>
48
- ): Promise<void>;
49
- deleteDocument(drive: string, id: string): Promise<void>;
50
- getOperationResultingState?(drive: string, id: string, index: number, scope: string, branch: string): Promise<unknown>;
51
- setStorageDelegate?(delegate: IStorageDelegate): void;
52
- getSynchronizationUnitsRevision(units: SynchronizationUnitQuery[]): Promise<{
53
- driveId: string;
54
- documentId: string;
55
- scope: string;
56
- branch: string;
57
- lastUpdated: string;
58
- revision: number;
59
- }[]>
30
+ checkDocumentExists(drive: string, id: string): Promise<boolean>;
31
+ getDocuments: (drive: string) => Promise<string[]>;
32
+ getDocument(drive: string, id: string): Promise<DocumentStorage>;
33
+ createDocument(
34
+ drive: string,
35
+ id: string,
36
+ document: DocumentStorage,
37
+ ): Promise<void>;
38
+ addDocumentOperations(
39
+ drive: string,
40
+ id: string,
41
+ operations: Operation[],
42
+ header: DocumentHeader,
43
+ ): Promise<void>;
44
+ addDocumentOperationsWithTransaction?(
45
+ drive: string,
46
+ id: string,
47
+ callback: (document: DocumentStorage) => Promise<{
48
+ operations: Operation[];
49
+ header: DocumentHeader;
50
+ }>,
51
+ ): Promise<void>;
52
+ deleteDocument(drive: string, id: string): Promise<void>;
53
+ getOperationResultingState?(
54
+ drive: string,
55
+ id: string,
56
+ index: number,
57
+ scope: string,
58
+ branch: string,
59
+ ): Promise<unknown>;
60
+ setStorageDelegate?(delegate: IStorageDelegate): void;
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
- getDrives(): Promise<string[]>;
63
- getDrive(id: string): Promise<DocumentDriveStorage>;
64
- getDriveBySlug(slug: string): Promise<DocumentDriveStorage>;
65
- createDrive(id: string, drive: DocumentDriveStorage): Promise<void>;
66
- deleteDrive(id: string): Promise<void>;
67
- clearStorage?(): Promise<void>;
68
- addDriveOperations(
69
- id: string,
70
- operations: Operation<DocumentDriveAction | BaseAction>[],
71
- header: DocumentHeader
72
- ): Promise<void>;
73
- addDriveOperationsWithTransaction?(
74
- drive: string,
75
- callback: (document: DocumentDriveStorage) => Promise<{
76
- operations: Operation[];
77
- header: DocumentHeader;
78
- }>
79
- ): Promise<void>;
80
- getDriveOperationResultingState?(drive: string, index: number, scope: string, branch: string): Promise<unknown>;
81
- }
73
+ getDrives(): Promise<string[]>;
74
+ getDrive(id: string): Promise<DocumentDriveStorage>;
75
+ getDriveBySlug(slug: string): Promise<DocumentDriveStorage>;
76
+ createDrive(id: string, drive: DocumentDriveStorage): Promise<void>;
77
+ deleteDrive(id: string): Promise<void>;
78
+ clearStorage?(): Promise<void>;
79
+ addDriveOperations(
80
+ id: string,
81
+ operations: Operation<DocumentDriveAction | BaseAction>[],
82
+ header: DocumentHeader,
83
+ ): Promise<void>;
84
+ addDriveOperationsWithTransaction?(
85
+ drive: string,
86
+ callback: (document: DocumentDriveStorage) => Promise<{
87
+ operations: Operation[];
88
+ header: DocumentHeader;
89
+ }>,
90
+ ): Promise<void>;
91
+ getDriveOperationResultingState?(
92
+ drive: string,
93
+ index: number,
94
+ scope: string,
95
+ branch: string,
96
+ ): Promise<unknown>;
97
+ }
@@ -0,0 +1,341 @@
1
+ import {
2
+ DefaultRemoteDriveInfo,
3
+ DocumentDriveServerOptions,
4
+ DriveEvents,
5
+ IBaseDocumentDriveServer,
6
+ IReadModeDriveServer,
7
+ RemoteDriveAccessLevel,
8
+ RemoveDriveStrategy,
9
+ RemoveOldRemoteDrivesOption,
10
+ } from "../server";
11
+ import { DriveNotFoundError } from "../server/error";
12
+ import { requestPublicDrive } from "./graphql";
13
+ import { logger } from "./logger";
14
+
15
+ export interface IServerDelegateDrivesManager {
16
+ emit: (...args: Parameters<DriveEvents["defaultRemoteDrive"]>) => void;
17
+ detachDrive: (driveId: string) => Promise<void>;
18
+ }
19
+
20
+ function isReadModeDriveServer(obj: unknown): obj is IReadModeDriveServer {
21
+ return typeof (obj as IReadModeDriveServer).getReadDrives === "function";
22
+ }
23
+
24
+ export interface IDefaultDrivesManager {
25
+ initializeDefaultRemoteDrives(): Promise<void>;
26
+ getDefaultRemoteDrives(): Map<string, DefaultRemoteDriveInfo>;
27
+ setDefaultDriveAccessLevel(
28
+ url: string,
29
+ level: RemoteDriveAccessLevel,
30
+ ): Promise<void>;
31
+ setAllDefaultDrivesAccessLevel(level: RemoteDriveAccessLevel): Promise<void>;
32
+ }
33
+
34
+ export class DefaultDrivesManager implements IDefaultDrivesManager {
35
+ private defaultRemoteDrives = new Map<string, DefaultRemoteDriveInfo>();
36
+ private removeOldRemoteDrivesConfig: RemoveOldRemoteDrivesOption;
37
+
38
+ constructor(
39
+ private server:
40
+ | IBaseDocumentDriveServer
41
+ | (IBaseDocumentDriveServer & IReadModeDriveServer),
42
+ private delegate: IServerDelegateDrivesManager,
43
+ options?: Pick<DocumentDriveServerOptions, "defaultDrives">,
44
+ ) {
45
+ if (options?.defaultDrives.remoteDrives) {
46
+ for (const defaultDrive of options.defaultDrives.remoteDrives) {
47
+ this.defaultRemoteDrives.set(defaultDrive.url, {
48
+ ...defaultDrive,
49
+ status: "PENDING",
50
+ });
51
+ }
52
+ }
53
+
54
+ this.removeOldRemoteDrivesConfig = options?.defaultDrives
55
+ .removeOldRemoteDrives || {
56
+ strategy: "preserve-all",
57
+ };
58
+ }
59
+
60
+ getDefaultRemoteDrives() {
61
+ return new Map(
62
+ JSON.parse(
63
+ JSON.stringify(Array.from(this.defaultRemoteDrives)),
64
+ ) as Iterable<[string, DefaultRemoteDriveInfo]>,
65
+ );
66
+ }
67
+
68
+ private async deleteDriveById(driveId: string) {
69
+ try {
70
+ await this.server.deleteDrive(driveId);
71
+ } catch (error) {
72
+ if (!(error instanceof DriveNotFoundError)) {
73
+ logger.error(error);
74
+ }
75
+ }
76
+ }
77
+
78
+ private async preserveDrivesById(
79
+ driveIdsToPreserve: string[],
80
+ drives: string[],
81
+ removeStrategy: RemoveDriveStrategy = "detach",
82
+ ) {
83
+ const getAllDrives = drives.map((driveId) => this.server.getDrive(driveId));
84
+
85
+ const drivesToRemove = (await Promise.all(getAllDrives))
86
+ .filter(
87
+ (drive) =>
88
+ drive.state.local.listeners.length > 0 ||
89
+ drive.state.local.triggers.length > 0,
90
+ )
91
+ .filter((drive) => !driveIdsToPreserve.includes(drive.state.global.id));
92
+
93
+ const driveIds = drivesToRemove.map((drive) => drive.state.global.id);
94
+
95
+ if (removeStrategy === "detach") {
96
+ await this.detachDrivesById(driveIds);
97
+ } else {
98
+ await this.removeDrivesById(driveIds);
99
+ }
100
+ }
101
+
102
+ private async removeDrivesById(driveIds: string[]) {
103
+ for (const driveId of driveIds) {
104
+ await this.deleteDriveById(driveId);
105
+ }
106
+ }
107
+
108
+ private async detachDrivesById(driveIds: string[]) {
109
+ const detachDrivesPromises = driveIds.map((driveId) =>
110
+ this.delegate.detachDrive(driveId),
111
+ );
112
+
113
+ await Promise.all(detachDrivesPromises);
114
+ }
115
+
116
+ async removeOldremoteDrives() {
117
+ const driveids = await this.server.getDrives();
118
+
119
+ switch (this.removeOldRemoteDrivesConfig.strategy) {
120
+ case "preserve-by-id-and-detach":
121
+ case "preserve-by-id": {
122
+ const detach: RemoveDriveStrategy =
123
+ this.removeOldRemoteDrivesConfig.strategy ===
124
+ "preserve-by-id-and-detach"
125
+ ? "detach"
126
+ : "remove";
127
+
128
+ await this.preserveDrivesById(
129
+ this.removeOldRemoteDrivesConfig.ids,
130
+ driveids,
131
+ detach,
132
+ );
133
+ break;
134
+ }
135
+ case "preserve-by-url-and-detach":
136
+ case "preserve-by-url": {
137
+ const detach: RemoveDriveStrategy =
138
+ this.removeOldRemoteDrivesConfig.strategy ===
139
+ "preserve-by-url-and-detach"
140
+ ? "detach"
141
+ : "remove";
142
+
143
+ const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map((url) =>
144
+ requestPublicDrive(url),
145
+ );
146
+
147
+ const drivesIdsToPreserve = (await Promise.all(getDrivesInfo)).map(
148
+ (driveInfo) => driveInfo.id,
149
+ );
150
+
151
+ await this.preserveDrivesById(drivesIdsToPreserve, driveids, detach);
152
+ break;
153
+ }
154
+ case "remove-by-id": {
155
+ const drivesIdsToRemove = this.removeOldRemoteDrivesConfig.ids.filter(
156
+ (driveId) => driveids.includes(driveId),
157
+ );
158
+
159
+ await this.removeDrivesById(drivesIdsToRemove);
160
+ break;
161
+ }
162
+ case "remove-by-url": {
163
+ const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
164
+ (driveUrl) => requestPublicDrive(driveUrl),
165
+ );
166
+ const drivesInfo = await Promise.all(getDrivesInfo);
167
+
168
+ const drivesIdsToRemove = drivesInfo
169
+ .map((driveInfo) => driveInfo.id)
170
+ .filter((driveId) => driveids.includes(driveId));
171
+
172
+ await this.removeDrivesById(drivesIdsToRemove);
173
+ break;
174
+ }
175
+ case "remove-all": {
176
+ const getDrives = driveids.map((driveId) =>
177
+ this.server.getDrive(driveId),
178
+ );
179
+ const drives = await Promise.all(getDrives);
180
+ const drivesToRemove = drives
181
+ .filter(
182
+ (drive) =>
183
+ drive.state.local.listeners.length > 0 ||
184
+ drive.state.local.triggers.length > 0,
185
+ )
186
+ .map((drive) => drive.state.global.id);
187
+
188
+ await this.removeDrivesById(drivesToRemove);
189
+ break;
190
+ }
191
+ case "detach-by-id": {
192
+ const drivesIdsToRemove = this.removeOldRemoteDrivesConfig.ids.filter(
193
+ (driveId) => driveids.includes(driveId),
194
+ );
195
+ const detachDrivesPromises = drivesIdsToRemove.map((driveId) =>
196
+ this.delegate.detachDrive(driveId),
197
+ );
198
+
199
+ await Promise.all(detachDrivesPromises);
200
+ break;
201
+ }
202
+ case "detach-by-url": {
203
+ const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
204
+ (driveUrl) => requestPublicDrive(driveUrl),
205
+ );
206
+ const drivesInfo = await Promise.all(getDrivesInfo);
207
+
208
+ const drivesIdsToRemove = drivesInfo
209
+ .map((driveInfo) => driveInfo.id)
210
+ .filter((driveId) => driveids.includes(driveId));
211
+
212
+ const detachDrivesPromises = drivesIdsToRemove.map((driveId) =>
213
+ this.delegate.detachDrive(driveId),
214
+ );
215
+
216
+ await Promise.all(detachDrivesPromises);
217
+ break;
218
+ }
219
+ }
220
+ }
221
+
222
+ async setAllDefaultDrivesAccessLevel(level: RemoteDriveAccessLevel) {
223
+ const drives = this.defaultRemoteDrives.values();
224
+ for (const drive of drives) {
225
+ await this.setDefaultDriveAccessLevel(drive.url, level);
226
+ }
227
+ }
228
+
229
+ async setDefaultDriveAccessLevel(url: string, level: RemoteDriveAccessLevel) {
230
+ const drive = this.defaultRemoteDrives.get(url);
231
+ if (drive && drive.options.accessLevel !== level) {
232
+ const newDriveValue = {
233
+ ...drive,
234
+ options: { ...drive.options, accessLevel: level },
235
+ };
236
+ this.defaultRemoteDrives.set(url, newDriveValue);
237
+ await this.initializeDefaultRemoteDrives([newDriveValue]);
238
+ }
239
+ }
240
+
241
+ async initializeDefaultRemoteDrives(
242
+ defaultDrives: DefaultRemoteDriveInfo[] = Array.from(
243
+ this.defaultRemoteDrives.values(),
244
+ ),
245
+ ) {
246
+ const drives = await this.server.getDrives();
247
+ const readServer = isReadModeDriveServer(this.server)
248
+ ? (this.server as IReadModeDriveServer)
249
+ : undefined;
250
+ const readDrives = await readServer?.getReadDrives();
251
+
252
+ for (const remoteDrive of defaultDrives) {
253
+ let remoteDriveInfo = { ...remoteDrive };
254
+
255
+ try {
256
+ const driveInfo =
257
+ remoteDrive.metadata ?? (await requestPublicDrive(remoteDrive.url));
258
+
259
+ remoteDriveInfo = { ...remoteDrive, metadata: driveInfo };
260
+
261
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
262
+
263
+ const driveIsAdded = drives.includes(driveInfo.id);
264
+ const readDriveIsAdded = readDrives?.includes(driveInfo.id);
265
+
266
+ const hasAccessLevel = remoteDrive.options.accessLevel !== undefined;
267
+ const readMode =
268
+ readServer && remoteDrive.options.accessLevel === "READ";
269
+ const isAdded = readMode ? readDriveIsAdded : driveIsAdded;
270
+
271
+ // if the read mode has changed then existing drives
272
+ // in the previous mode should be deleted
273
+ const driveToDelete =
274
+ hasAccessLevel && (readMode ? driveIsAdded : readDriveIsAdded);
275
+ if (driveToDelete) {
276
+ try {
277
+ await (readMode
278
+ ? this.server.deleteDrive(driveInfo.id)
279
+ : readServer?.deleteReadDrive(driveInfo.id));
280
+ } catch (e) {
281
+ logger.error(e);
282
+ }
283
+ }
284
+
285
+ if (isAdded) {
286
+ remoteDriveInfo.status = "ALREADY_ADDED";
287
+
288
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
289
+ this.delegate.emit(
290
+ "ALREADY_ADDED",
291
+ this.defaultRemoteDrives,
292
+ remoteDriveInfo,
293
+ driveInfo.id,
294
+ driveInfo.name,
295
+ );
296
+ continue;
297
+ }
298
+
299
+ remoteDriveInfo.status = "ADDING";
300
+
301
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
302
+ this.delegate.emit("ADDING", this.defaultRemoteDrives, remoteDriveInfo);
303
+
304
+ if ((!hasAccessLevel && readServer) || readMode) {
305
+ await readServer.addReadDrive(remoteDrive.url, {
306
+ ...remoteDrive.options,
307
+ expectedDriveInfo: driveInfo,
308
+ });
309
+ } else {
310
+ await this.server.addRemoteDrive(remoteDrive.url, {
311
+ ...remoteDrive.options,
312
+ expectedDriveInfo: driveInfo,
313
+ });
314
+ }
315
+
316
+ remoteDriveInfo.status = "SUCCESS";
317
+
318
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
319
+ this.delegate.emit(
320
+ "SUCCESS",
321
+ this.defaultRemoteDrives,
322
+ remoteDriveInfo,
323
+ driveInfo.id,
324
+ driveInfo.name,
325
+ );
326
+ } catch (error) {
327
+ remoteDriveInfo.status = "ERROR";
328
+
329
+ this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
330
+ this.delegate.emit(
331
+ "ERROR",
332
+ this.defaultRemoteDrives,
333
+ remoteDriveInfo,
334
+ undefined,
335
+ undefined,
336
+ error as Error,
337
+ );
338
+ }
339
+ }
340
+ }
341
+ }
@@ -1,20 +1,21 @@
1
- import { utils } from 'document-model/document';
1
+ import { utils } from "document-model/document";
2
2
 
3
- export const { attachBranch,
4
- garbageCollect,
5
- groupOperationsByScope,
6
- merge,
7
- split,
8
- precedes,
9
- removeExistingOperations,
10
- reshuffleByTimestamp,
11
- sortOperations,
12
- addUndo,
13
- checkOperationsIntegrity,
14
- checkCleanedOperationsIntegrity,
15
- reshuffleByTimestampAndIndex,
16
- nextSkipNumber,
17
- prepareOperations,
18
- IntegrityIssueSubType,
19
- IntegrityIssueType
3
+ export const {
4
+ attachBranch,
5
+ garbageCollect,
6
+ groupOperationsByScope,
7
+ merge,
8
+ split,
9
+ precedes,
10
+ removeExistingOperations,
11
+ reshuffleByTimestamp,
12
+ sortOperations,
13
+ addUndo,
14
+ checkOperationsIntegrity,
15
+ checkCleanedOperationsIntegrity,
16
+ reshuffleByTimestampAndIndex,
17
+ nextSkipNumber,
18
+ prepareOperations,
19
+ IntegrityIssueSubType,
20
+ IntegrityIssueType,
20
21
  } = utils.documentHelpers;