document-drive 1.0.0-websockets.1 → 1.0.1

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,35 +1,34 @@
1
- import type { Document, OperationScope } from 'document-model/document';
2
- import { RevisionsFilter, StrandUpdate } from './types';
1
+ import type { Document, OperationScope } from "document-model/document";
2
+ import { RevisionsFilter, StrandUpdate } from "./types";
3
3
 
4
4
  export function buildRevisionsFilter(
5
- strands: StrandUpdate[],
6
- driveId: string,
7
- documentId: string
5
+ strands: StrandUpdate[],
6
+ driveId: string,
7
+ documentId: string,
8
8
  ): RevisionsFilter {
9
- return strands.reduce<RevisionsFilter>((acc, s) => {
10
- if (!(s.driveId === driveId && s.documentId === documentId)) {
11
- return acc;
12
- }
13
- acc[s.scope] = s.operations[s.operations.length - 1]?.index ?? -1;
14
- return acc;
15
- }, {});
9
+ return strands.reduce<RevisionsFilter>((acc, s) => {
10
+ if (!(s.driveId === driveId && s.documentId === documentId)) {
11
+ return acc;
12
+ }
13
+ acc[s.scope] = s.operations[s.operations.length - 1]?.index ?? -1;
14
+ return acc;
15
+ }, {});
16
16
  }
17
17
 
18
18
  export function filterOperationsByRevision(
19
- operations: Document['operations'],
20
- revisions?: RevisionsFilter
21
- ): Document['operations'] {
22
- if (!revisions) {
23
- return operations;
19
+ operations: Document["operations"],
20
+ revisions?: RevisionsFilter,
21
+ ): Document["operations"] {
22
+ if (!revisions) {
23
+ return operations;
24
+ }
25
+ return (Object.keys(operations) as OperationScope[]).reduce<
26
+ Document["operations"]
27
+ >((acc, scope) => {
28
+ const revision = revisions[scope];
29
+ if (revision !== undefined) {
30
+ acc[scope] = operations[scope].filter((op) => op.index <= revision);
24
31
  }
25
- return (Object.keys(operations) as OperationScope[]).reduce<
26
- Document['operations']
27
- >((acc, scope) => {
28
- const revision = revisions[scope];
29
- if (revision !== undefined) {
30
- acc[scope] = operations[scope].filter(op => op.index <= revision);
31
- }
32
- return acc;
33
- }, operations);
32
+ return acc;
33
+ }, operations);
34
34
  }
35
-
@@ -0,0 +1,81 @@
1
+ import { DocumentDriveAction } from "document-model-libs/document-drive";
2
+ import { BaseAction, DocumentHeader, Operation } from "document-model/document";
3
+ import { SynchronizationUnitQuery } from "../server";
4
+ import {
5
+ DocumentDriveStorage,
6
+ DocumentStorage,
7
+ IDriveStorage,
8
+ IStorage,
9
+ IStorageDelegate,
10
+ } from "./types";
11
+
12
+ abstract class BaseStorage implements IStorage {
13
+ abstract checkDocumentExists(drive: string, id: string): Promise<boolean>;
14
+
15
+ abstract getDocuments(drive: string): Promise<string[]>;
16
+
17
+ abstract getDocument(drive: string, id: string): Promise<DocumentStorage>;
18
+
19
+ abstract createDocument(
20
+ drive: string,
21
+ id: string,
22
+ document: DocumentStorage,
23
+ ): Promise<void>;
24
+
25
+ abstract addDocumentOperations(
26
+ drive: string,
27
+ id: string,
28
+ operations: Operation[],
29
+ header: DocumentHeader,
30
+ ): Promise<void>;
31
+
32
+ abstract addDocumentOperationsWithTransaction?(
33
+ drive: string,
34
+ id: string,
35
+ callback: (document: DocumentStorage) => Promise<{
36
+ operations: Operation[];
37
+ header: DocumentHeader;
38
+ }>,
39
+ ): Promise<void>;
40
+
41
+ abstract deleteDocument(drive: string, id: string): Promise<void>;
42
+
43
+ abstract getOperationResultingState?(
44
+ drive: string,
45
+ id: string,
46
+ index: number,
47
+ scope: string,
48
+ branch: string,
49
+ ): Promise<unknown>;
50
+
51
+ abstract setStorageDelegate?(delegate: IStorageDelegate): void;
52
+
53
+ abstract getSynchronizationUnitsRevision(
54
+ units: SynchronizationUnitQuery[],
55
+ ): Promise<
56
+ {
57
+ driveId: string;
58
+ documentId: string;
59
+ scope: string;
60
+ branch: string;
61
+ lastUpdated: string;
62
+ revision: number;
63
+ }[]
64
+ >;
65
+ }
66
+
67
+ export abstract class BaseDriveStorage
68
+ extends BaseStorage
69
+ implements IDriveStorage
70
+ {
71
+ abstract getDrives(): Promise<string[]>;
72
+ abstract getDrive(id: string): Promise<DocumentDriveStorage>;
73
+ abstract getDriveBySlug(slug: string): Promise<DocumentDriveStorage>;
74
+ abstract createDrive(id: string, drive: DocumentDriveStorage): Promise<void>;
75
+ abstract deleteDrive(id: string): Promise<void>;
76
+ abstract addDriveOperations(
77
+ id: string,
78
+ operations: Operation<DocumentDriveAction | BaseAction>[],
79
+ header: DocumentHeader,
80
+ ): Promise<void>;
81
+ }
@@ -1,221 +1,238 @@
1
- import { DocumentDriveAction } from 'document-model-libs/document-drive';
1
+ import { DocumentDriveAction } from "document-model-libs/document-drive";
2
2
  import {
3
- BaseAction,
4
- Document,
5
- DocumentHeader,
6
- Operation,
7
- OperationScope
8
- } from 'document-model/document';
9
- import { mergeOperations, type SynchronizationUnitQuery } from '..';
10
- import {
11
- DocumentDriveStorage,
12
- DocumentStorage,
13
- IDriveStorage,
14
- } from './types';
3
+ BaseAction,
4
+ Document,
5
+ DocumentHeader,
6
+ Operation,
7
+ OperationScope,
8
+ } from "document-model/document";
9
+ import LocalForage from "localforage";
10
+ import { DriveNotFoundError } from "../server/error";
11
+ import { SynchronizationUnitQuery } from "../server/types";
12
+ import { mergeOperations } from "../utils";
13
+ import { migrateDocumentOperationSigatures } from "../utils/migrations";
14
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from "./types";
15
15
 
