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,47 +1,301 @@
1
- import request, { GraphQLClient, gql } from 'graphql-request';
2
- import { logger } from './logger';
1
+ import { pascalCase } from "change-case";
2
+ import {
3
+ DocumentDriveLocalState,
4
+ FileNode,
5
+ FolderNode,
6
+ } from "document-model-libs/document-drive";
7
+ import { Document, DocumentModel, Operation } from "document-model/document";
8
+ import { DocumentModelState } from "document-model/document-model";
9
+ import {
10
+ BuildSchemaOptions,
11
+ GraphQLError,
12
+ GraphQLList,
13
+ GraphQLNonNull,
14
+ GraphQLObjectType,
15
+ GraphQLOutputType,
16
+ GraphQLScalarType,
17
+ GraphQLUnionType,
18
+ ParseOptions,
19
+ buildSchema,
20
+ } from "graphql";
21
+ import request, { GraphQLClient, gql } from "graphql-request";
22
+ import {
23
+ InferDocumentLocalState,
24
+ InferDocumentOperation,
25
+ InferDocumentState,
26
+ } from "../read-mode/types";
27
+ import { logger } from "./logger";
3
28
 
4
- export { gql } from 'graphql-request';
29
+ export { gql } from "graphql-request";
5
30
 
6
- export type DriveInfo = {
7
- id: string;
8
- name: string;
9
- slug: string;
10
- icon?: string;
31
+ type ReqGraphQLError = {
32
+ message: string;
33
+ };
34
+
35
+ export type GraphQLResult<T> = { [K in keyof T]: T[K] | null } & {
36
+ errors?: GraphQLError[];
11
37
  };
12
38
 
13
39
  // replaces fetch so it can be used in Node and Browser envs
14
- export async function requestGraphql<T>(...args: Parameters<typeof request>) {
15
- const [url, ...requestArgs] = args;
16
- const client = new GraphQLClient(url, { fetch });
17
- return client.request<T>(...requestArgs);
40
+ export async function requestGraphql<T>(
41
+ ...args: Parameters<typeof request>
42
+ ): Promise<GraphQLResult<T>> {
43
+ const [url, ...requestArgs] = args;
44
+ const client = new GraphQLClient(url, { fetch });
45
+ const { errors, ...response } = await client.request<
46
+ { [K in keyof T]: T[K] | null } & { errors?: ReqGraphQLError[] }
47
+ >(...requestArgs);
48
+
49
+ const result = { ...response } as GraphQLResult<T>;
50
+ if (errors?.length) {
51
+ result.errors = errors.map(
52
+ ({ message, ...options }) => new GraphQLError(message, options),
53
+ );
54
+ }
55
+ return result;
56
+ }
57
+
58
+ export type DriveInfo = {
59
+ id: string;
60
+ name: string;
61
+ slug: string;
62
+ icon?: string;
63
+ };
64
+
65
+ function getFields(type: GraphQLOutputType): string {
66
+ if (type instanceof GraphQLObjectType) {
67
+ return Object.entries(type.getFields())
68
+ .map(([fieldName, field]) => {
69
+ const fieldType =
70
+ field.type instanceof GraphQLNonNull ? field.type.ofType : field.type;
71
+
72
+ if (
73
+ fieldType instanceof GraphQLObjectType ||
74
+ fieldType instanceof GraphQLUnionType
75
+ ) {
76
+ return `${fieldName} { ${getFields(fieldType)} }`;
77
+ }
78
+
79
+ if (fieldType instanceof GraphQLList) {
80
+ const listItemType =
81
+ fieldType.ofType instanceof GraphQLNonNull
82
+ ? fieldType.ofType.ofType
83
+ : fieldType.ofType;
84
+
85
+ if (listItemType instanceof GraphQLScalarType) {
86
+ return fieldName;
87
+ } else if (
88
+ listItemType instanceof GraphQLObjectType ||
89
+ listItemType instanceof GraphQLUnionType
90
+ ) {
91
+ return `${fieldName} { ${getFields(listItemType)} }`;
92
+ } else {
93
+ throw new Error(
94
+ `List item type ${listItemType.toString()} is not handled`,
95
+ );
96
+ }
97
+ }
98
+
99
+ return fieldName;
100
+ })
101
+ .join(" ");
102
+ } else if (type instanceof GraphQLUnionType) {
103
+ return type
104
+ .getTypes()
105
+ .map((unionType) => {
106
+ return `... on ${unionType.name} { ${getFields(unionType)} }`;
107
+ })
108
+ .join(" ");
109
+ }
110
+ return "";
111
+ }
112
+
113
+ export function generateDocumentStateQueryFields(
114
+ documentModel: DocumentModelState,
115
+ options?: BuildSchemaOptions & ParseOptions,
116
+ ): string {
117
+ const name = pascalCase(documentModel.name);
118
+ const spec = documentModel.specifications.at(-1);
119
+ if (!spec) {
120
+ throw new Error("No document model specification found");
121
+ }
122
+ const source = `${spec.state.global.schema} type Query { ${name}: ${name}State }`;
123
+ const schema = buildSchema(source, options);
124
+ const queryType = schema.getQueryType();
125
+ if (!queryType) {
126
+ throw new Error("No query type found");
127
+ }
128
+ const fields = queryType.getFields();
129
+ const stateQuery = fields[name];
130
+
131
+ if (!stateQuery) {
132
+ throw new Error("No state query found");
133
+ }
134
+
135
+ const queryFields = getFields(stateQuery.type);
136
+ return queryFields;
18
137
  }
19
138
 
20
139
  export async function requestPublicDrive(url: string): Promise<DriveInfo> {
21
- let drive: DriveInfo;
22
- try {
23
- const result = await requestGraphql<{ drive: DriveInfo }>(
24
- url,
25
- gql`
26
- query getDrive {
27
- drive {
140
+ let drive: DriveInfo;
141
+ try {
142
+ const result = await requestGraphql<{ drive: DriveInfo }>(
143
+ url,
144
+ gql`
145
+ query getDrive {
146
+ drive {
147
+ id
148
+ name
149
+ icon
150
+ slug
151
+ }
152
+ }
153
+ `,
154
+ );
155
+ if (result.errors?.length || !result.drive) {
156
+ throw result.errors?.at(0) ?? new Error("Drive not found");
157
+ }
158
+ drive = result.drive;
159
+ } catch (e) {
160
+ logger.error(e);
161
+ throw new Error("Couldn't find drive info");
162
+ }
163
+
164
+ return drive;
165
+ }
166
+
167
+ export type DriveState = DriveInfo &
168
+ Pick<DocumentDriveLocalState, "availableOffline" | "sharingType"> & {
169
+ nodes: Array<FolderNode | Omit<FileNode, "synchronizationUnits">>;
170
+ };
171
+
172
+ export type DocumentGraphQLResult<D extends Document> = Pick<
173
+ D,
174
+ "name" | "created" | "documentType" | "lastModified"
175
+ > & {
176
+ id: string;
177
+ revision: number;
178
+ state: InferDocumentState<D>;
179
+ initialState: InferDocumentState<D>;
180
+ operations: (Pick<
181
+ Operation,
182
+ | "id"
183
+ | "hash"
184
+ | "index"
185
+ | "skip"
186
+ | "timestamp"
187
+ | "type"
188
+ | "error"
189
+ | "context"
190
+ > & { inputText: string })[];
191
+ };
192
+
193
+ export async function fetchDocument<D extends Document>(
194
+ url: string,
195
+ documentId: string,
196
+ documentModelLib: DocumentModel<
197
+ InferDocumentState<D>,
198
+ InferDocumentOperation<D>,
199
+ InferDocumentLocalState<D>
200
+ >,
201
+ ): Promise<
202
+ GraphQLResult<{
203
+ document: Document<
204
+ InferDocumentState<D>,
205
+ InferDocumentOperation<D>,
206
+ InferDocumentLocalState<D>
207
+ >;
208
+ }>
209
+ > {
210
+ const { documentModel, utils } = documentModelLib;
211
+ const stateFields = generateDocumentStateQueryFields(documentModel);
212
+ const name = pascalCase(documentModel.name);
213
+ const result = await requestGraphql<{
214
+ document: DocumentGraphQLResult<D>;
215
+ }>(
216
+ url,
217
+ gql`
218
+ query ($id: String!) {
219
+ document(id: $id) {
220
+ id
221
+ name
222
+ created
223
+ documentType
224
+ lastModified
225
+ revision
226
+ operations {
28
227
  id
29
- name
30
- icon
31
- slug
228
+ error
229
+ hash
230
+ index
231
+ skip
232
+ timestamp
233
+ type
234
+ inputText
235
+ context {
236
+ signer {
237
+ user {
238
+ address
239
+ networkId
240
+ chainId
241
+ }
242
+ app {
243
+ name
244
+ key
245
+ }
246
+ signatures
247
+ }
248
+ }
249
+ }
250
+ ... on ${name} {
251
+ state {
252
+ ${stateFields}
253
+ }
254
+ initialState {
255
+ ${stateFields}
256
+ }
32
257
  }
33
258
  }
34
- `
35
- );
36
- drive = result.drive;
37
- } catch (e) {
38
- logger.error(e);
39
- throw new Error("Couldn't find drive info");
40
- }
41
-
42
- if (!drive) {
43
- throw new Error('Drive not found');
44
- }
259
+ }
260
+ `,
261
+ { id: documentId },
262
+ );
263
+ const document: Document<
264
+ InferDocumentState<D>,
265
+ InferDocumentOperation<D>,
266
+ InferDocumentLocalState<D>
267
+ > | null = result.document
268
+ ? {
269
+ ...result.document,
270
+ revision: {
271
+ global: result.document.revision,
272
+ local: 0,
273
+ },
274
+ state: utils.createState({ global: result.document.state }),
275
+ operations: {
276
+ global: result.document.operations.map(({ inputText, ...o }) => ({
277
+ ...o,
278
+ error: o.error ?? undefined,
279
+ scope: "global",
280
+ input: JSON.parse(inputText) as D,
281
+ })),
282
+ local: [],
283
+ },
284
+ attachments: {},
285
+ initialState: utils.createExtendedState({
286
+ // TODO: getDocument should return all the initial state fields
287
+ created: result.document.created,
288
+ lastModified: result.document.created,
289
+ state: utils.createState({
290
+ global: result.document.initialState,
291
+ }),
292
+ }),
293
+ clipboard: [],
294
+ }
295
+ : null;
45
296
 
46
- return drive;
297
+ return {
298
+ ...result,
299
+ document,
300
+ };
47
301
  }
@@ -1,89 +1,91 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
1
  import {
3
- DocumentDriveDocument,
4
- documentModel as DocumentDriveModel,
5
- z
6
- } from 'document-model-libs/document-drive';
2
+ DocumentDriveDocument,
3
+ documentModel as DocumentDriveModel,
4
+ } from "document-model-libs/document-drive";
7
5
  import {
8
- Action,
9
- BaseAction,
10
- Document,
11
- DocumentOperations,
12
- Operation,
13
- OperationScope
14
- } from 'document-model/document';
15
- import { OperationError } from '../server/error';
16
- import { DocumentDriveStorage, DocumentStorage } from '../storage';
6
+ Action,
7
+ BaseAction,
8
+ Document,
9
+ DocumentOperations,
10
+ Operation,
11
+ OperationScope,
12
+ } from "document-model/document";
13
+ // import setAsap from 'setasap';
14
+ import { v4 as uuidv4 } from "uuid";
15
+ import { OperationError } from "../server/error";
16
+ import { DocumentDriveStorage, DocumentStorage } from "../storage";
17
+ import { RunAsap } from "./run-asap";
18
+ export * from "./run-asap";
19
+
20
+ export const runAsap = RunAsap.runAsap;
21
+ export const runAsapAsync = RunAsap.runAsapAsync;
17
22
 
18
23
  export function isDocumentDriveStorage(
19
- document: DocumentStorage
24
+ document: DocumentStorage,
20
25
  ): document is DocumentDriveStorage {
21
- return (
22
- document.documentType === DocumentDriveModel.id
23
- );
26
+ return document.documentType === DocumentDriveModel.id;
24
27
  }
25
28
 
26
29
  export function isDocumentDrive(
27
- document: Document
30
+ document: Document,
28
31
  ): document is DocumentDriveDocument {
29
- return (
30
- document.documentType === DocumentDriveModel.id
31
- );
32
+ return document.documentType === DocumentDriveModel.id;
32
33
  }
33
34
 
34
35
  export function mergeOperations<A extends Action = Action>(
35
- currentOperations: DocumentOperations<A>,
36
- newOperations: Operation<A | BaseAction>[]
36
+ currentOperations: DocumentOperations<A>,
37
+ newOperations: Operation<A | BaseAction>[],
37
38
  ): DocumentOperations<A> {
38
- const minIndexByScope = Object.keys(currentOperations).reduce<Partial<Record<OperationScope, number>>>((acc, curr) => {
39
- const scope = curr as OperationScope;
40
- acc[scope] = currentOperations[scope].at(-1)?.index ?? 0
41
- return acc;
42
- }, {});
39
+ const minIndexByScope = Object.keys(currentOperations).reduce<
40
+ Partial<Record<OperationScope, number>>
41
+ >((acc, curr) => {
42
+ const scope = curr as OperationScope;
43
+ acc[scope] = currentOperations[scope].at(-1)?.index ?? 0;
44
+ return acc;
45
+ }, {});
43
46
 
44
- const conflictOp = newOperations.find(op => op.index < (minIndexByScope[op.scope] ?? 0));
45
- if (conflictOp) {
46
- throw new OperationError(
47
- "ERROR",
48
- conflictOp,
49
- `Tried to add operation with index ${conflictOp.index} and document is at index ${minIndexByScope[conflictOp.scope]}`
50
- );
51
- }
47
+ const conflictOp = newOperations.find(
48
+ (op) => op.index < (minIndexByScope[op.scope] ?? 0),
49
+ );
50
+ if (conflictOp) {
51
+ throw new OperationError(
52
+ "ERROR",
53
+ conflictOp,
54
+ `Tried to add operation with index ${conflictOp.index} and document is at index ${minIndexByScope[conflictOp.scope]}`,
55
+ );
56
+ }
52
57
 
53
- return newOperations.sort((a, b) => a.index - b.index
54
- ).reduce<DocumentOperations<A>>((acc, curr) => {
55
- const existingOperations = acc[curr.scope] || [];
56
- return { ...acc, [curr.scope]: [...existingOperations, curr] };
58
+ return newOperations
59
+ .sort((a, b) => a.index - b.index)
60
+ .reduce<DocumentOperations<A>>((acc, curr) => {
61
+ const existingOperations = acc[curr.scope] || [];
62
+ return { ...acc, [curr.scope]: [...existingOperations, curr] };
57
63
  }, currentOperations);
58
64
  }
59
65
 
60
66
  export function generateUUID(): string {
61
- return uuidv4();
67
+ return uuidv4();
62
68
  }
63
69
 
64
70
  export function isNoopUpdate(
65
- operation: Operation,
66
- latestOperation?: Operation
71
+ operation: Operation,
72
+ latestOperation?: Operation,
67
73
  ) {
68
- if (!latestOperation) {
69
- return false;
70
- }
74
+ if (!latestOperation) {
75
+ return false;
76
+ }
71
77
 
72
- const isNoopOp = operation.type === 'NOOP';
73
- const isNoopLatestOp = latestOperation.type === 'NOOP';
74
- const isSameIndexOp = operation.index === latestOperation.index;
75
- const isSkipOpGreaterThanLatestOp = operation.skip > latestOperation.skip;
78
+ const isNoopOp = operation.type === "NOOP";
79
+ const isNoopLatestOp = latestOperation.type === "NOOP";
80
+ const isSameIndexOp = operation.index === latestOperation.index;
81
+ const isSkipOpGreaterThanLatestOp = operation.skip > latestOperation.skip;
76
82
 
77
- return (
78
- isNoopOp &&
79
- isNoopLatestOp &&
80
- isSameIndexOp &&
81
- isSkipOpGreaterThanLatestOp
82
- );
83
+ return (
84
+ isNoopOp && isNoopLatestOp && isSameIndexOp && isSkipOpGreaterThanLatestOp
85
+ );
83
86
  }
84
87
 
85
88
  // return true if dateA is before dateB
86
89
  export function isBefore(dateA: Date | string, dateB: Date | string) {
87
- return new Date(dateA) < new Date(dateB);
90
+ return new Date(dateA) < new Date(dateB);
88
91
  }
89
-
@@ -1,41 +1,43 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- export type ILogger = Pick<Console, 'log' | 'info' | 'warn' | 'error' | 'debug' | 'trace'>;
1
+ export type ILogger = Pick<
2
+ Console,
3
+ "log" | "info" | "warn" | "error" | "debug" | "trace"
4
+ >;
3
5
  class Logger implements ILogger {
4
- #logger: ILogger = console;
5
-
6
- set logger(logger: ILogger) {
7
- this.#logger = logger;
8
- }
9
-
10
- log(...data: any[]): void {
11
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
12
- return this.#logger.log(...data);
13
- }
14
-
15
- info(...data: any[]): void {
16
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
17
- return this.#logger.info(...data);
18
- }
19
-
20
- warn(...data: any[]): void {
21
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
22
- return this.#logger.warn(...data);
23
- }
24
-
25
- error(...data: any[]): void {
26
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
27
- return this.#logger.error(...data);
28
- }
29
-
30
- debug(...data: any[]): void {
31
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
32
- return this.#logger.debug(...data);
33
- }
34
-
35
- trace(...data: any[]): void {
36
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
37
- return this.#logger.trace(...data);
38
- }
6
+ #logger: ILogger = console;
7
+
8
+ set logger(logger: ILogger) {
9
+ this.#logger = logger;
10
+ }
11
+
12
+ log(...data: any[]): void {
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
14
+ return this.#logger.log(...data);
15
+ }
16
+
17
+ info(...data: any[]): void {
18
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
19
+ return this.#logger.info(...data);
20
+ }
21
+
22
+ warn(...data: any[]): void {
23
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
24
+ return this.#logger.warn(...data);
25
+ }
26
+
27
+ error(...data: any[]): void {
28
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
29
+ return this.#logger.error(...data);
30
+ }
31
+
32
+ debug(...data: any[]): void {
33
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
34
+ return this.#logger.debug(...data);
35
+ }
36
+
37
+ trace(...data: any[]): void {
38
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
39
+ return this.#logger.trace(...data);
40
+ }
39
41
  }
40
42
 
41
43
  const loggerInstance = new Logger();
@@ -0,0 +1,58 @@
1
+ import {
2
+ Action,
3
+ Document,
4
+ DocumentOperations,
5
+ Operation,
6
+ OperationScope,
7
+ } from "document-model/document";
8
+ import { DocumentStorage } from "../storage/types";
9
+
10
+ export function migrateDocumentOperationSigatures<D extends Document>(
11
+ document: DocumentStorage<D>,
12
+ ): DocumentStorage<D> | undefined {
13
+ let legacy = false;
14
+ const operations = Object.entries(document.operations).reduce<
15
+ DocumentOperations<Action>
16
+ >(
17
+ (acc, [key, operations]) => {
18
+ const scope = key as unknown as OperationScope;
19
+ for (const op of operations) {
20
+ const newOp = migrateLegacyOperationSignature(op);
21
+ acc[scope].push(newOp);
22
+ if (newOp !== op) {
23
+ legacy = true;
24
+ }
25
+ }
26
+ return acc;
27
+ },
28
+ { global: [], local: [] },
29
+ );
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
31
+ return legacy ? { ...document, operations } : document;
32
+ }
33
+
34
+ export function migrateLegacyOperationSignature<A extends Action>(
35
+ operation: Operation<A>,
36
+ ): Operation<A> {
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
38
+ if (!operation.context?.signer || operation.context.signer.signatures) {
39
+ return operation;
40
+ }
41
+ const { signer } = operation.context;
42
+ if ("signature" in signer) {
43
+ const signature = signer.signature as string | undefined;
44
+ return {
45
+ ...operation,
46
+ context: {
47
+ ...operation.context,
48
+ signer: {
49
+ user: signer.user,
50
+ app: signer.app,
51
+ signatures: signature?.length ? [signature] : [],
52
+ },
53
+ },
54
+ };
55
+ } else {
56
+ return operation;
57
+ }
58
+ }