document-drive 1.8.5 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.8.5",
3
+ "version": "1.10.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -33,9 +33,9 @@
33
33
  "nanoevents": "^9.0.0",
34
34
  "sanitize-filename": "^1.6.3",
35
35
  "uuid": "^9.0.1",
36
- "document-model": "2.10.0",
37
- "document-model-libs": "1.120.4",
38
- "@powerhousedao/scalars": "1.12.0"
36
+ "@powerhousedao/scalars": "1.13.0",
37
+ "document-model": "2.11.0",
38
+ "document-model-libs": "1.121.1"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "@prisma/client": "^5.18.0",
@@ -44,7 +44,7 @@
44
44
  "redis": "^4.6.15",
45
45
  "sequelize": "^6.37.3",
46
46
  "sqlite3": "^5.1.7",
47
- "@powerhousedao/scalars": "1.12.0"
47
+ "@powerhousedao/scalars": "1.13.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@prisma/client": "5.17.0",
@@ -66,8 +66,8 @@
66
66
  "nanoevents": "^9.0.0",
67
67
  "sanitize-filename": "^1.6.3",
68
68
  "uuid": "^9.0.1",
69
- "document-model": "2.10.0",
70
- "document-model-libs": "1.120.4"
69
+ "document-model": "2.11.0",
70
+ "document-model-libs": "1.121.1"
71
71
  },
72
72
  "scripts": {
73
73
  "check-types": "tsc --build",
@@ -79,6 +79,6 @@
79
79
  "test:watch": "vitest watch",
80
80
  "clean": "rimraf dist",
81
81
  "clean:node_modules": "rimraf node_modules",
82
- "build": "prisma generate"
82
+ "postinstall": "prisma generate"
83
83
  }
84
84
  }
package/src/index.ts CHANGED
@@ -2,5 +2,3 @@ export * from "./server";
2
2
  export * from "./server/error";
3
3
  export * from "./storage";
4
4
  export * from "./utils";
5
-
6
- export const test = "test";
@@ -100,7 +100,7 @@ import {
100
100
  type SignalResult,
101
101
  type SynchronizationUnit,
102
102
  } from "./types";
103
- import { filterOperationsByRevision } from "./utils";
103
+ import { filterOperationsByRevision, isAtRevision } from "./utils";
104
104
 
105
105
  export * from "./listener";
106
106
  export type * from "./types";
@@ -1000,21 +1000,27 @@ export class BaseDocumentDriveServer
1000
1000
  }
1001
1001
 