16
16
  export class BrowserStorage implements IDriveStorage {
17
- private db: Promise<LocalForage>;
18
-
19
- static DBName = 'DOCUMENT_DRIVES';
20
- static SEP = ':';
21
- static DRIVES_KEY = 'DRIVES';
22
-
23
- constructor(namespace?: string) {
24
- this.db = import('localforage').then(localForage =>
25
- localForage.default.createInstance({
26
- name: namespace
27
- ? `${namespace}:${BrowserStorage.DBName}`
28
- : BrowserStorage.DBName
29
- })
30
- );
31
- }
32
-
33
- buildKey(...args: string[]) {
34
- return args.join(BrowserStorage.SEP);
35
- }
36
-
37
- async checkDocumentExists(drive: string, id: string): Promise<boolean> {
38
- const document = await (
39
- await this.db
40
- ).getItem<Document>(this.buildKey(drive, id));
41
- return document !== undefined;
42
- }
43
-
44
- async getDocuments(drive: string) {
45
- const keys = await (await this.db).keys();
46
- const driveKey = `${drive}${BrowserStorage.SEP}`;
47
- return keys
48
- .filter(key => key.startsWith(driveKey))
49
- .map(key => key.slice(driveKey.length));
50
- }
51
-
52
- async getDocument(driveId: string, id: string) {
53
- const document = await (
54
- await this.db
55
- ).getItem<Document>(this.buildKey(driveId, id));
56
- if (!document) {
57
- throw new Error(`Document with id ${id} not found`);
17
+ private db: Promise<LocalForage>;
18
+
19
+ static DBName = "DOCUMENT_DRIVES";
20
+ static SEP = ":";
21
+ static DRIVES_KEY = "DRIVES";
22
+
23
+ constructor(namespace?: string) {
24
+ this.db = LocalForage.ready().then(() =>
25
+ LocalForage.createInstance({
26
+ name: namespace
27
+ ? `${namespace}:${BrowserStorage.DBName}`
28
+ : BrowserStorage.DBName,
29
+ }),
30
+ );
31
+ }
32
+
33
+ buildKey(...args: string[]) {
34
+ return args.join(BrowserStorage.SEP);
35
+ }
36
+
37
+ async checkDocumentExists(drive: string, id: string): Promise<boolean> {
38
+ const document = await (
39
+ await this.db
40
+ ).getItem<Document>(this.buildKey(drive, id));
41
+ return !!document;
42
+ }
43
+
44
+ async getDocuments(drive: string) {
45
+ const db = await this.db;
46
+ const keys = await db.keys();
47
+ const driveKey = `${drive}${BrowserStorage.SEP}`;
48
+ return keys
49
+ .filter((key) => key.startsWith(driveKey))
50
+ .map((key) => key.slice(driveKey.length));
51
+ }
52
+
53
+ async getDocument(driveId: string, id: string) {
54
+ const document = await (
55
+ await this.db
56
+ ).getItem<Document>(this.buildKey(driveId, id));
57
+ if (!document) {
58
+ throw new Error(`Document with id ${id} not found`);
59
+ }
60
+ return document;
61
+ }
62
+
63
+ async createDocument(drive: string, id: string, document: DocumentStorage) {
64
+ await (await this.db).setItem(this.buildKey(drive, id), document);
65
+ }
66
+
67
+ async deleteDocument(drive: string, id: string) {
68
+ await (await this.db).removeItem(this.buildKey(drive, id));
69
+ }
70
+
71
+ async clearStorage(): Promise<void> {
72
+ return (await this.db).clear();
73
+ }
74
+
75
+ async addDocumentOperations(
76
+ drive: string,
77
+ id: string,
78
+ operations: Operation[],
79
+ header: DocumentHeader,
80
+ ): Promise<void> {
81
+ const document = await this.getDocument(drive, id);
82
+ if (!document) {
83
+ throw new Error(`Document with id ${id} not found`);
84
+ }
85
+
86
+ const mergedOperations = mergeOperations(document.operations, operations);
87
+
88
+ const db = await this.db;
89
+ await db.setItem(this.buildKey(drive, id), {
90
+ ...document,
91
+ ...header,
92
+ operations: mergedOperations,
93
+ });
94
+ }
95
+
96
+ async getDrives() {
97
+ const db = await this.db;
98
+ const keys = await db.keys();
99
+ return keys
100
+ .filter((key) => key.startsWith(BrowserStorage.DRIVES_KEY))
101
+ .map((key) =>
102
+ key.slice(BrowserStorage.DRIVES_KEY.length + BrowserStorage.SEP.length),
103
+ );
104
+ }
105
+
106
+ async getDrive(id: string) {
107
+ const db = await this.db;
108
+ const drive = await db.getItem<DocumentDriveStorage>(
109
+ this.buildKey(BrowserStorage.DRIVES_KEY, id),
110
+ );
111
+ if (!drive) {
112
+ throw new DriveNotFoundError(id);
113
+ }
114
+ return drive;
115
+ }
116
+
117
+ async getDriveBySlug(slug: string) {
118
+ // get oldes drives first
119
+ const drives = (await this.getDrives()).reverse();
120
+ for (const drive of drives) {
121
+ const driveData = await this.getDrive(drive);
122
+ if (driveData.initialState.state.global.slug === slug) {
123
+ return this.getDrive(drive);
124
+ }
125
+ }
126
+
127
+ throw new Error(`Drive with slug ${slug} not found`);
128
+ }
129
+
130
+ async createDrive(id: string, drive: DocumentDriveStorage) {
131
+ const db = await this.db;
132
+ await db.setItem(this.buildKey(BrowserStorage.DRIVES_KEY, id), drive);
133
+ }
134
+
135
+ async deleteDrive(id: string) {
136
+ const documents = await this.getDocuments(id);
137
+ await Promise.all(documents.map((doc) => this.deleteDocument(id, doc)));
138
+ return (await this.db).removeItem(
139
+ this.buildKey(BrowserStorage.DRIVES_KEY, id),
140
+ );
141
+ }
142
+
143
+ async addDriveOperations(
144
+ id: string,
145
+ operations: Operation<DocumentDriveAction | BaseAction>[],
146
+ header: DocumentHeader,
147
+ ): Promise<void> {
148
+ const drive = await this.getDrive(id);
149
+ const mergedOperations = mergeOperations(drive.operations, operations);
150
+ const db = await this.db;
151
+
152
+ await db.setItem(this.buildKey(BrowserStorage.DRIVES_KEY, id), {
153
+ ...drive,
154
+ ...header,
155
+ operations: mergedOperations,
156
+ });
157
+ return;
158
+ }
159
+
160
+ async getSynchronizationUnitsRevision(
161
+ units: SynchronizationUnitQuery[],
162
+ ): Promise<
163
+ {
164
+ driveId: string;
165
+ documentId: string;
166
+ scope: string;
167
+ branch: string;
168
+ lastUpdated: string;
169
+ revision: number;
170
+ }[]
171
+ > {
172
+ const results = await Promise.allSettled(
173
+ units.map(async (unit) => {
174
+ try {
175
+ const document = await (unit.documentId
176
+ ? this.getDocument(unit.driveId, unit.documentId)
177
+ : this.getDrive(unit.driveId));
178
+ if (!document) {
179
+ return undefined;
180
+ }
181
+ const operation =
182
+ document.operations[unit.scope as OperationScope].at(-1);
183
+ if (operation) {
184
+ return {
185
+ driveId: unit.driveId,
186
+ documentId: unit.documentId,
187
+ scope: unit.scope,
188
+ branch: unit.branch,
189
+ lastUpdated: operation.timestamp,
190
+ revision: operation.index,
191
+ };
192
+ }
193
+ } catch {
194
+ return undefined;
58
195
  }
59
- return document;
60
- }
61
-
62
- async createDocument(drive: string, id: string, document: DocumentStorage) {
63
- await (await this.db).setItem(this.buildKey(drive, id), document);
64
- }
65
-
66
- async deleteDocument(drive: string, id: string) {
67
- await (await this.db).removeItem(this.buildKey(drive, id));
68
- }
69
-
70
- async clearStorage(): Promise<void> {
71
- return (await this.db).clear();
72
- }
73
-
74
- async addDocumentOperations(
75
- drive: string,
76
- id: string,
77
- operations: Operation[],
78
- header: DocumentHeader
79
- ): Promise<void> {
80
- const document = await this.getDocument(drive, id);
81
- if (!document) {
82
- throw new Error(`Document with id ${id} not found`);
83
- }
84
-
85
- const mergedOperations = mergeOperations(
86
- document.operations,
87
- operations
88
- );
89
-
90
- const db = await this.db;
91
- await db.setItem(this.buildKey(drive, id), {
92
- ...document,
93
- ...header,
94
- operations: mergedOperations
95
- });
96
- }
97
-
98
- async getDrives() {
99
- const db = await this.db;
100
- const keys = (await db.keys()) ?? [];
101
- return keys
102
- .filter(key => key.startsWith(BrowserStorage.DRIVES_KEY))
103
- .map(key =>
104
- key.slice(
105
- BrowserStorage.DRIVES_KEY.length + BrowserStorage.SEP.length
106
- )
107
- );
108
- }
109
-
110
- async getDrive(id: string) {
111
- const drive = await (
112
- await this.db
113
- ).getItem<DocumentDriveStorage>(
114
- this.buildKey(BrowserStorage.DRIVES_KEY, id)
115
- );
116
- if (!drive) {
117
- throw new Error(`Drive with id ${id} not found`);
118
- }
119
- return drive;
120
- }
121
-
122
- async getDriveBySlug(slug: string) {
123
- // get oldes drives first
124
- const drives = (await this.getDrives()).reverse();
125
- for (const drive of drives) {
126
- const driveData = await this.getDrive(drive);
127
- if (driveData.initialState.state.global.slug === slug) {
128
- return this.getDrive(drive);
129
- }
130
- }
131
-
132
- throw new Error(`Drive with slug ${slug} not found`);
133
- }
134
-
135
- async createDrive(id: string, drive: DocumentDriveStorage) {
136
- const db = await this.db;
137
- await db.setItem(this.buildKey(BrowserStorage.DRIVES_KEY, id), drive);
138
- }
139
-
140
- async deleteDrive(id: string) {
141
- const documents = await this.getDocuments(id);
142
- await Promise.all(documents.map(doc => this.deleteDocument(id, doc)));
143
- return (await this.db).removeItem(
144
- this.buildKey(BrowserStorage.DRIVES_KEY, id)
145
- );
146
- }
147
-
148
- async addDriveOperations(
149
- id: string,
150
- operations: Operation<DocumentDriveAction | BaseAction>[],
151
- header: DocumentHeader
152
- ): Promise<void> {
153
- const drive = await this.getDrive(id);
154
- const mergedOperations = mergeOperations(drive.operations, operations);
155
- const db = await this.db;
156
-
157
- await db.setItem(this.buildKey(BrowserStorage.DRIVES_KEY, id), {
158
- ...drive,
159
- ...header,
160
- operations: mergedOperations
161
- });
162
- return;
163
- }
164
-
165
- async getSynchronizationUnitsRevision(
166
- units: SynchronizationUnitQuery[]
167
- ): Promise<
168
- {
169
- driveId: string;
170
- documentId: string;
171
- scope: string;
172
- branch: string;
173
- lastUpdated: string;
174
- revision: number;
175
- }[]
176
- > {
177
- const results = await Promise.allSettled(
178
- units.map(async unit => {
179
- try {
180
- const document = await (unit.documentId
181
- ? this.getDocument(unit.driveId, unit.documentId)
182
- : this.getDrive(unit.driveId));
183
- if (!document) {
184
- return undefined;
185
- }
186
- const operation =
187
- document.operations[unit.scope as OperationScope]?.at(
188
- -1
189
- );
190
- if (operation) {
191
- return {
192
- driveId: unit.driveId,
193
- documentId: unit.documentId,
194
- scope: unit.scope,
195
- branch: unit.branch,
196
- lastUpdated: operation.timestamp,
197
- revision: operation.index
198
- };
199
- }
200
- } catch {
201
- return undefined;
202
- }
203
- })
204
- );
205
- return results.reduce<
206
- {
207
- driveId: string;
208
- documentId: string;
209
- scope: string;
210
- branch: string;
211
- lastUpdated: string;
212
- revision: number;
213
- }[]
214
- >((acc, curr) => {
215
- if (curr.status === 'fulfilled' && curr.value !== undefined) {
216
- acc.push(curr.value);
217
- }
218
- return acc;
219
- }, []);
220
- }
196
+ }),
197
+ );
198
+ return results.reduce<
199
+ {
200
+ driveId: string;
201
+ documentId: string;
202
+ scope: string;
203
+ branch: string;
204
+ lastUpdated: string;
205
+ revision: number;
206
+ }[]
207
+ >((acc, curr) => {
208
+ if (curr.status === "fulfilled" && curr.value !== undefined) {
209
+ acc.push(curr.value);
210
+ }
211
+ return acc;
212
+ }, []);
213
+ }
214
+
215
+ // migrates all stored operations from legacy signature to signatures array
216
+ async migrateOperationSignatures() {
217
+ const drives = await this.getDrives();
218
+ for (const drive of drives) {
219
+ await this.migrateDocument(BrowserStorage.DRIVES_KEY, drive);
220
+
221
+ const documents = await this.getDocuments(drive);
222
+ await Promise.all(
223
+ documents.map(async (docId) => this.migrateDocument(drive, docId)),
224
+ );
225
+ }
226
+ }
227
+
228
+ private async migrateDocument(drive: string, id: string) {
229
+ const document = await this.getDocument(drive, id);
230
+ const migratedDocument = migrateDocumentOperationSigatures(document);
231
+ if (migratedDocument !== document) {
232
+ return (await this.db).setItem(
233
+ this.buildKey(drive, id),
234
+ migratedDocument,
235
+ );
236
+ }
237
+ }
221
238
  }