1002
1002
  async getDrive(drive: string, options?: GetDocumentOptions) {
1003
+ let document: DocumentDriveDocument | undefined;
1003
1004
  try {
1004
- const document = await this.cache.getDocument("drives", drive); // TODO support GetDocumentOptions
1005
- if (document && isDocumentDrive(document)) {
1006
- return document;
1005
+ const cachedDocument = await this.cache.getDocument("drives", drive); // TODO support GetDocumentOptions
1006
+ if (cachedDocument && isDocumentDrive(cachedDocument)) {
1007
+ document = cachedDocument;
1008
+ if (isAtRevision(document, options?.revisions)) {
1009
+ return document;
1010
+ }
1007
1011
  }
1008
1012
  } catch (e) {
1009
1013
  logger.error("Error getting drive from cache", e);
1010
1014
  }
1011
- const driveStorage = await this.storage.getDrive(drive);
1012
- const document = this._buildDocument(driveStorage, options);
1013
- if (!isDocumentDrive(document)) {
1015
+ const driveStorage = document ?? (await this.storage.getDrive(drive));
1016
+ const result = this._buildDocument(driveStorage, options);
1017
+ if (!isDocumentDrive(result)) {
1014
1018
  throw new Error(`Document with id ${drive} is not a Document Drive`);
1015
1019
  } else {
1016
- this.cache.setDocument("drives", drive, document).catch(logger.error);
1017
- return document;
1020
+ if (!options?.revisions) {
1021
+ this.cache.setDocument("drives", drive, result).catch(logger.error);
1022
+ }
1023
+ return result;
1018
1024
  }
1019
1025
  }
1020
1026
 
@@ -1039,18 +1045,22 @@ export class BaseDocumentDriveServer
1039
1045
  }
1040
1046
 
1041
1047
  async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
1048
+ let cachedDocument: Document | undefined;
1042
1049
  try {
1043
- const document = await this.cache.getDocument(drive, id); // TODO support GetDocumentOptions
1044
- if (document) {
1045
- return document;
1050
+ cachedDocument = await this.cache.getDocument(drive, id); // TODO support GetDocumentOptions
1051
+ if (cachedDocument && isAtRevision(cachedDocument, options?.revisions)) {
1052
+ return cachedDocument;
1046
1053
  }
1047
1054
  } catch (e) {
1048
1055
  logger.error("Error getting document from cache", e);
1049
1056
  }
1050
- const documentStorage = await this.storage.getDocument(drive, id);
1057
+ const documentStorage =
1058
+ cachedDocument ?? (await this.storage.getDocument(drive, id));
1051
1059
  const document = this._buildDocument(documentStorage, options);
1052
1060
 
1053
- this.cache.setDocument(drive, id, document).catch(logger.error);
1061
+ if (!options?.revisions) {
1062
+ this.cache.setDocument(drive, id, document).catch(logger.error);
1063
+ }
1054
1064
  return document;
1055
1065
  }
1056
1066
 
@@ -1293,7 +1303,11 @@ export class BaseDocumentDriveServer
1293
1303
  documentStorage: DocumentStorage<T>,
1294
1304
  options?: GetDocumentOptions,
1295
1305
  ): T {
1296
- if (documentStorage.state && (!options || options.checkHashes === false)) {
1306
+ if (
1307
+ documentStorage.state &&
1308
+ (!options || options.checkHashes === false) &&
1309
+ isAtRevision(documentStorage as unknown as Document, options?.revisions)
1310
+ ) {
1297
1311
  return documentStorage as T;
1298
1312
  }
1299
1313
 
@@ -102,6 +102,8 @@ export class ListenerManager extends BaseListenerManager {
102
102
  const driveTransmitters = this.transmitters[drive] || {};
103
103
  driveTransmitters[listener.listenerId] = transmitter;
104
104
  this.transmitters[drive] = driveTransmitters;
105
+
106
+ this.triggerUpdate(true, { type: "local" });
105
107
  return Promise.resolve(transmitter);
106
108
  }
107
109
 
@@ -1,30 +1,41 @@
1
- import { Document, OperationScope } from "document-model/document";
1
+ import { Document, Operation, OperationScope } from "document-model/document";
2
2
  import { logger } from "../../../utils/logger";
3
3
  import {
4
+ GetDocumentOptions,
4
5
  IBaseDocumentDriveServer,
5
6
  Listener,
6
7
  ListenerRevision,
7
- OperationUpdate,
8
8
  StrandUpdate,
9
9
  } from "../../types";
10
- import { buildRevisionsFilter } from "../../utils";
11
- import { ITransmitter } from "./types";
10
+ import { ITransmitter, StrandUpdateSource } from "./types";
11
+ import { InferDocumentOperation } from "../../../read-mode/types";
12
12
 
13
- export interface IReceiver {
14
- transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
15
- disconnect: () => Promise<void>;
13
+ export interface IReceiver<
14
+ T extends Document = Document,
15
+ S extends OperationScope = OperationScope,
16
+ > {
17
+ onStrands: (strands: InternalTransmitterUpdate<T, S>[]) => Promise<void>;
18
+ onDisconnect: () => Promise<void>;
16
19
  }
17
20
 
21
+ export type InternalOperationUpdate<
22
+ D extends Document = Document,
23
+ S extends OperationScope = OperationScope,
24
+ > = Omit<Operation<InferDocumentOperation<D>>, "scope"> & {
25
+ state: D["state"][S];
26
+ previousState: D["state"][S];
27
+ };
28
+
18
29
  export type InternalTransmitterUpdate<
19
- T extends Document = Document,
30
+ D extends Document = Document,
20
31
  S extends OperationScope = OperationScope,
21
32
  > = {
22
33
  driveId: string;
23
34
  documentId: string;
24
35
  scope: S;
25
36
  branch: string;
26
- operations: OperationUpdate[];
27
- state: T["state"][S];
37
+ operations: InternalOperationUpdate<D, S>[];
38
+ state: D["state"][S];
28
39
  };
29
40
 
30
41
  export interface IInternalTransmitter extends ITransmitter {
@@ -32,52 +43,80 @@ export interface IInternalTransmitter extends ITransmitter {
32
43
  }
33
44
 
34
45
  export class InternalTransmitter implements ITransmitter {
35
- private drive: IBaseDocumentDriveServer;
36
- private listener: Listener;
37
- private receiver: IReceiver | undefined;
46
+ protected drive: IBaseDocumentDriveServer;
47
+ protected listener: Listener;
48
+ protected receiver: IReceiver | undefined;
38
49
 
39
50
  constructor(listener: Listener, drive: IBaseDocumentDriveServer) {
40
51
  this.listener = listener;
41
52
  this.drive = drive;
42
53
  }
43
54
 
55
+ async #buildInternalOperationUpdate(strand: StrandUpdate) {
56
+ const operations: InternalOperationUpdate[] = [];
57
+ const stateByIndex = new Map<number, unknown>();
58
+ const getStateByIndex = async (index: number) => {
59
+ const state = stateByIndex.get(index);
60
+ if (state) {
61
+ return state;
62
+ }
63
+
64
+ const getDocumentOptions: GetDocumentOptions = {
65
+ revisions: {
66
+ [strand.scope]: index,
67
+ },
68
+ checkHashes: false,
69
+ };
70
+ const document = await (strand.documentId
71
+ ? this.drive.getDocument(
72
+ strand.driveId,
73
+ strand.documentId,
74
+ getDocumentOptions,
75
+ )
76
+ : this.drive.getDrive(strand.driveId, getDocumentOptions));
77
+
78
+ if (index < 0) {
79
+ stateByIndex.set(index, document.initialState.state[strand.scope]);
80
+ } else {
81
+ stateByIndex.set(index, document.state[strand.scope]);
82
+ }
83
+ return stateByIndex.get(index);
84
+ };
85
+ for (const operation of strand.operations) {
86
+ operations.push({
87
+ ...operation,
88
+ state: await getStateByIndex(operation.index),
89
+ previousState: await getStateByIndex(operation.index - 1),
90
+ });
91
+ }
92
+ return operations;
93
+ }
94
+
44
95
  async transmit(
45
- strands: InternalTransmitterUpdate[],
96
+ strands: StrandUpdate[],
97
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
98
+ _source: StrandUpdateSource,
46
99
  ): Promise<ListenerRevision[]> {
47
100
  if (!this.receiver) {
48
101
  return [];
49
102
  }
50
103
 
51
- const retrievedDocuments = new Map<string, Document>();
52
104
  const updates: InternalTransmitterUpdate[] = [];
53
105
  for (const strand of strands) {
54
- let document = retrievedDocuments.get(
55
- `${strand.driveId}:${strand.documentId}`,
56
- );
57
- if (!document) {
58
- const revisions = buildRevisionsFilter(
59
- strands,
60
- strand.driveId,
61
- strand.documentId,
62
- );
63
- document = await (strand.documentId
64
- ? this.drive.getDocument(strand.driveId, strand.documentId, {
65
- revisions,
66
- })
67
- : this.drive.getDrive(strand.driveId, { revisions }));
68
- retrievedDocuments.set(
69
- `${strand.driveId}:${strand.documentId}`,
70
- document,
71
- );
72
- }
73
- updates.push({ ...strand, state: document.state[strand.scope] });
106
+ const operations = await this.#buildInternalOperationUpdate(strand);
107
+ const state = operations.at(-1)?.state ?? {};
108
+ updates.push({
109
+ ...strand,
110
+ operations,
111
+ state,
112
+ });
74
113
  }
75
114
  try {
76
- await this.receiver.transmit(updates);
115
+ await this.receiver.onStrands(updates);
77
116
  return strands.map(({ operations, ...s }) => ({
78
117
  ...s,
79
118
  status: "SUCCESS",
80
- revision: operations[operations.length - 1]?.index ?? -1,
119
+ revision: operations.at(operations.length - 1)?.index ?? -1,
81
120
  }));
82
121
  } catch (error) {
83
122
  logger.error(error);
@@ -85,7 +124,7 @@ export class InternalTransmitter implements ITransmitter {
85
124
  return strands.map(({ operations, ...s }) => ({
86
125
  ...s,
87
126
  status: "ERROR",
88
- revision: (operations[0]?.index ?? 0) - 1,
127
+ revision: (operations.at(0)?.index ?? 0) - 1,
89
128
  }));
90
129
  }
91
130
  }
@@ -95,6 +134,10 @@ export class InternalTransmitter implements ITransmitter {
95
134
  }
96
135
 
97
136
  async disconnect(): Promise<void> {
98
- await this.receiver?.disconnect();
137
+ await this.receiver?.onDisconnect();
138
+ }
139
+
140
+ getListener(): Listener {
141
+ return this.listener;
99
142
  }
100
143
  }
@@ -15,6 +15,18 @@ export function buildRevisionsFilter(
15
15
  }, {});
16
16
  }
17
17
 
18
+ export function buildDocumentRevisionsFilter(
19
+ document: Document,
20
+ ): RevisionsFilter {
21
+ return Object.entries(document.operations).reduce<RevisionsFilter>(
22
+ (acc, [scope, operations]) => {
23
+ acc[scope as OperationScope] = operations.at(-1)?.index ?? -1;
24
+ return acc;
25
+ },
26
+ {} as RevisionsFilter,
27
+ );
28
+ }
29
+
18
30
  export function filterOperationsByRevision(
19
31
  operations: Document["operations"],
20
32
  revisions?: RevisionsFilter,
@@ -24,11 +36,47 @@ export function filterOperationsByRevision(
24
36
  }
25
37
  return (Object.keys(operations) as OperationScope[]).reduce<
26
38
  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);
39
+ >(
40
+ (acc, scope) => {
41
+ const revision = revisions[scope];
42
+ if (revision !== undefined) {
43
+ acc[scope] = operations[scope].filter((op) => op.index <= revision);
44
+ }
45
+ return acc;
46
+ },
47
+ { global: [], local: [] } as unknown as Document["operations"],
48
+ );
49
+ }
50
+
51
+ export function isAtRevision(
52
+ document: Document,
53
+ revisions?: RevisionsFilter,
54
+ ): boolean {
55
+ return (
56
+ !revisions ||
57
+ Object.entries(revisions).find(([scope, revision]) => {
58
+ const operation = document.operations[scope as OperationScope].at(-1);
59
+ if (revision === -1) {
60
+ return operation !== undefined;
61
+ }
62
+ return operation?.index !== revision;
63
+ }) === undefined
64
+ );
65
+ }
66
+
67
+ export function isAfterRevision(
68
+ document: Document,
69
+ revisions?: RevisionsFilter,
70
+ ): boolean {
71
+ return (
72
+ !revisions ||
73
+ Object.entries(revisions).every(([scope, revision]) => {
74
+ const operation = document.operations[scope as OperationScope].at(-1);
75
+
76
+ if (revision === -1) {
77
+ return operation !== undefined;
78
+ }
79
+ return operation && operation.index > revision;
80
+ })
81
+ );
34
82
  }