document-drive 1.19.1 → 1.20.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 (252) hide show
  1. package/README.md +4 -0
  2. package/dist/index.d.ts +28 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +18 -0
  5. package/dist/src/cache/memory.d.ts +10 -0
  6. package/dist/src/cache/memory.d.ts.map +1 -0
  7. package/dist/src/cache/memory.js +26 -0
  8. package/dist/src/cache/redis.d.ts +14 -0
  9. package/dist/src/cache/redis.d.ts.map +1 -0
  10. package/dist/src/cache/redis.js +40 -0
  11. package/dist/src/cache/types.d.ts +7 -0
  12. package/dist/src/cache/types.d.ts.map +1 -0
  13. package/dist/src/cache/types.js +1 -0
  14. package/dist/src/drive-document-model/constants.d.ts +2 -0
  15. package/dist/src/drive-document-model/constants.d.ts.map +1 -0
  16. package/dist/src/drive-document-model/constants.js +1 -0
  17. package/dist/src/drive-document-model/gen/actions.d.ts +7 -0
  18. package/dist/src/drive-document-model/gen/actions.d.ts.map +1 -0
  19. package/dist/src/drive-document-model/gen/actions.js +2 -0
  20. package/dist/src/drive-document-model/gen/constants.d.ts +7 -0
  21. package/dist/src/drive-document-model/gen/constants.d.ts.map +1 -0
  22. package/dist/src/drive-document-model/gen/constants.js +16 -0
  23. package/dist/src/drive-document-model/gen/creators.d.ts +3 -0
  24. package/dist/src/drive-document-model/gen/creators.d.ts.map +1 -0
  25. package/dist/src/drive-document-model/gen/creators.js +2 -0
  26. package/dist/src/drive-document-model/gen/document-model.d.ts +3 -0
  27. package/dist/src/drive-document-model/gen/document-model.d.ts.map +1 -0
  28. package/dist/src/drive-document-model/gen/document-model.js +210 -0
  29. package/dist/src/drive-document-model/gen/drive/actions.d.ts +12 -0
  30. package/dist/src/drive-document-model/gen/drive/actions.d.ts.map +1 -0
  31. package/dist/src/drive-document-model/gen/drive/actions.js +1 -0
  32. package/dist/src/drive-document-model/gen/drive/creators.d.ts +11 -0
  33. package/dist/src/drive-document-model/gen/drive/creators.d.ts.map +1 -0
  34. package/dist/src/drive-document-model/gen/drive/creators.js +10 -0
  35. package/dist/src/drive-document-model/gen/drive/error.d.ts +2 -0
  36. package/dist/src/drive-document-model/gen/drive/error.d.ts.map +1 -0
  37. package/dist/src/drive-document-model/gen/drive/error.js +1 -0
  38. package/dist/src/drive-document-model/gen/drive/object.d.ts +14 -0
  39. package/dist/src/drive-document-model/gen/drive/object.d.ts.map +1 -0
  40. package/dist/src/drive-document-model/gen/drive/object.js +28 -0
  41. package/dist/src/drive-document-model/gen/drive/operations.d.ts +14 -0
  42. package/dist/src/drive-document-model/gen/drive/operations.d.ts.map +1 -0
  43. package/dist/src/drive-document-model/gen/drive/operations.js +1 -0
  44. package/dist/src/drive-document-model/gen/node/actions.d.ts +11 -0
  45. package/dist/src/drive-document-model/gen/node/actions.d.ts.map +1 -0
  46. package/dist/src/drive-document-model/gen/node/actions.js +1 -0
  47. package/dist/src/drive-document-model/gen/node/creators.d.ts +10 -0
  48. package/dist/src/drive-document-model/gen/node/creators.d.ts.map +1 -0
  49. package/dist/src/drive-document-model/gen/node/creators.js +9 -0
  50. package/dist/src/drive-document-model/gen/node/error.d.ts +2 -0
  51. package/dist/src/drive-document-model/gen/node/error.d.ts.map +1 -0
  52. package/dist/src/drive-document-model/gen/node/error.js +1 -0
  53. package/dist/src/drive-document-model/gen/node/object.d.ts +13 -0
  54. package/dist/src/drive-document-model/gen/node/object.d.ts.map +1 -0
  55. package/dist/src/drive-document-model/gen/node/object.js +25 -0
  56. package/dist/src/drive-document-model/gen/node/operations.d.ts +13 -0
  57. package/dist/src/drive-document-model/gen/node/operations.d.ts.map +1 -0
  58. package/dist/src/drive-document-model/gen/node/operations.js +1 -0
  59. package/dist/src/drive-document-model/gen/object.d.ts +21 -0
  60. package/dist/src/drive-document-model/gen/object.d.ts.map +1 -0
  61. package/dist/src/drive-document-model/gen/object.js +28 -0
  62. package/dist/src/drive-document-model/gen/reducer.d.ts +4 -0
  63. package/dist/src/drive-document-model/gen/reducer.d.ts.map +1 -0
  64. package/dist/src/drive-document-model/gen/reducer.js +74 -0
  65. package/dist/src/drive-document-model/gen/schema/types.d.ts +176 -0
  66. package/dist/src/drive-document-model/gen/schema/types.d.ts.map +1 -0
  67. package/dist/src/drive-document-model/gen/schema/types.js +1 -0
  68. package/dist/src/drive-document-model/gen/schema/zod.d.ts +87 -0
  69. package/dist/src/drive-document-model/gen/schema/zod.d.ts.map +1 -0
  70. package/dist/src/drive-document-model/gen/schema/zod.js +203 -0
  71. package/dist/src/drive-document-model/gen/types.d.ts +9 -0
  72. package/dist/src/drive-document-model/gen/types.d.ts.map +1 -0
  73. package/dist/src/drive-document-model/gen/types.js +1 -0
  74. package/dist/src/drive-document-model/gen/utils.d.ts +10 -0
  75. package/dist/src/drive-document-model/gen/utils.d.ts.map +1 -0
  76. package/dist/src/drive-document-model/gen/utils.js +27 -0
  77. package/dist/src/drive-document-model/index.d.ts +2 -0
  78. package/dist/src/drive-document-model/index.d.ts.map +1 -0
  79. package/dist/src/drive-document-model/index.js +1 -0
  80. package/dist/src/drive-document-model/module.d.ts +3 -0
  81. package/dist/src/drive-document-model/module.d.ts.map +1 -0
  82. package/dist/src/drive-document-model/module.js +12 -0
  83. package/dist/src/drive-document-model/src/reducers/drive.d.ts +8 -0
  84. package/dist/src/drive-document-model/src/reducers/drive.d.ts.map +1 -0
  85. package/dist/src/drive-document-model/src/reducers/drive.js +37 -0
  86. package/dist/src/drive-document-model/src/reducers/node.d.ts +8 -0
  87. package/dist/src/drive-document-model/src/reducers/node.d.ts.map +1 -0
  88. package/dist/src/drive-document-model/src/reducers/node.js +185 -0
  89. package/dist/src/drive-document-model/src/utils.d.ts +34 -0
  90. package/dist/src/drive-document-model/src/utils.d.ts.map +1 -0
  91. package/dist/src/drive-document-model/src/utils.js +146 -0
  92. package/dist/src/queue/base.d.ts +43 -0
  93. package/dist/src/queue/base.d.ts.map +1 -0
  94. package/dist/src/queue/base.js +241 -0
  95. package/dist/src/queue/redis.d.ts +28 -0
  96. package/dist/src/queue/redis.d.ts.map +1 -0
  97. package/dist/src/queue/redis.js +110 -0
  98. package/dist/src/queue/types.d.ts +55 -0
  99. package/dist/src/queue/types.d.ts.map +1 -0
  100. package/dist/src/queue/types.js +6 -0
  101. package/dist/src/read-mode/errors.d.ts +12 -0
  102. package/dist/src/read-mode/errors.d.ts.map +1 -0
  103. package/dist/src/read-mode/errors.js +17 -0
  104. package/dist/src/read-mode/server.d.ts +4 -0
  105. package/dist/src/read-mode/server.d.ts.map +1 -0
  106. package/dist/src/read-mode/server.js +78 -0
  107. package/dist/src/read-mode/service.d.ts +18 -0
  108. package/dist/src/read-mode/service.d.ts.map +1 -0
  109. package/dist/src/read-mode/service.js +112 -0
  110. package/dist/src/read-mode/types.d.ts +35 -0
  111. package/dist/src/read-mode/types.d.ts.map +1 -0
  112. package/dist/src/read-mode/types.js +1 -0
  113. package/dist/src/server/base-server.d.ts +112 -0
  114. package/dist/src/server/base-server.d.ts.map +1 -0
  115. package/dist/src/server/base-server.js +1280 -0
  116. package/dist/src/server/builder.d.ts +30 -0
  117. package/dist/src/server/builder.d.ts.map +1 -0
  118. package/dist/src/server/builder.js +89 -0
  119. package/dist/src/server/constants.d.ts +2 -0
  120. package/dist/src/server/constants.d.ts.map +1 -0
  121. package/dist/src/server/constants.js +1 -0
  122. package/dist/src/server/error.d.ts +30 -0
  123. package/dist/src/server/error.d.ts.map +1 -0
  124. package/dist/src/server/error.js +47 -0
  125. package/dist/src/server/event-emitter.d.ts +8 -0
  126. package/dist/src/server/event-emitter.d.ts.map +1 -0
  127. package/dist/src/server/event-emitter.js +10 -0
  128. package/dist/src/server/listener/index.d.ts +2 -0
  129. package/dist/src/server/listener/index.d.ts.map +1 -0
  130. package/dist/src/server/listener/index.js +1 -0
  131. package/dist/src/server/listener/listener-manager.d.ts +27 -0
  132. package/dist/src/server/listener/listener-manager.d.ts.map +1 -0
  133. package/dist/src/server/listener/listener-manager.js +401 -0
  134. package/dist/src/server/listener/transmitter/factory.d.ts +8 -0
  135. package/dist/src/server/listener/transmitter/factory.d.ts.map +1 -0
  136. package/dist/src/server/listener/transmitter/factory.js +25 -0
  137. package/dist/src/server/listener/transmitter/internal.d.ts +34 -0
  138. package/dist/src/server/listener/transmitter/internal.d.ts.map +1 -0
  139. package/dist/src/server/listener/transmitter/internal.js +87 -0
  140. package/dist/src/server/listener/transmitter/pull-responder.d.ts +38 -0
  141. package/dist/src/server/listener/transmitter/pull-responder.d.ts.map +1 -0
  142. package/dist/src/server/listener/transmitter/pull-responder.js +256 -0
  143. package/dist/src/server/listener/transmitter/switchboard-push.d.ts +9 -0
  144. package/dist/src/server/listener/transmitter/switchboard-push.d.ts.map +1 -0
  145. package/dist/src/server/listener/transmitter/switchboard-push.js +77 -0
  146. package/dist/src/server/listener/transmitter/types.d.ts +20 -0
  147. package/dist/src/server/listener/transmitter/types.d.ts.map +1 -0
  148. package/dist/src/server/listener/transmitter/types.js +1 -0
  149. package/dist/src/server/listener/util.d.ts +2 -0
  150. package/dist/src/server/listener/util.d.ts.map +1 -0
  151. package/dist/src/server/listener/util.js +22 -0
  152. package/dist/src/server/sync-manager.d.ts +30 -0
  153. package/dist/src/server/sync-manager.d.ts.map +1 -0
  154. package/dist/src/server/sync-manager.js +287 -0
  155. package/dist/src/server/types.d.ts +308 -0
  156. package/dist/src/server/types.d.ts.map +1 -0
  157. package/dist/src/server/types.js +12 -0
  158. package/dist/src/server/utils.d.ts +8 -0
  159. package/dist/src/server/utils.d.ts.map +1 -0
  160. package/dist/src/server/utils.js +47 -0
  161. package/dist/src/storage/base.d.ts +36 -0
  162. package/dist/src/storage/base.d.ts.map +1 -0
  163. package/dist/src/storage/base.js +4 -0
  164. package/dist/src/storage/browser.d.ts +36 -0
  165. package/dist/src/storage/browser.d.ts.map +1 -0
  166. package/dist/src/storage/browser.js +155 -0
  167. package/dist/src/storage/filesystem.d.ts +33 -0
  168. package/dist/src/storage/filesystem.d.ts.map +1 -0
  169. package/dist/src/storage/filesystem.js +197 -0
  170. package/dist/src/storage/memory.d.ts +33 -0
  171. package/dist/src/storage/memory.d.ts.map +1 -0
  172. package/dist/src/storage/memory.js +139 -0
  173. package/dist/src/storage/prisma.d.ts +67 -0
  174. package/dist/src/storage/prisma.d.ts.map +1 -0
  175. package/dist/src/storage/prisma.js +445 -0
  176. package/dist/src/storage/sequelize.d.ts +32 -0
  177. package/dist/src/storage/sequelize.d.ts.map +1 -0
  178. package/dist/src/storage/sequelize.js +373 -0
  179. package/dist/src/storage/types.d.ts +43 -0
  180. package/dist/src/storage/types.d.ts.map +1 -0
  181. package/dist/src/storage/types.js +1 -0
  182. package/dist/src/utils/default-drives-manager.d.ts +29 -0
  183. package/dist/src/utils/default-drives-manager.d.ts.map +1 -0
  184. package/dist/src/utils/default-drives-manager.js +208 -0
  185. package/dist/src/utils/graphql.d.ts +34 -0
  186. package/dist/src/utils/graphql.d.ts.map +1 -0
  187. package/dist/src/utils/graphql.js +183 -0
  188. package/dist/src/utils/logger.d.ts +27 -0
  189. package/dist/src/utils/logger.d.ts.map +1 -0
  190. package/dist/src/utils/logger.js +105 -0
  191. package/dist/src/utils/migrations.d.ts +4 -0
  192. package/dist/src/utils/migrations.d.ts.map +1 -0
  193. package/dist/src/utils/migrations.js +41 -0
  194. package/dist/src/utils/misc.d.ts +11 -0
  195. package/dist/src/utils/misc.d.ts.map +1 -0
  196. package/dist/src/utils/misc.js +43 -0
  197. package/dist/src/utils/run-asap.d.ts +12 -0
  198. package/dist/src/utils/run-asap.d.ts.map +1 -0
  199. package/dist/src/utils/run-asap.js +131 -0
  200. package/dist/test/document-helpers/utils.d.ts +8 -0
  201. package/dist/test/document-helpers/utils.d.ts.map +1 -0
  202. package/dist/test/document-helpers/utils.js +21 -0
  203. package/dist/test/utils.d.ts +48 -0
  204. package/dist/test/utils.d.ts.map +1 -0
  205. package/dist/test/utils.js +132 -0
  206. package/dist/test/vitest-setup.d.ts +2 -0
  207. package/dist/test/vitest-setup.d.ts.map +1 -0
  208. package/dist/test/vitest-setup.js +4 -0
  209. package/dist/tsconfig.tsbuildinfo +1 -0
  210. package/dist/vitest.config.d.ts +3 -0
  211. package/dist/vitest.config.d.ts.map +1 -0
  212. package/dist/vitest.config.js +20 -0
  213. package/package.json +20 -38
  214. package/src/cache/index.ts +0 -2
  215. package/src/cache/memory.ts +0 -33
  216. package/src/cache/redis.ts +0 -56
  217. package/src/cache/types.ts +0 -9
  218. package/src/index.ts +0 -4
  219. package/src/queue/base.ts +0 -320
  220. package/src/queue/index.ts +0 -2
  221. package/src/queue/redis.ts +0 -144
  222. package/src/queue/types.ts +0 -79
  223. package/src/read-mode/errors.ts +0 -19
  224. package/src/read-mode/index.ts +0 -125
  225. package/src/read-mode/service.ts +0 -207
  226. package/src/read-mode/types.ts +0 -108
  227. package/src/server/error.ts +0 -70
  228. package/src/server/index.ts +0 -2444
  229. package/src/server/listener/index.ts +0 -2
  230. package/src/server/listener/manager.ts +0 -652
  231. package/src/server/listener/transmitter/index.ts +0 -4
  232. package/src/server/listener/transmitter/internal.ts +0 -143
  233. package/src/server/listener/transmitter/pull-responder.ts +0 -462
  234. package/src/server/listener/transmitter/switchboard-push.ts +0 -125
  235. package/src/server/listener/transmitter/types.ts +0 -27
  236. package/src/server/types.ts +0 -596
  237. package/src/server/utils.ts +0 -82
  238. package/src/storage/base.ts +0 -81
  239. package/src/storage/browser.ts +0 -238
  240. package/src/storage/filesystem.ts +0 -297
  241. package/src/storage/index.ts +0 -2
  242. package/src/storage/memory.ts +0 -211
  243. package/src/storage/prisma.ts +0 -653
  244. package/src/storage/sequelize.ts +0 -498
  245. package/src/storage/types.ts +0 -97
  246. package/src/utils/default-drives-manager.ts +0 -341
  247. package/src/utils/document-helpers.ts +0 -21
  248. package/src/utils/graphql.ts +0 -301
  249. package/src/utils/index.ts +0 -90
  250. package/src/utils/logger.ts +0 -38
  251. package/src/utils/migrations.ts +0 -58
  252. package/src/utils/run-asap.ts +0 -156
@@ -1,2444 +0,0 @@
1
- import {
2
- actions,
3
- AddListenerInput,
4
- DocumentDriveAction,
5
- DocumentDriveDocument,
6
- DocumentDriveState,
7
- FileNode,
8
- isFileNode,
9
- ListenerFilter,
10
- RemoveListenerInput,
11
- Trigger,
12
- utils,
13
- } from "document-model-libs/document-drive";
14
- import {
15
- Action,
16
- BaseAction,
17
- utils as baseUtils,
18
- Document,
19
- DocumentHeader,
20
- DocumentModel,
21
- utils as DocumentUtils,
22
- Operation,
23
- OperationScope,
24
- } from "document-model/document";
25
- import { ClientError } from "graphql-request";
26
- import { createNanoEvents, Unsubscribe } from "nanoevents";
27
- import { ICache } from "../cache";
28
- import InMemoryCache from "../cache/memory";
29
- import { BaseQueueManager } from "../queue/base";
30
- import {
31
- ActionJob,
32
- IQueueManager,
33
- isActionJob,
34
- isOperationJob,
35
- Job,
36
- OperationJob,
37
- } from "../queue/types";
38
- import { ReadModeServer } from "../read-mode";
39
- import { MemoryStorage } from "../storage/memory";
40
- import type {
41
- DocumentDriveStorage,
42
- DocumentStorage,
43
- IDriveStorage,
44
- } from "../storage/types";
45
- import {
46
- generateUUID,
47
- isBefore,
48
- isDocumentDrive,
49
- RunAsap,
50
- runAsapAsync,
51
- } from "../utils";
52
- import { DefaultDrivesManager } from "../utils/default-drives-manager";
53
- import {
54
- attachBranch,
55
- garbageCollect,
56
- groupOperationsByScope,
57
- merge,
58
- precedes,
59
- removeExistingOperations,
60
- reshuffleByTimestamp,
61
- sortOperations,
62
- } from "../utils/document-helpers";
63
- import { requestPublicDrive } from "../utils/graphql";
64
- import { logger } from "../utils/logger";
65
- import {
66
- ConflictOperationError,
67
- DriveAlreadyExistsError,
68
- OperationError,
69
- SynchronizationUnitNotFoundError,
70
- } from "./error";
71
- import { ListenerManager } from "./listener";
72
- import {
73
- CancelPullLoop,
74
- InternalTransmitter,
75
- IReceiver,
76
- ITransmitter,
77
- PullResponderTransmitter,
78
- StrandUpdateSource,
79
- SwitchboardPushTransmitter,
80
- } from "./listener/transmitter";
81
- import {
82
- AddOperationOptions,
83
- DefaultListenerManagerOptions,
84
- DocumentDriveServerOptions,
85
- DriveEvents,
86
- GetDocumentOptions,
87
- GetStrandsOptions,
88
- IBaseDocumentDriveServer,
89
- IListenerManager,
90
- IOperationResult,
91
- Listener,
92
- ListenerState,
93
- RemoteDriveAccessLevel,
94
- RemoteDriveOptions,
95
- StrandUpdate,
96
- SynchronizationUnitQuery,
97
- SyncStatus,
98
- SyncUnitStatusObject,
99
- type CreateDocumentInput,
100
- type DriveInput,
101
- type OperationUpdate,
102
- type SignalResult,
103
- type SynchronizationUnit,
104
- } from "./types";
105
- import { filterOperationsByRevision, isAtRevision } from "./utils";
106
-
107
- export * from "./listener";
108
- export type * from "./types";
109
-
110
- export * from "../read-mode";
111
-
112
- export const PULL_DRIVE_INTERVAL = 5000;
113
-
114
- interface ITransmitterFactory {
115
- instance(
116
- transmitterType: string,
117
- listener: Listener,
118
- driveServer: IBaseDocumentDriveServer,
119
- ): ITransmitter;
120
- }
121
-
122
- export class TransmitterFactory implements ITransmitterFactory {
123
- private readonly listenerManager: IListenerManager;
124
-
125
- constructor(listenerManager: IListenerManager) {
126
- this.listenerManager = listenerManager;
127
- }
128
-
129
- instance(
130
- transmitterType: string,
131
- listener: Listener,
132
- driveServer: IBaseDocumentDriveServer,
133
- ): ITransmitter {
134
- switch (transmitterType) {
135
- case "SwitchboardPush": {
136
- return new SwitchboardPushTransmitter(listener.callInfo!.data!);
137
- }
138
- case "Internal": {
139
- return new InternalTransmitter(listener, driveServer);
140
- }
141
- default: {
142
- return new PullResponderTransmitter(listener, this.listenerManager);
143
- }
144
- }
145
- }
146
- }
147
-
148
- export class BaseDocumentDriveServer implements IBaseDocumentDriveServer {
149
- private emitter = createNanoEvents<DriveEvents>();
150
- private cache: ICache;
151
- private documentModels: DocumentModel[];
152
- private storage: IDriveStorage;
153
- private transmitterFactory: ITransmitterFactory;
154
- private listenerManager: IListenerManager;
155
- private triggerMap = new Map<
156
- DocumentDriveState["id"],
157
- Map<Trigger["id"], CancelPullLoop>
158
- >();
159
- private syncStatus = new Map<string, SyncUnitStatusObject>();
160
-
161
- private queueManager: IQueueManager;
162
- private initializePromise: Promise<Error[] | null>;
163
-
164
- private defaultDrivesManager: DefaultDrivesManager;
165
-
166
- protected options: Required<DocumentDriveServerOptions>;
167
-
168
- constructor(
169
- documentModels: DocumentModel[],
170
- storage: IDriveStorage = new MemoryStorage(),
171
- cache: ICache = new InMemoryCache(),
172
- queueManager: IQueueManager = new BaseQueueManager(),
173
- options?: DocumentDriveServerOptions,
174
- ) {
175
- this.options = {
176
- ...options,
177
- defaultDrives: {
178
- ...options?.defaultDrives,
179
- },
180
- listenerManager: {
181
- ...DefaultListenerManagerOptions,
182
- ...options?.listenerManager,
183
- },
184
- taskQueueMethod:
185
- options?.taskQueueMethod === undefined
186
- ? RunAsap.runAsap
187
- : options.taskQueueMethod,
188
- };
189
-
190
- // todo: pull this into the constructor -- there is a circular dependency right now
191
- this.listenerManager = new ListenerManager(
192
- this,
193
- undefined,
194
- this.options.listenerManager,
195
- );
196
-
197
- // todo: pull this into the constructor, depends on listenerManager
198
- this.transmitterFactory = new TransmitterFactory(this.listenerManager);
199
-
200
- this.documentModels = documentModels;
201
- this.storage = storage;
202
- this.cache = cache;
203
- this.queueManager = queueManager;
204
- this.defaultDrivesManager = new DefaultDrivesManager(
205
- this,
206
- this.defaultDrivesManagerDelegate,
207
- options,
208
- );
209
-
210
- this.storage.setStorageDelegate?.({
211
- getCachedOperations: async (drive, id) => {
212
- try {
213
- const document = await this.cache.getDocument(drive, id);
214
- return document?.operations;
215
- } catch (error) {
216
- logger.error(error);
217
- return undefined;
218
- }
219
- },
220
- });
221
-
222
- this.initializePromise = this._initialize();
223
- }
224
-
225
- setDocumentModels(models: DocumentModel[]): void {
226
- this.documentModels = [...models];
227
- this.emit("documentModels", [...models]);
228
- }
229
-
230
- initializeDefaultRemoteDrives() {
231
- return this.defaultDrivesManager.initializeDefaultRemoteDrives();
232
- }
233
-
234
- getDefaultRemoteDrives() {
235
- return this.defaultDrivesManager.getDefaultRemoteDrives();
236
- }
237
-
238
- setDefaultDriveAccessLevel(url: string, level: RemoteDriveAccessLevel) {
239
- return this.defaultDrivesManager.setDefaultDriveAccessLevel(url, level);
240
- }
241
-
242
- setAllDefaultDrivesAccessLevel(level: RemoteDriveAccessLevel) {
243
- return this.defaultDrivesManager.setAllDefaultDrivesAccessLevel(level);
244
- }
245
-
246
- private getOperationSource(source: StrandUpdateSource) {
247
- return source.type === "local" ? "push" : "pull";
248
- }
249
-
250
- private getCombinedSyncUnitStatus(
251
- syncUnitStatus: SyncUnitStatusObject,
252
- ): SyncStatus {
253
- if (!syncUnitStatus.pull && !syncUnitStatus.push) return "INITIAL_SYNC";
254
- if (syncUnitStatus.pull === "INITIAL_SYNC") return "INITIAL_SYNC";
255
- if (syncUnitStatus.push === "INITIAL_SYNC")
256
- return syncUnitStatus.pull || "INITIAL_SYNC";
257
-
258
- const order: Array<SyncStatus> = [
259
- "ERROR",
260
- "MISSING",
261
- "CONFLICT",
262
- "SYNCING",
263
- "SUCCESS",
264
- ];
265
- const sortedStatus = Object.values(syncUnitStatus).sort(
266
- (a, b) => order.indexOf(a) - order.indexOf(b),
267
- );
268
-
269
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270
- return sortedStatus[0]!;
271
- }
272
-
273
- private initSyncStatus(
274
- syncUnitId: string,
275
- status: Partial<SyncUnitStatusObject>,
276
- ) {
277
- const defaultSyncUnitStatus: SyncUnitStatusObject = Object.entries(
278
- status,
279
- ).reduce((acc, [key, _status]) => {
280
- return {
281
- ...acc,
282
- [key]: _status !== "SYNCING" ? _status : "INITIAL_SYNC",
283
- };
284
- }, {});
285
-
286
- this.syncStatus.set(syncUnitId, defaultSyncUnitStatus);
287
- this.emit(
288
- "syncStatus",
289
- syncUnitId,
290
- this.getCombinedSyncUnitStatus(defaultSyncUnitStatus),
291
- undefined,
292
- defaultSyncUnitStatus,
293
- );
294
- }
295
-
296
- private async initializeDriveSyncStatus(
297
- driveId: string,
298
- drive: DocumentDriveDocument,
299
- ) {
300
- const syncUnits = await this.getSynchronizationUnitsIds(driveId);
301
- const syncStatus: SyncUnitStatusObject = {
302
- pull: drive.state.local.triggers.length > 0 ? "INITIAL_SYNC" : undefined,
303
- push: drive.state.local.listeners.length > 0 ? "SUCCESS" : undefined,
304
- };
305
-
306
- if (!syncStatus.pull && !syncStatus.push) return;
307
-
308
- const syncUnitsIds = [driveId, ...syncUnits.map((s) => s.syncId)];
309
-
310
- for (const syncUnitId of syncUnitsIds) {
311
- this.initSyncStatus(syncUnitId, syncStatus);
312
- }
313
- }
314
-
315
- private updateSyncUnitStatus(
316
- syncUnitId: string,
317
- status: Partial<SyncUnitStatusObject> | null,
318
- error?: Error,
319
- ) {
320
- if (status === null) {
321
- this.syncStatus.delete(syncUnitId);
322
- return;
323
- }
324
-
325
- const syncUnitStatus = this.syncStatus.get(syncUnitId);
326
-
327
- if (!syncUnitStatus) {
328
- this.initSyncStatus(syncUnitId, status);
329
- return;
330
- }
331
-
332
- const shouldUpdateStatus = Object.entries(status).some(
333
- ([key, _status]) =>
334
- syncUnitStatus[key as keyof SyncUnitStatusObject] !== _status,
335
- );
336
-
337
- if (shouldUpdateStatus) {
338
- const newstatus = Object.entries(status).reduce((acc, [key, _status]) => {
339
- return {
340
- ...acc,
341
- // do not replace initial_syncing if it has not finished yet
342
- [key]:
343
- acc[key as keyof SyncUnitStatusObject] === "INITIAL_SYNC" &&
344
- _status === "SYNCING"
345
- ? "INITIAL_SYNC"
346
- : _status,
347
- };
348
- }, syncUnitStatus);
349
-
350
- const previousCombinedStatus =
351
- this.getCombinedSyncUnitStatus(syncUnitStatus);
352
- const newCombinedStatus = this.getCombinedSyncUnitStatus(newstatus);
353
-
354
- this.syncStatus.set(syncUnitId, newstatus);
355
-
356
- if (previousCombinedStatus !== newCombinedStatus) {
357
- this.emit(
358
- "syncStatus",
359
- syncUnitId,
360
- this.getCombinedSyncUnitStatus(newstatus),
361
- error,
362
- newstatus,
363
- );
364
- }
365
- }
366
- }
367
-
368
- private async saveStrand(strand: StrandUpdate, source: StrandUpdateSource) {
369
- const operations: Operation[] = strand.operations.map((op) => ({
370
- ...op,
371
- scope: strand.scope,
372
- branch: strand.branch,
373
- }));
374
-
375
- const result = await (!strand.documentId
376
- ? this.queueDriveOperations(
377
- strand.driveId,
378
- operations as Operation<DocumentDriveAction | BaseAction>[],
379
- { source },
380
- )
381
- : this.queueOperations(strand.driveId, strand.documentId, operations, {
382
- source,
383
- }));
384
-
385
- if (result.status === "ERROR") {
386
- const syncUnits =
387
- strand.documentId !== ""
388
- ? (
389
- await this.getSynchronizationUnitsIds(
390
- strand.driveId,
391
- [strand.documentId],
392
- [strand.scope],
393
- [strand.branch],
394
- )
395
- ).map((s) => s.syncId)
396
- : [strand.driveId];
397
-
398
- const operationSource = this.getOperationSource(source);
399
-
400
- for (const syncUnit of syncUnits) {
401
- this.updateSyncUnitStatus(
402
- syncUnit,
403
- { [operationSource]: result.status },
404
- result.error,
405
- );
406
- }
407
- }
408
- this.emit("strandUpdate", strand);
409
- return result;
410
- }
411
-
412
- private handleListenerError(
413
- error: Error,
414
- driveId: string,
415
- listener: ListenerState,
416
- ) {
417
- logger.error(
418
- `Listener ${listener.listener.label ?? listener.listener.listenerId} error:`,
419
- error,
420
- );
421
-
422
- const status = error instanceof OperationError ? error.status : "ERROR";
423
-
424
- this.updateSyncUnitStatus(driveId, { push: status }, error);
425
- }
426
-
427
- private shouldSyncRemoteDrive(drive: DocumentDriveDocument) {
428
- return (
429
- drive.state.local.availableOffline &&
430
- drive.state.local.triggers.length > 0
431
- );
432
- }
433
-
434
- private async startSyncRemoteDrive(driveId: string) {
435
- const drive = await this.getDrive(driveId);
436
- let driveTriggers = this.triggerMap.get(driveId);
437
-
438
- const syncUnits = await this.getSynchronizationUnitsIds(
439
- driveId,
440
- undefined,
441
- undefined,
442
- undefined,
443
- undefined,
444
- drive,
445
- );
446
-
447
- for (const trigger of drive.state.local.triggers) {
448
- if (driveTriggers?.get(trigger.id)) {
449
- continue;
450
- }
451
-
452
- if (!driveTriggers) {
453
- driveTriggers = new Map();
454
- }
455
-
456
- this.updateSyncUnitStatus(driveId, { pull: "SYNCING" });
457
-
458
- for (const syncUnit of syncUnits) {
459
- this.updateSyncUnitStatus(syncUnit.syncId, { pull: "SYNCING" });
460
- }
461
-
462
- if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
463
- let firstPull = true;
464
- const cancelPullLoop = PullResponderTransmitter.setupPull(
465
- driveId,
466
- trigger,
467
- this.saveStrand.bind(this),
468
- (error) => {
469
- const statusError =
470
- error instanceof OperationError ? error.status : "ERROR";
471
-
472
- this.updateSyncUnitStatus(driveId, { pull: statusError }, error);
473
-
474
- if (error instanceof ClientError) {
475
- this.emit(
476
- "clientStrandsError",
477
- driveId,
478
- trigger,
479
- error.response.status,
480
- error.message,
481
- );
482
- }
483
- },
484
- (revisions) => {
485
- const errorRevision = revisions.filter(
486
- (r) => r.status !== "SUCCESS",
487
- );
488
-
489
- if (errorRevision.length < 1) {
490
- this.updateSyncUnitStatus(driveId, {
491
- pull: "SUCCESS",
492
- });
493
- }
494
-
495
- const documentIdsFromRevision = revisions
496
- .filter((rev) => rev.documentId !== "")
497
- .map((rev) => rev.documentId);
498
-
499
- this.getSynchronizationUnitsIds(driveId, documentIdsFromRevision)
500
- .then((revSyncUnits) => {
501
- for (const syncUnit of revSyncUnits) {
502
- const fileErrorRevision = errorRevision.find(
503
- (r) => r.documentId === syncUnit.documentId,
504
- );
505
-
506
- if (fileErrorRevision) {
507
- this.updateSyncUnitStatus(
508
- syncUnit.syncId,
509
- { pull: fileErrorRevision.status },
510
- fileErrorRevision.error,
511
- );
512
- } else {
513
- this.updateSyncUnitStatus(syncUnit.syncId, {
514
- pull: "SUCCESS",
515
- });
516
- }
517
- }
518
- })
519
- .catch(console.error);
520
-
521
- // if it is the first pull and returns empty
522
- // then updates corresponding push transmitter
523
- if (firstPull) {
524
- firstPull = false;
525
- const pushListener = drive.state.local.listeners.find(
526
- (listener) => trigger.data.url === listener.callInfo?.data,
527
- );
528
- if (pushListener) {
529
- this.getSynchronizationUnitsRevision(driveId, syncUnits)
530
- .then((syncUnitRevisions) => {
531
- for (const revision of syncUnitRevisions) {
532
- this.listenerManager
533
- .updateListenerRevision(
534
- pushListener.listenerId,
535
- driveId,
536
- revision.syncId,
537
- revision.revision,
538
- )
539
- .catch(logger.error);
540
- }
541
- })
542
- .catch(logger.error);
543
- }
544
- }
545
- },
546
- );
547
- driveTriggers.set(trigger.id, cancelPullLoop);
548
- this.triggerMap.set(driveId, driveTriggers);
549
- }
550
- }
551
- }
552
-
553
- private async stopSyncRemoteDrive(driveId: string) {
554
- const syncUnits = await this.getSynchronizationUnitsIds(driveId);
555
- const filesNodeSyncId = syncUnits
556
- .filter((syncUnit) => syncUnit.documentId !== "")
557
- .map((syncUnit) => syncUnit.syncId);
558
-
559
- const triggers = this.triggerMap.get(driveId);
560
- triggers?.forEach((cancel) => cancel());
561
- this.updateSyncUnitStatus(driveId, null);
562
-
563
- for (const fileNodeSyncId of filesNodeSyncId) {
564
- this.updateSyncUnitStatus(fileNodeSyncId, null);
565
- }
566
- return this.triggerMap.delete(driveId);
567
- }
568
-
569
- private defaultDrivesManagerDelegate = {
570
- detachDrive: this.detachDrive.bind(this),
571
- emit: (...args: Parameters<DriveEvents["defaultRemoteDrive"]>) =>
572
- this.emit("defaultRemoteDrive", ...args),
573
- };
574
-
575
- private queueDelegate = {
576
- checkDocumentExists: (
577
- driveId: string,
578
- documentId: string,
579
- ): Promise<boolean> =>
580
- this.storage.checkDocumentExists(driveId, documentId),
581
- processOperationJob: async ({
582
- driveId,
583
- documentId,
584
- operations,
585
- options,
586
- }: OperationJob) => {
587
- return documentId
588
- ? this.addOperations(driveId, documentId, operations, options)
589
- : this.addDriveOperations(
590
- driveId,
591
- operations as Operation<DocumentDriveAction | BaseAction>[],
592
- options,
593
- );
594
- },
595
- processActionJob: async ({
596
- driveId,
597
- documentId,
598
- actions,
599
- options,
600
- }: ActionJob) => {
601
- return documentId
602
- ? this.addActions(driveId, documentId, actions, options)
603
- : this.addDriveActions(
604
- driveId,
605
- actions as Operation<DocumentDriveAction | BaseAction>[],
606
- options,
607
- );
608
- },
609
- processJob: async (job: Job) => {
610
- if (isOperationJob(job)) {
611
- return this.queueDelegate.processOperationJob(job);
612
- } else if (isActionJob(job)) {
613
- return this.queueDelegate.processActionJob(job);
614
- } else {
615
- throw new Error("Unknown job type", job);
616
- }
617
- },
618
- };
619
-
620
- initialize() {
621
- return this.initializePromise;
622
- }
623
-
624
- private async _initialize() {
625
- await this.listenerManager.initialize(this.handleListenerError);
626
-
627
- await this.queueManager.init(this.queueDelegate, (error) => {
628
- logger.error(`Error initializing queue manager`, error);
629
- errors.push(error);
630
- });
631
-
632
- try {
633
- await this.defaultDrivesManager.removeOldremoteDrives();
634
- } catch (error) {
635
- logger.error(error);
636
- }
637
-
638
- const errors: Error[] = [];
639
- const drives = await this.getDrives();
640
- for (const drive of drives) {
641
- await this._initializeDrive(drive).catch((error) => {
642
- logger.error(`Error initializing drive ${drive}`, error);
643
- errors.push(error as Error);
644
- });
645
- }
646
-
647
- if (this.options.defaultDrives.loadOnInit !== false) {
648
- await this.defaultDrivesManager.initializeDefaultRemoteDrives();
649
- }
650
-
651
- return errors.length === 0 ? null : errors;
652
- }
653
-
654
- private async _initializeDrive(driveId: string) {
655
- const drive = await this.getDrive(driveId);
656
- await this.initializeDriveSyncStatus(driveId, drive);
657
-
658
- if (this.shouldSyncRemoteDrive(drive)) {
659
- await this.startSyncRemoteDrive(driveId);
660
- }
661
-
662
- for (const zodListener of drive.state.local.listeners) {
663
- const transmitter = this.transmitterFactory.instance(
664
- zodListener.callInfo?.transmitterType ?? "",
665
- {
666
- driveId,
667
- listenerId: zodListener.listenerId,
668
- block: zodListener.block,
669
- filter: zodListener.filter,
670
- system: zodListener.system,
671
- label: zodListener.label || undefined,
672
- callInfo: zodListener.callInfo || undefined,
673
- },
674
- this,
675
- );
676
-
677
- await this.listenerManager.setListener(driveId, {
678
- block: zodListener.block,
679
- driveId: drive.state.global.id,
680
- filter: {
681
- branch: zodListener.filter.branch ?? [],
682
- documentId: zodListener.filter.documentId ?? [],
683
- documentType: zodListener.filter.documentType,
684
- scope: zodListener.filter.scope ?? [],
685
- },
686
- listenerId: zodListener.listenerId,
687
- system: zodListener.system,
688
- label: zodListener.label ?? "",
689
- transmitter,
690
- });
691
- }
692
- }
693
-
694
- public async getSynchronizationUnits(
695
- driveId: string,
696
- documentId?: string[],
697
- scope?: string[],
698
- branch?: string[],
699
- documentType?: string[],
700
- loadedDrive?: DocumentDriveDocument,
701
- ) {
702
- const drive = loadedDrive || (await this.getDrive(driveId));
703
-
704
- const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
705
- driveId,
706
- documentId,
707
- scope,
708
- branch,
709
- documentType,
710
- drive,
711
- );
712
- return this.getSynchronizationUnitsRevision(
713
- driveId,
714
- synchronizationUnitsQuery,
715
- drive,
716
- );
717
- }
718
-
719
- public async getSynchronizationUnitsRevision(
720
- driveId: string,
721
- syncUnitsQuery: SynchronizationUnitQuery[],
722
- loadedDrive?: DocumentDriveDocument,
723
- ): Promise<SynchronizationUnit[]> {
724
- const drive = loadedDrive || (await this.getDrive(driveId));
725
-
726
- const revisions =
727
- await this.storage.getSynchronizationUnitsRevision(syncUnitsQuery);
728
-
729
- const synchronizationUnits: SynchronizationUnit[] = syncUnitsQuery.map(
730
- (s) => ({
731
- ...s,
732
- lastUpdated: drive.created,
733
- revision: -1,
734
- }),
735
- );
736
- for (const revision of revisions) {
737
- const syncUnit = synchronizationUnits.find(
738
- (s) =>
739
- revision.driveId === s.driveId &&
740
- revision.documentId === s.documentId &&
741
- revision.scope === s.scope &&
742
- revision.branch === s.branch,
743
- );
744
- if (syncUnit) {
745
- syncUnit.revision = revision.revision;
746
- syncUnit.lastUpdated = revision.lastUpdated;
747
- }
748
- }
749
- return synchronizationUnits;
750
- }
751
-
752
- public async getSynchronizationUnitsIds(
753
- driveId: string,
754
- documentId?: string[],
755
- scope?: string[],
756
- branch?: string[],
757
- documentType?: string[],
758
- loadedDrive?: DocumentDriveDocument,
759
- ): Promise<SynchronizationUnitQuery[]> {
760
- const drive = loadedDrive ?? (await this.getDrive(driveId));
761
- const nodes = drive.state.global.nodes.filter(
762
- (node) =>
763
- isFileNode(node) &&
764
- (!documentId?.length ||
765
- documentId.includes(node.id) ||
766
- documentId.includes("*")) &&
767
- (!documentType?.length ||
768
- documentType.includes(node.documentType) ||
769
- documentType.includes("*")),
770
- ) as Pick<FileNode, "id" | "documentType" | "synchronizationUnits">[];
771
-
772
- // checks if document drive synchronization unit should be added
773
- if (
774
- (!documentId || documentId.includes("*") || documentId.includes("")) &&
775
- (!documentType?.length ||
776
- documentType.includes("powerhouse/document-drive") ||
777
- documentType.includes("*"))
778
- ) {
779
- nodes.unshift({
780
- id: "",
781
- documentType: "powerhouse/document-drive",
782
- synchronizationUnits: [
783
- {
784
- syncId: "0",
785
- scope: "global",
786
- branch: "main",
787
- },
788
- ],
789
- });
790
- }
791
-
792
- const synchronizationUnitsQuery: Omit<
793
- SynchronizationUnit,
794
- "revision" | "lastUpdated"
795
- >[] = [];
796
- for (const node of nodes) {
797
- const nodeUnits =
798
- scope?.length || branch?.length
799
- ? node.synchronizationUnits.filter(
800
- (unit) =>
801
- (!scope?.length ||
802
- scope.includes(unit.scope) ||
803
- scope.includes("*")) &&
804
- (!branch?.length ||
805
- branch.includes(unit.branch) ||
806
- branch.includes("*")),
807
- )
808
- : node.synchronizationUnits;
809
- if (!nodeUnits.length) {
810
- continue;
811
- }
812
- synchronizationUnitsQuery.push(
813
- ...nodeUnits.map((n) => ({
814
- driveId,
815
- documentId: node.id,
816
- syncId: n.syncId,
817
- documentType: node.documentType,
818
- scope: n.scope,
819
- branch: n.branch,
820
- })),
821
- );
822
- }
823
- return synchronizationUnitsQuery;
824
- }
825
-
826
- public async getSynchronizationUnitIdInfo(
827
- driveId: string,
828
- syncId: string,
829
- loadedDrive?: DocumentDriveDocument,
830
- ): Promise<SynchronizationUnitQuery | undefined> {
831
- const drive = loadedDrive || (await this.getDrive(driveId));
832
- const node = drive.state.global.nodes.find(
833
- (node) =>
834
- isFileNode(node) &&
835
- node.synchronizationUnits.find((unit) => unit.syncId === syncId),
836
- );
837
-
838
- if (!node || !isFileNode(node)) {
839
- return undefined;
840
- }
841
-
842
- const syncUnit = node.synchronizationUnits.find(
843
- (unit) => unit.syncId === syncId,
844
- );
845
- if (!syncUnit) {
846
- return undefined;
847
- }
848
-
849
- return {
850
- syncId,
851
- scope: syncUnit.scope,
852
- branch: syncUnit.branch,
853
- driveId,
854
- documentId: node.id,
855
- documentType: node.documentType,
856
- };
857
- }
858
-
859
- public async getSynchronizationUnit(
860
- driveId: string,
861
- syncId: string,
862
- loadedDrive?: DocumentDriveDocument,
863
- ): Promise<SynchronizationUnit | undefined> {
864
- const syncUnit = await this.getSynchronizationUnitIdInfo(
865
- driveId,
866
- syncId,
867
- loadedDrive,
868
- );
869
-
870
- if (!syncUnit) {
871
- return undefined;
872
- }
873
-
874
- const { scope, branch, documentId, documentType } = syncUnit;
875
-
876
- // TODO: REPLACE WITH GET DOCUMENT OPERATIONS
877
- const document = await this.getDocument(driveId, documentId);
878
- const operations = document.operations[scope as OperationScope] ?? [];
879
- const lastOperation = operations[operations.length - 1];
880
-
881
- return {
882
- syncId,
883
- scope,
884
- branch,
885
- driveId,
886
- documentId,
887
- documentType,
888
- lastUpdated: lastOperation?.timestamp ?? document.lastModified,
889
- revision: lastOperation?.index ?? 0,
890
- };
891
- }
892
-
893
- async getOperationData(
894
- driveId: string,
895
- syncId: string,
896
- filter: GetStrandsOptions,
897
- loadedDrive?: DocumentDriveDocument,
898
- ): Promise<OperationUpdate[]> {
899
- const syncUnit =
900
- syncId === "0"
901
- ? { documentId: "", scope: "global" }
902
- : await this.getSynchronizationUnitIdInfo(driveId, syncId, loadedDrive);
903
-
904
- if (!syncUnit) {
905
- throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
906
- }
907
-
908
- const document =
909
- syncId === "0"
910
- ? loadedDrive || (await this.getDrive(driveId))
911
- : await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
912
-
913
- const operations =
914
- document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
915
-
916
- const filteredOperations = operations.filter(
917
- (operation) =>
918
- Object.keys(filter).length === 0 ||
919
- ((filter.since === undefined ||
920
- isBefore(filter.since, operation.timestamp)) &&
921
- (filter.fromRevision === undefined ||
922
- operation.index > filter.fromRevision)),
923
- );
924
-
925
- const limitedOperations = filter.limit
926
- ? filteredOperations.slice(0, filter.limit)
927
- : filteredOperations;
928
-
929
- return limitedOperations.map((operation) => ({
930
- hash: operation.hash,
931
- index: operation.index,
932
- timestamp: operation.timestamp,
933
- type: operation.type,
934
- input: operation.input as object,
935
- skip: operation.skip,
936
- context: operation.context,
937
- id: operation.id,
938
- }));
939
- }
940
-
941
- protected getDocumentModel(documentType: string) {
942
- const documentModel = this.documentModels.find(
943
- (model) => model.documentModel.id === documentType,
944
- );
945
- if (!documentModel) {
946
- throw new Error(`Document type ${documentType} not supported`);
947
- }
948
- return documentModel;
949
- }
950
-
951
- getDocumentModels() {
952
- return [...this.documentModels];
953
- }
954
-
955
- async addDrive(input: DriveInput): Promise<DocumentDriveDocument> {
956
- const id = input.global.id || generateUUID();
957
- if (!id) {
958
- throw new Error("Invalid Drive Id");
959
- }
960
-
961
- const drives = await this.storage.getDrives();
962
- if (drives.includes(id)) {
963
- throw new DriveAlreadyExistsError(id);
964
- }
965
-
966
- const document = utils.createDocument({
967
- state: input,
968
- });
969
-
970
- await this.storage.createDrive(id, document);
971
-
972
- if (input.global.slug) {
973
- await this.cache.deleteDocument("drives-slug", input.global.slug);
974
- }
975
-
976
- await this._initializeDrive(id);
977
-
978
- this.emit("driveAdded", document);
979
-
980
- return document;
981
- }
982
-
983
- async addRemoteDrive(
984
- url: string,
985
- options: RemoteDriveOptions,
986
- ): Promise<DocumentDriveDocument> {
987
- const { id, name, slug, icon } =
988
- options.expectedDriveInfo || (await requestPublicDrive(url));
989
-
990
- const {
991
- pullFilter,
992
- pullInterval,
993
- availableOffline,
994
- sharingType,
995
- listeners,
996
- triggers,
997
- } = options;
998
-
999
- const pullTrigger =
1000
- await PullResponderTransmitter.createPullResponderTrigger(id, url, {
1001
- pullFilter,
1002
- pullInterval,
1003
- });
1004
-
1005
- return await this.addDrive({
1006
- global: {
1007
- id: id,
1008
- name,
1009
- slug,
1010
- icon: icon ?? null,
1011
- },
1012
- local: {
1013
- triggers: [...triggers, pullTrigger],
1014
- listeners: listeners,
1015
- availableOffline,
1016
- sharingType,
1017
- },
1018
- });
1019
- }
1020
-
1021
- public async registerPullResponderTrigger(
1022
- driveId: string,
1023
- url: string,
1024
- options: Pick<RemoteDriveOptions, "pullFilter" | "pullInterval">,
1025
- ) {
1026
- const pullTrigger =
1027
- await PullResponderTransmitter.createPullResponderTrigger(
1028
- driveId,
1029
- url,
1030
- options,
1031
- );
1032
-
1033
- return pullTrigger;
1034
- }
1035
-
1036
- async deleteDrive(driveId: string) {
1037
- const result = await Promise.allSettled([
1038
- this.stopSyncRemoteDrive(driveId),
1039
- this.listenerManager.removeDrive(driveId),
1040
- this.cache.deleteDocument("drives", driveId),
1041
- this.storage.deleteDrive(driveId),
1042
- ]);
1043
-
1044
- result.forEach((r) => {
1045
- if (r.status === "rejected") {
1046
- throw r.reason;
1047
- }
1048
- });
1049
- }
1050
-
1051
- getDrives() {
1052
- return this.storage.getDrives();
1053
- }
1054
-
1055
- async getDrive(driveId: string, options?: GetDocumentOptions) {
1056
- let document: DocumentDriveDocument | undefined;
1057
- try {
1058
- const cachedDocument = await this.cache.getDocument("drives", driveId); // TODO support GetDocumentOptions
1059
- if (cachedDocument && isDocumentDrive(cachedDocument)) {
1060
- document = cachedDocument;
1061
- if (isAtRevision(document, options?.revisions)) {
1062
- return document;
1063
- }
1064
- }
1065
- } catch (e) {
1066
- logger.error("Error getting drive from cache", e);
1067
- }
1068
- const driveStorage = document ?? (await this.storage.getDrive(driveId));
1069
- const result = this._buildDocument(driveStorage, options);
1070
- if (!isDocumentDrive(result)) {
1071
- throw new Error(`Document with id ${driveId} is not a Document Drive`);
1072
- } else {
1073
- if (!options?.revisions) {
1074
- this.cache.setDocument("drives", driveId, result).catch(logger.error);
1075
- }
1076
- return result;
1077
- }
1078
- }
1079
-
1080
- async getDriveBySlug(slug: string, options?: GetDocumentOptions) {
1081
- try {
1082
- const document = await this.cache.getDocument("drives-slug", slug);
1083
- if (document && isDocumentDrive(document)) {
1084
- return document;
1085
- }
1086
- } catch (e) {
1087
- logger.error("Error getting drive from cache", e);
1088
- }
1089
-
1090
- const driveStorage = await this.storage.getDriveBySlug(slug);
1091
- const document = this._buildDocument(driveStorage, options);
1092
- if (!isDocumentDrive(document)) {
1093
- throw new Error(`Document with slug ${slug} is not a Document Drive`);
1094
- } else {
1095
- this.cache.setDocument("drives-slug", slug, document).catch(logger.error);
1096
- return document;
1097
- }
1098
- }
1099
-
1100
- async getDocument(
1101
- driveId: string,
1102
- documentId: string,
1103
- options?: GetDocumentOptions,
1104
- ) {
1105
- let cachedDocument: Document | undefined;
1106
- try {
1107
- cachedDocument = await this.cache.getDocument(driveId, documentId); // TODO support GetDocumentOptions
1108
- if (cachedDocument && isAtRevision(cachedDocument, options?.revisions)) {
1109
- return cachedDocument;
1110
- }
1111
- } catch (e) {
1112
- logger.error("Error getting document from cache", e);
1113
- }
1114
- const documentStorage =
1115
- cachedDocument ?? (await this.storage.getDocument(driveId, documentId));
1116
- const document = this._buildDocument(documentStorage, options);
1117
-
1118
- if (!options?.revisions) {
1119
- this.cache.setDocument(driveId, documentId, document).catch(logger.error);
1120
- }
1121
- return document;
1122
- }
1123
-
1124
- getDocuments(driveId: string) {
1125
- return this.storage.getDocuments(driveId);
1126
- }
1127
-
1128
- protected async createDocument(driveId: string, input: CreateDocumentInput) {
1129
- // if a document was provided then checks if it's valid
1130
- let state = undefined;
1131
- if (input.document) {
1132
- if (input.documentType !== input.document.documentType) {
1133
- throw new Error(`Provided document is not ${input.documentType}`);
1134
- }
1135
- const doc = this._buildDocument(input.document);
1136
- state = doc.state;
1137
- }
1138
-
1139
- // if no document was provided then create a new one
1140
- const document =
1141
- input.document ??
1142
- this.getDocumentModel(input.documentType).utils.createDocument();
1143
-
1144
- // stores document information
1145
- const documentStorage: DocumentStorage = {
1146
- name: document.name,
1147
- revision: document.revision,
1148
- documentType: document.documentType,
1149
- created: document.created,
1150
- lastModified: document.lastModified,
1151
- operations: { global: [], local: [] },
1152
- initialState: document.initialState,
1153
- clipboard: [],
1154
- state: state ?? document.state,
1155
- };
1156
- await this.storage.createDocument(driveId, input.id, documentStorage);
1157
-
1158
- // set initial state for new syncUnits
1159
- for (const syncUnit of input.synchronizationUnits) {
1160
- this.initSyncStatus(syncUnit.syncId, {
1161
- pull: this.triggerMap.get(driveId) ? "INITIAL_SYNC" : undefined,
1162
- push: this.listenerManager.driveHasListeners(driveId)
1163
- ? "SUCCESS"
1164
- : undefined,
1165
- });
1166
- }
1167
-
1168
- // if the document contains operations then
1169
- // stores the operations in the storage
1170
- const operations = Object.values(document.operations).flat();
1171
- if (operations.length) {
1172
- if (isDocumentDrive(document)) {
1173
- await this.storage.addDriveOperations(
1174
- driveId,
1175
- operations as Operation<DocumentDriveAction>[],
1176
- document,
1177
- );
1178
- } else {
1179
- await this.storage.addDocumentOperations(
1180
- driveId,
1181
- input.id,
1182
- operations,
1183
- document,
1184
- );
1185
- }
1186
- }
1187
-
1188
- return document;
1189
- }
1190
-
1191
- async deleteDocument(driveId: string, documentId: string) {
1192
- try {
1193
- const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
1194
- documentId,
1195
- ]);
1196
-
1197
- // remove document sync units status when a document is deleted
1198
- for (const syncUnit of syncUnits) {
1199
- this.updateSyncUnitStatus(syncUnit.syncId, null);
1200
- }
1201
- await this.listenerManager.removeSyncUnits(driveId, syncUnits);
1202
- } catch (error) {
1203
- logger.warn("Error deleting document", error);
1204
- }
1205
- await this.cache.deleteDocument(driveId, documentId);
1206
- return this.storage.deleteDocument(driveId, documentId);
1207
- }
1208
-
1209
- async _processOperations<T extends Document, A extends Action>(
1210
- driveId: string,
1211
- documentId: string | undefined,
1212
- documentStorage: DocumentStorage<T>,
1213
- operations: Operation<A | BaseAction>[],
1214
- ) {
1215
- const operationsApplied: Operation<A | BaseAction>[] = [];
1216
- const signals: SignalResult[] = [];
1217
-
1218
- const documentStorageWithState = await this._addDocumentResultingStage(
1219
- documentStorage,
1220
- driveId,
1221
- documentId,
1222
- );
1223
-
1224
- let document: T = this._buildDocument(documentStorageWithState);
1225
- let error: OperationError | undefined; // TODO: replace with an array of errors/consistency issues
1226
- const operationsByScope = groupOperationsByScope(operations);
1227
-
1228
- for (const scope of Object.keys(operationsByScope)) {
1229
- const storageDocumentOperations =
1230
- documentStorage.operations[scope as OperationScope];
1231
-
1232
- // TODO two equal operations done by two clients will be considered the same, ie: { type: "INCREMENT" }
1233
- const branch = removeExistingOperations(
1234
- operationsByScope[scope as OperationScope] || [],
1235
- storageDocumentOperations,
1236
- );
1237
-
1238
- // No operations to apply
1239
- if (branch.length < 1) {
1240
- continue;
1241
- }
1242
-
1243
- const trunk = garbageCollect(sortOperations(storageDocumentOperations));
1244
-
1245
- const [invertedTrunk, tail] = attachBranch(trunk, branch);
1246
-
1247
- const newHistory =
1248
- tail.length < 1
1249
- ? invertedTrunk
1250
- : merge(trunk, invertedTrunk, reshuffleByTimestamp);
1251
-
1252
- const newOperations = newHistory.filter(
1253
- (op) => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op),
1254
- );
1255
-
1256
- for (const nextOperation of newOperations) {
1257
- let skipHashValidation = false;
1258
-
1259
- // when dealing with a merge (tail.length > 0) we have to skip hash validation
1260
- // for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
1261
- if (tail.length > 0) {
1262
- const sourceOperation = operations.find(
1263
- (op) => op.hash === nextOperation.hash,
1264
- );
1265
-
1266
- skipHashValidation =
1267
- !sourceOperation ||
1268
- sourceOperation.index !== nextOperation.index ||
1269
- sourceOperation.skip !== nextOperation.skip;
1270
- }
1271
-
1272
- try {
1273
- // runs operation on next available tick, to avoid blocking the main thread
1274
- const taskQueueMethod = this.options.taskQueueMethod;
1275
- const task = () =>
1276
- this._performOperation(
1277
- driveId,
1278
- documentId,
1279
- document,
1280
- nextOperation,
1281
- skipHashValidation,
1282
- );
1283
- const appliedResult = await (taskQueueMethod
1284
- ? runAsapAsync(task, taskQueueMethod)
1285
- : task());
1286
- document = appliedResult.document;
1287
- signals.push(...appliedResult.signals);
1288
- operationsApplied.push(appliedResult.operation);
1289
-
1290
- // TODO what to do if one of the applied operations has an error?
1291
- } catch (e) {
1292
- error =
1293
- e instanceof OperationError
1294
- ? e
1295
- : new OperationError(
1296
- "ERROR",
1297
- nextOperation,
1298
- (e as Error).message,
1299
- (e as Error).cause,
1300
- );
1301
-
1302
- // TODO: don't break on errors...
1303
- break;
1304
- }
1305
- }
1306
- }
1307
-
1308
- return {
1309
- document,
1310
- operationsApplied,
1311
- signals,
1312
- error,
1313
- } as const;
1314
- }
1315
-
1316
- private async _addDocumentResultingStage<T extends Document>(
1317
- document: DocumentStorage<T>,
1318
- driveId: string,
1319
- documentId?: string,
1320
- options?: GetDocumentOptions,
1321
- ): Promise<DocumentStorage<T>> {
1322
- // apply skip header operations to all scopes
1323
- const operations =
1324
- options?.revisions !== undefined
1325
- ? filterOperationsByRevision(document.operations, options.revisions)
1326
- : document.operations;
1327
- const documentOperations =
1328
- DocumentUtils.documentHelpers.garbageCollectDocumentOperations(
1329
- operations,
1330
- );
1331
-
1332
- for (const scope of Object.keys(documentOperations)) {
1333
- const lastRemainingOperation =
1334
- documentOperations[scope as OperationScope].at(-1);
1335
- // if the latest operation doesn't have a resulting state then tries
1336
- // to retrieve it from the db to avoid rerunning all the operations
1337
- if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
1338
- lastRemainingOperation.resultingState = await (documentId
1339
- ? this.storage.getOperationResultingState?.(
1340
- driveId,
1341
- documentId,
1342
- lastRemainingOperation.index,
1343
- lastRemainingOperation.scope,
1344
- "main",
1345
- )
1346
- : this.storage.getDriveOperationResultingState?.(
1347
- driveId,
1348
- lastRemainingOperation.index,
1349
- lastRemainingOperation.scope,
1350
- "main",
1351
- ));
1352
- }
1353
- }
1354
-
1355
- return {
1356
- ...document,
1357
- operations: documentOperations,
1358
- };
1359
- }
1360
-
1361
- private _buildDocument<T extends Document>(
1362
- documentStorage: DocumentStorage<T>,
1363
- options?: GetDocumentOptions,
1364
- ): T {
1365
- if (
1366
- documentStorage.state &&
1367
- (!options || options.checkHashes === false) &&
1368
- isAtRevision(documentStorage as unknown as Document, options?.revisions)
1369
- ) {
1370
- return documentStorage as T;
1371
- }
1372
-
1373
- const documentModel = this.getDocumentModel(documentStorage.documentType);
1374
-
1375
- const revisionOperations =
1376
- options?.revisions !== undefined
1377
- ? filterOperationsByRevision(
1378
- documentStorage.operations,
1379
- options.revisions,
1380
- )
1381
- : documentStorage.operations;
1382
- const operations =
1383
- baseUtils.documentHelpers.garbageCollectDocumentOperations(
1384
- revisionOperations,
1385
- );
1386
-
1387
- return baseUtils.replayDocument(
1388
- documentStorage.initialState,
1389
- operations,
1390
- documentModel.reducer,
1391
- undefined,
1392
- documentStorage,
1393
- undefined,
1394
- {
1395
- ...options,
1396
- checkHashes: options?.checkHashes ?? true,
1397
- reuseOperationResultingState: options?.checkHashes ?? true,
1398
- },
1399
- ) as T;
1400
- }
1401
-
1402
- private async _performOperation<T extends Document>(
1403
- driveId: string,
1404
- documentId: string | undefined,
1405
- document: T,
1406
- operation: Operation,
1407
- skipHashValidation = false,
1408
- ) {
1409
- const documentModel = this.getDocumentModel(document.documentType);
1410
-
1411
- const signalResults: SignalResult[] = [];
1412
- let newDocument = document;
1413
-
1414
- const scope = operation.scope;
1415
- const documentOperations =
1416
- DocumentUtils.documentHelpers.garbageCollectDocumentOperations({
1417
- ...document.operations,
1418
- [scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
1419
- document.operations[scope],
1420
- operation,
1421
- ),
1422
- });
1423
-
1424
- const lastRemainingOperation = documentOperations[scope].at(-1);
1425
- // if the latest operation doesn't have a resulting state then tries
1426
- // to retrieve it from the db to avoid rerunning all the operations
1427
- if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
1428
- lastRemainingOperation.resultingState = await (documentId
1429
- ? this.storage.getOperationResultingState?.(
1430
- driveId,
1431
- documentId,
1432
- lastRemainingOperation.index,
1433
- lastRemainingOperation.scope,
1434
- "main",
1435
- )
1436
- : this.storage.getDriveOperationResultingState?.(
1437
- driveId,
1438
- lastRemainingOperation.index,
1439
- lastRemainingOperation.scope,
1440
- "main",
1441
- ));
1442
- }
1443
-
1444
- const operationSignals: (() => Promise<SignalResult>)[] = [];
1445
- newDocument = documentModel.reducer(
1446
- newDocument,
1447
- operation,
1448
- (signal) => {
1449
- let handler: (() => Promise<unknown>) | undefined = undefined;
1450
- switch (signal.type) {
1451
- case "CREATE_CHILD_DOCUMENT":
1452
- handler = () => this.createDocument(driveId, signal.input);
1453
- break;
1454
- case "DELETE_CHILD_DOCUMENT":
1455
- handler = () => this.deleteDocument(driveId, signal.input.id);
1456
- break;
1457
- case "COPY_CHILD_DOCUMENT":
1458
- handler = () =>
1459
- this.getDocument(driveId, signal.input.id).then(
1460
- (documentToCopy) =>
1461
- this.createDocument(driveId, {
1462
- id: signal.input.newId,
1463
- documentType: documentToCopy.documentType,
1464
- document: documentToCopy,
1465
- synchronizationUnits: signal.input.synchronizationUnits,
1466
- }),
1467
- );
1468
- break;
1469
- }
1470
- if (handler) {
1471
- operationSignals.push(() =>
1472
- handler().then((result) => ({ signal, result })),
1473
- );
1474
- }
1475
- },
1476
- { skip: operation.skip, reuseOperationResultingState: true },
1477
- ) as T;
1478
-
1479
- const appliedOperations = newDocument.operations[operation.scope].filter(
1480
- (op) => op.index == operation.index && op.skip == operation.skip,
1481
- );
1482
- const appliedOperation = appliedOperations.at(0);
1483
-
1484
- if (!appliedOperation) {
1485
- throw new OperationError(
1486
- "ERROR",
1487
- operation,
1488
- `Operation with index ${operation.index}:${operation.skip} was not applied.`,
1489
- );
1490
- }
1491
- if (
1492
- !appliedOperation.error &&
1493
- appliedOperation.hash !== operation.hash &&
1494
- !skipHashValidation
1495
- ) {
1496
- throw new ConflictOperationError(operation, appliedOperation);
1497
- }
1498
-
1499
- for (const signalHandler of operationSignals) {
1500
- const result = await signalHandler();
1501
- signalResults.push(result);
1502
- }
1503
-
1504
- return {
1505
- document: newDocument,
1506
- signals: signalResults,
1507
- operation: appliedOperation,
1508
- };
1509
- }
1510
-
1511
- addOperation(
1512
- driveId: string,
1513
- documentId: string,
1514
- operation: Operation,
1515
- options?: AddOperationOptions,
1516
- ): Promise<IOperationResult> {
1517
- return this.addOperations(driveId, documentId, [operation], options);
1518
- }
1519
-
1520
- private async _addOperations(
1521
- driveId: string,
1522
- documentId: string,
1523
- callback: (document: DocumentStorage) => Promise<{
1524
- operations: Operation[];
1525
- header: DocumentHeader;
1526
- }>,
1527
- ) {
1528
- if (!this.storage.addDocumentOperationsWithTransaction) {
1529
- const documentStorage = await this.storage.getDocument(
1530
- driveId,
1531
- documentId,
1532
- );
1533
- const result = await callback(documentStorage);
1534
- // saves the applied operations to storage
1535
- if (result.operations.length > 0) {
1536
- await this.storage.addDocumentOperations(
1537
- driveId,
1538
- documentId,
1539
- result.operations,
1540
- result.header,
1541
- );
1542
- }
1543
- } else {
1544
- await this.storage.addDocumentOperationsWithTransaction(
1545
- driveId,
1546
- documentId,
1547
- callback,
1548
- );
1549
- }
1550
- }
1551
-
1552
- queueOperation(
1553
- driveId: string,
1554
- documentId: string,
1555
- operation: Operation,
1556
- options?: AddOperationOptions,
1557
- ): Promise<IOperationResult> {
1558
- return this.queueOperations(driveId, documentId, [operation], options);
1559
- }
1560
-
1561
- private async resultIfExistingOperations(
1562
- drive: string,
1563
- id: string,
1564
- operations: Operation[],
1565
- ): Promise<IOperationResult | undefined> {
1566
- try {
1567
- const document = await this.getDocument(drive, id);
1568
- const newOperation = operations.find(
1569
- (op) =>
1570
- !op.id ||
1571
- !document.operations[op.scope].find(
1572
- (existingOp) =>
1573
- existingOp.id === op.id &&
1574
- existingOp.index === op.index &&
1575
- existingOp.type === op.type &&
1576
- existingOp.hash === op.hash,
1577
- ),
1578
- );
1579
- if (!newOperation) {
1580
- return {
1581
- status: "SUCCESS",
1582
- document,
1583
- operations,
1584
- signals: [],
1585
- };
1586
- } else {
1587
- return undefined;
1588
- }
1589
- } catch (error) {
1590
- if (
1591
- !(error as Error).message.includes(`Document with id ${id} not found`)
1592
- ) {
1593
- console.error(error);
1594
- }
1595
- return undefined;
1596
- }
1597
- }
1598
-
1599
- async queueOperations(
1600
- driveId: string,
1601
- documentId: string,
1602
- operations: Operation[],
1603
- options?: AddOperationOptions,
1604
- ) {
1605
- // if operations are already stored then returns cached document
1606
- const result = await this.resultIfExistingOperations(
1607
- driveId,
1608
- documentId,
1609
- operations,
1610
- );
1611
- if (result) {
1612
- return result;
1613
- }
1614
- try {
1615
- const jobId = await this.queueManager.addJob({
1616
- driveId: driveId,
1617
- documentId: documentId,
1618
- operations,
1619
- options,
1620
- });
1621
-
1622
- return new Promise<IOperationResult>((resolve, reject) => {
1623
- const unsubscribe = this.queueManager.on(
1624
- "jobCompleted",
1625
- (job, result) => {
1626
- if (job.jobId === jobId) {
1627
- unsubscribe();
1628
- unsubscribeError();
1629
- resolve(result);
1630
- }
1631
- },
1632
- );
1633
- const unsubscribeError = this.queueManager.on(
1634
- "jobFailed",
1635
- (job, error) => {
1636
- if (job.jobId === jobId) {
1637
- unsubscribe();
1638
- unsubscribeError();
1639
- reject(error);
1640
- }
1641
- },
1642
- );
1643
- });
1644
- } catch (error) {
1645
- logger.error("Error adding job", error);
1646
- throw error;
1647
- }
1648
- }
1649
-
1650
- async queueAction(
1651
- driveId: string,
1652
- documentId: string,
1653
- action: Action,
1654
- options?: AddOperationOptions,
1655
- ): Promise<IOperationResult> {
1656
- return this.queueActions(driveId, documentId, [action], options);
1657
- }
1658
-
1659
- async queueActions(
1660
- driveId: string,
1661
- documentId: string,
1662
- actions: Action[],
1663
- options?: AddOperationOptions,
1664
- ): Promise<IOperationResult> {
1665
- try {
1666
- const jobId = await this.queueManager.addJob({
1667
- driveId: driveId,
1668
- documentId: documentId,
1669
- actions,
1670
- options,
1671
- });
1672
-
1673
- return new Promise<IOperationResult>((resolve, reject) => {
1674
- const unsubscribe = this.queueManager.on(
1675
- "jobCompleted",
1676
- (job, result) => {
1677
- if (job.jobId === jobId) {
1678
- unsubscribe();
1679
- unsubscribeError();
1680
- resolve(result);
1681
- }
1682
- },
1683
- );
1684
- const unsubscribeError = this.queueManager.on(
1685
- "jobFailed",
1686
- (job, error) => {
1687
- if (job.jobId === jobId) {
1688
- unsubscribe();
1689
- unsubscribeError();
1690
- reject(error);
1691
- }
1692
- },
1693
- );
1694
- });
1695
- } catch (error) {
1696
- logger.error("Error adding job", error);
1697
- throw error;
1698
- }
1699
- }
1700
-
1701
- async queueDriveAction(
1702
- driveId: string,
1703
- action: DocumentDriveAction | BaseAction,
1704
- options?: AddOperationOptions,
1705
- ): Promise<IOperationResult<DocumentDriveDocument>> {
1706
- return this.queueDriveActions(driveId, [action], options);
1707
- }
1708
-
1709
- async queueDriveActions(
1710
- driveId: string,
1711
- actions: (DocumentDriveAction | BaseAction)[],
1712
- options?: AddOperationOptions,
1713
- ): Promise<IOperationResult<DocumentDriveDocument>> {
1714
- try {
1715
- const jobId = await this.queueManager.addJob({
1716
- driveId: driveId,
1717
- actions,
1718
- options,
1719
- });
1720
- return new Promise<IOperationResult<DocumentDriveDocument>>(
1721
- (resolve, reject) => {
1722
- const unsubscribe = this.queueManager.on(
1723
- "jobCompleted",
1724
- (job, result) => {
1725
- if (job.jobId === jobId) {
1726
- unsubscribe();
1727
- unsubscribeError();
1728
- resolve(result as IOperationResult<DocumentDriveDocument>);
1729
- }
1730
- },
1731
- );
1732
- const unsubscribeError = this.queueManager.on(
1733
- "jobFailed",
1734
- (job, error) => {
1735
- if (job.jobId === jobId) {
1736
- unsubscribe();
1737
- unsubscribeError();
1738
- reject(error);
1739
- }
1740
- },
1741
- );
1742
- },
1743
- );
1744
- } catch (error) {
1745
- logger.error("Error adding drive job", error);
1746
- throw error;
1747
- }
1748
- }
1749
-
1750
- async addOperations(
1751
- driveId: string,
1752
- documentId: string,
1753
- operations: Operation[],
1754
- options?: AddOperationOptions,
1755
- ) {
1756
- // if operations are already stored then returns the result
1757
- const result = await this.resultIfExistingOperations(
1758
- driveId,
1759
- documentId,
1760
- operations,
1761
- );
1762
- if (result) {
1763
- return result;
1764
- }
1765
- let document: Document | undefined;
1766
- const operationsApplied: Operation[] = [];
1767
- const signals: SignalResult[] = [];
1768
- let error: Error | undefined;
1769
-
1770
- try {
1771
- await this._addOperations(
1772
- driveId,
1773
- documentId,
1774
- async (documentStorage) => {
1775
- const result = await this._processOperations(
1776
- driveId,
1777
- documentId,
1778
- documentStorage,
1779
- operations,
1780
- );
1781
-
1782
- if (!result.document) {
1783
- logger.error("Invalid document");
1784
- throw result.error ?? new Error("Invalid document");
1785
- }
1786
-
1787
- document = result.document;
1788
- error = result.error;
1789
- signals.push(...result.signals);
1790
- operationsApplied.push(...result.operationsApplied);
1791
-
1792
- return {
1793
- operations: result.operationsApplied,
1794
- header: result.document,
1795
- newState: document.state,
1796
- };
1797
- },
1798
- );
1799
-
1800
- if (document) {
1801
- this.cache
1802
- .setDocument(driveId, documentId, document)
1803
- .catch(logger.error);
1804
- }
1805
-
1806
- // gets all the different scopes and branches combinations from the operations
1807
- const { scopes, branches } = operationsApplied.reduce(
1808
- (acc, operation) => {
1809
- if (!acc.scopes.includes(operation.scope)) {
1810
- acc.scopes.push(operation.scope);
1811
- }
1812
- return acc;
1813
- },
1814
- { scopes: [] as string[], branches: ["main"] },
1815
- );
1816
-
1817
- const syncUnits = await this.getSynchronizationUnits(
1818
- driveId,
1819
- [documentId],
1820
- scopes,
1821
- branches,
1822
- );
1823
-
1824
- // checks if any of the provided operations where reshufled
1825
- const newOp = operationsApplied.find(
1826
- (appliedOp) =>
1827
- !operations.find(
1828
- (o) =>
1829
- o.id === appliedOp.id &&
1830
- o.index === appliedOp.index &&
1831
- o.skip === appliedOp.skip &&
1832
- o.hash === appliedOp.hash,
1833
- ),
1834
- );
1835
-
1836
- // if there are no new operations then reuses the provided source
1837
- // otherwise sets it to local so listeners know that there were
1838
- // new changes originating from this document drive server
1839
- const source: StrandUpdateSource = newOp
1840
- ? { type: "local" }
1841
- : (options?.source ?? { type: "local" });
1842
-
1843
- // update listener cache
1844
-
1845
- const operationSource = this.getOperationSource(source);
1846
-
1847
- this.listenerManager
1848
- .updateSynchronizationRevisions(
1849
- driveId,
1850
- syncUnits,
1851
- source,
1852
- () => {
1853
- this.updateSyncUnitStatus(driveId, {
1854
- [operationSource]: "SYNCING",
1855
- });
1856
-
1857
- for (const syncUnit of syncUnits) {
1858
- this.updateSyncUnitStatus(syncUnit.syncId, {
1859
- [operationSource]: "SYNCING",
1860
- });
1861
- }
1862
- },
1863
- this.handleListenerError.bind(this),
1864
- options?.forceSync ?? source.type === "local",
1865
- )
1866
- .then((updates) => {
1867
- if (updates.length) {
1868
- this.updateSyncUnitStatus(driveId, {
1869
- [operationSource]: "SUCCESS",
1870
- });
1871
- }
1872
-
1873
- for (const syncUnit of syncUnits) {
1874
- this.updateSyncUnitStatus(syncUnit.syncId, {
1875
- [operationSource]: "SUCCESS",
1876
- });
1877
- }
1878
- })
1879
- .catch((error) => {
1880
- logger.error("Non handled error updating sync revision", error);
1881
- this.updateSyncUnitStatus(
1882
- driveId,
1883
- {
1884
- [operationSource]: "ERROR",
1885
- },
1886
- error as Error,
1887
- );
1888
-
1889
- for (const syncUnit of syncUnits) {
1890
- this.updateSyncUnitStatus(
1891
- syncUnit.syncId,
1892
- {
1893
- [operationSource]: "ERROR",
1894
- },
1895
- error as Error,
1896
- );
1897
- }
1898
- });
1899
-
1900
- // after applying all the valid operations,throws
1901
- // an error if there was an invalid operation
1902
- if (error) {
1903
- throw error;
1904
- }
1905
-
1906
- return {
1907
- status: "SUCCESS",
1908
- document,
1909
- operations: operationsApplied,
1910
- signals,
1911
- } satisfies IOperationResult;
1912
- } catch (error) {
1913
- const operationError =
1914
- error instanceof OperationError
1915
- ? error
1916
- : new OperationError(
1917
- "ERROR",
1918
- undefined,
1919
- (error as Error).message,
1920
- (error as Error).cause,
1921
- );
1922
-
1923
- return {
1924
- status: operationError.status,
1925
- error: operationError,
1926
- document,
1927
- operations: operationsApplied,
1928
- signals,
1929
- } satisfies IOperationResult;
1930
- }
1931
- }
1932
-
1933
- addDriveOperation(
1934
- driveId: string,
1935
- operation: Operation<DocumentDriveAction | BaseAction>,
1936
- options?: AddOperationOptions,
1937
- ) {
1938
- return this.addDriveOperations(driveId, [operation], options);
1939
- }
1940
-
1941
- async clearStorage() {
1942
- for (const drive of await this.getDrives()) {
1943
- await this.deleteDrive(drive);
1944
- }
1945
-
1946
- await this.storage.clearStorage?.();
1947
- }
1948
-
1949
- private async _addDriveOperations(
1950
- driveId: string,
1951
- callback: (document: DocumentDriveStorage) => Promise<{
1952
- operations: Operation<DocumentDriveAction | BaseAction>[];
1953
- header: DocumentHeader;
1954
- }>,
1955
- ) {
1956
- if (!this.storage.addDriveOperationsWithTransaction) {
1957
- const documentStorage = await this.storage.getDrive(driveId);
1958
- const result = await callback(documentStorage);
1959
- // saves the applied operations to storage
1960
- if (result.operations.length > 0) {
1961
- await this.storage.addDriveOperations(
1962
- driveId,
1963
- result.operations,
1964
- result.header,
1965
- );
1966
- }
1967
- return result;
1968
- } else {
1969
- return this.storage.addDriveOperationsWithTransaction(driveId, callback);
1970
- }
1971
- }
1972
-
1973
- queueDriveOperation(
1974
- driveId: string,
1975
- operation: Operation<DocumentDriveAction | BaseAction>,
1976
- options?: AddOperationOptions,
1977
- ): Promise<IOperationResult<DocumentDriveDocument>> {
1978
- return this.queueDriveOperations(driveId, [operation], options);
1979
- }
1980
-
1981
- private async resultIfExistingDriveOperations(
1982
- driveId: string,
1983
- operations: Operation<DocumentDriveAction | BaseAction>[],
1984
- ): Promise<IOperationResult<DocumentDriveDocument> | undefined> {
1985
- try {
1986
- const drive = await this.getDrive(driveId);
1987
- const newOperation = operations.find(
1988
- (op) =>
1989
- !op.id ||
1990
- !drive.operations[op.scope].find(
1991
- (existingOp) =>
1992
- existingOp.id === op.id &&
1993
- existingOp.index === op.index &&
1994
- existingOp.type === op.type &&
1995
- existingOp.hash === op.hash,
1996
- ),
1997
- );
1998
- if (!newOperation) {
1999
- return {
2000
- status: "SUCCESS",
2001
- document: drive,
2002
- operations: operations,
2003
- signals: [],
2004
- } as IOperationResult<DocumentDriveDocument>;
2005
- } else {
2006
- return undefined;
2007
- }
2008
- } catch (error) {
2009
- console.error(error); // TODO error
2010
- return undefined;
2011
- }
2012
- }
2013
-
2014
- async queueDriveOperations(
2015
- driveId: string,
2016
- operations: Operation<DocumentDriveAction | BaseAction>[],
2017
- options?: AddOperationOptions,
2018
- ): Promise<IOperationResult<DocumentDriveDocument>> {
2019
- // if operations are already stored then returns cached document
2020
- const result = await this.resultIfExistingDriveOperations(
2021
- driveId,
2022
- operations,
2023
- );
2024
- if (result) {
2025
- return result;
2026
- }
2027
- try {
2028
- const jobId = await this.queueManager.addJob({
2029
- driveId: driveId,
2030
- operations,
2031
- options,
2032
- });
2033
- return new Promise<IOperationResult<DocumentDriveDocument>>(
2034
- (resolve, reject) => {
2035
- const unsubscribe = this.queueManager.on(
2036
- "jobCompleted",
2037
- (job, result) => {
2038
- if (job.jobId === jobId) {
2039
- unsubscribe();
2040
- unsubscribeError();
2041
- resolve(result as IOperationResult<DocumentDriveDocument>);
2042
- }
2043
- },
2044
- );
2045
- const unsubscribeError = this.queueManager.on(
2046
- "jobFailed",
2047
- (job, error) => {
2048
- if (job.jobId === jobId) {
2049
- unsubscribe();
2050
- unsubscribeError();
2051
- reject(error);
2052
- }
2053
- },
2054
- );
2055
- },
2056
- );
2057
- } catch (error) {
2058
- logger.error("Error adding drive job", error);
2059
- throw error;
2060
- }
2061
- }
2062
-
2063
- async addDriveOperations(
2064
- driveId: string,
2065
- operations: Operation<DocumentDriveAction | BaseAction>[],
2066
- options?: AddOperationOptions,
2067
- ) {
2068
- let document: DocumentDriveDocument | undefined;
2069
- const operationsApplied: Operation<DocumentDriveAction | BaseAction>[] = [];
2070
- const signals: SignalResult[] = [];
2071
- let error: Error | undefined;
2072
-
2073
- // if operations are already stored then returns cached drive
2074
- const result = await this.resultIfExistingDriveOperations(
2075
- driveId,
2076
- operations,
2077
- );
2078
- if (result) {
2079
- return result;
2080
- }
2081
-
2082
- try {
2083
- await this._addDriveOperations(driveId, async (documentStorage) => {
2084
- const result = await this._processOperations<
2085
- DocumentDriveDocument,
2086
- DocumentDriveAction
2087
- >(driveId, undefined, documentStorage, operations.slice());
2088
- document = result.document;
2089
- operationsApplied.push(...result.operationsApplied);
2090
- signals.push(...result.signals);
2091
- error = result.error;
2092
-
2093
- return {
2094
- operations: result.operationsApplied,
2095
- header: result.document,
2096
- };
2097
- });
2098
-
2099
- if (!document || !isDocumentDrive(document)) {
2100
- throw error ?? new Error("Invalid Document Drive document");
2101
- }
2102
-
2103
- this.cache.setDocument("drives", driveId, document).catch(logger.error);
2104
-
2105
- for (const operation of operationsApplied) {
2106
- switch (operation.type) {
2107
- case "ADD_LISTENER": {
2108
- const zodListener = operation.input.listener;
2109
-
2110
- // create the transmitter
2111
- const transmitter = this.transmitterFactory.instance(
2112
- zodListener.callInfo?.transmitterType ?? "",
2113
- {
2114
- driveId,
2115
- listenerId: zodListener.listenerId,
2116
- block: zodListener.block,
2117
- filter: zodListener.filter,
2118
- system: zodListener.system,
2119
- label: zodListener.label || undefined,
2120
- callInfo: zodListener.callInfo || undefined,
2121
- },
2122
- this,
2123
- );
2124
-
2125
- // create the listener
2126
- const listener = {
2127
- ...zodListener,
2128
- driveId: driveId,
2129
- label: zodListener.label ?? "",
2130
- system: zodListener.system ?? false,
2131
- filter: {
2132
- branch: zodListener.filter.branch ?? [],
2133
- documentId: zodListener.filter.documentId ?? [],
2134
- documentType: zodListener.filter.documentType ?? [],
2135
- scope: zodListener.filter.scope ?? [],
2136
- },
2137
- callInfo: {
2138
- data: zodListener.callInfo?.data ?? "",
2139
- name: zodListener.callInfo?.name ?? "PullResponder",
2140
- transmitterType:
2141
- zodListener.callInfo?.transmitterType ?? "PullResponder",
2142
- },
2143
- transmitter,
2144
- };
2145
-
2146
- await this.addListener(driveId, listener);
2147
- break;
2148
- }
2149
- case "REMOVE_LISTENER": {
2150
- await this.removeListener(driveId, operation);
2151
- break;
2152
- }
2153
- }
2154
- }
2155
-
2156
- // update listener cache
2157
- const lastOperation = operationsApplied
2158
- .filter((op) => op.scope === "global")
2159
- .slice()
2160
- .pop();
2161
-
2162
- if (lastOperation) {
2163
- // checks if any of the provided operations where reshufled
2164
- const newOp = operationsApplied.find(
2165
- (appliedOp) =>
2166
- !operations.find(
2167
- (o) =>
2168
- o.id === appliedOp.id &&
2169
- o.index === appliedOp.index &&
2170
- o.skip === appliedOp.skip &&
2171
- o.hash === appliedOp.hash,
2172
- ),
2173
- );
2174
-
2175
- // if there are no new operations then reuses the provided source
2176
- // otherwise sets it to local so listeners know that there were
2177
- // new changes originating from this document drive server
2178
- const source: StrandUpdateSource = newOp
2179
- ? { type: "local" }
2180
- : (options?.source ?? { type: "local" });
2181
-
2182
- const operationSource = this.getOperationSource(source);
2183
-
2184
- this.listenerManager
2185
- .updateSynchronizationRevisions(
2186
- driveId,
2187
- [
2188
- {
2189
- syncId: "0",
2190
- driveId: driveId,
2191
- documentId: "",
2192
- scope: "global",
2193
- branch: "main",
2194
- documentType: "powerhouse/document-drive",
2195
- lastUpdated: lastOperation.timestamp,
2196
- revision: lastOperation.index,
2197
- },
2198
- ],
2199
- source,
2200
- () => {
2201
- this.updateSyncUnitStatus(driveId, {
2202
- [operationSource]: "SYNCING",
2203
- });
2204
- },
2205
- this.handleListenerError.bind(this),
2206
- options?.forceSync ?? source.type === "local",
2207
- )
2208
- .then((updates) => {
2209
- if (updates.length) {
2210
- this.updateSyncUnitStatus(driveId, {
2211
- [operationSource]: "SUCCESS",
2212
- });
2213
- }
2214
- })
2215
- .catch((error) => {
2216
- logger.error("Non handled error updating sync revision", error);
2217
- this.updateSyncUnitStatus(
2218
- driveId,
2219
- { [operationSource]: "ERROR" },
2220
- error as Error,
2221
- );
2222
- });
2223
- }
2224
-
2225
- if (this.shouldSyncRemoteDrive(document)) {
2226
- this.startSyncRemoteDrive(document.state.global.id);
2227
- } else {
2228
- this.stopSyncRemoteDrive(document.state.global.id);
2229
- }
2230
-
2231
- // after applying all the valid operations,throws
2232
- // an error if there was an invalid operation
2233
- if (error) {
2234
- throw error;
2235
- }
2236
-
2237
- return {
2238
- status: "SUCCESS",
2239
- document,
2240
- operations: operationsApplied,
2241
- signals,
2242
- } satisfies IOperationResult;
2243
- } catch (error) {
2244
- const operationError =
2245
- error instanceof OperationError
2246
- ? error
2247
- : new OperationError(
2248
- "ERROR",
2249
- undefined,
2250
- (error as Error).message,
2251
- (error as Error).cause,
2252
- );
2253
-
2254
- return {
2255
- status: operationError.status,
2256
- error: operationError,
2257
- document,
2258
- operations: operationsApplied,
2259
- signals,
2260
- } satisfies IOperationResult;
2261
- }
2262
- }
2263
-
2264
- private _buildOperations<T extends Action>(
2265
- documentId: Document,
2266
- actions: (T | BaseAction)[],
2267
- ): Operation<T | BaseAction>[] {
2268
- const operations: Operation<T | BaseAction>[] = [];
2269
- const { reducer } = this.getDocumentModel(documentId.documentType);
2270
- for (const action of actions) {
2271
- documentId = reducer(documentId, action);
2272
- const operation = documentId.operations[action.scope].slice().pop();
2273
- if (!operation) {
2274
- throw new Error("Error creating operations");
2275
- }
2276
- operations.push(operation);
2277
- }
2278
- return operations;
2279
- }
2280
-
2281
- async addAction(
2282
- driveId: string,
2283
- documentId: string,
2284
- action: Action,
2285
- options?: AddOperationOptions,
2286
- ): Promise<IOperationResult> {
2287
- return this.addActions(driveId, documentId, [action], options);
2288
- }
2289
-
2290
- async addActions(
2291
- driveId: string,
2292
- documentId: string,
2293
- actions: Action[],
2294
- options?: AddOperationOptions,
2295
- ): Promise<IOperationResult> {
2296
- const document = await this.getDocument(driveId, documentId);
2297
- const operations = this._buildOperations(document, actions);
2298
- return this.addOperations(driveId, documentId, operations, options);
2299
- }
2300
-
2301
- async addDriveAction(
2302
- driveId: string,
2303
- action: DocumentDriveAction | BaseAction,
2304
- options?: AddOperationOptions,
2305
- ): Promise<IOperationResult<DocumentDriveDocument>> {
2306
- return this.addDriveActions(driveId, [action], options);
2307
- }
2308
-
2309
- async addDriveActions(
2310
- driveId: string,
2311
- actions: (DocumentDriveAction | BaseAction)[],
2312
- options?: AddOperationOptions,
2313
- ): Promise<IOperationResult<DocumentDriveDocument>> {
2314
- const document = await this.getDrive(driveId);
2315
- const operations = this._buildOperations(document, actions);
2316
- const result = await this.addDriveOperations(driveId, operations, options);
2317
- return result;
2318
- }
2319
-
2320
- async detachDrive(driveId: string) {
2321
- const documentDrive = await this.getDrive(driveId);
2322
- const listeners = documentDrive.state.local.listeners || [];
2323
- const triggers = documentDrive.state.local.triggers || [];
2324
-
2325
- for (const listener of listeners) {
2326
- await this.addDriveAction(
2327
- driveId,
2328
- actions.removeListener({ listenerId: listener.listenerId }),
2329
- );
2330
- }
2331
-
2332
- for (const trigger of triggers) {
2333
- await this.addDriveAction(
2334
- driveId,
2335
- actions.removeTrigger({ triggerId: trigger.id }),
2336
- );
2337
- }
2338
-
2339
- await this.addDriveAction(
2340
- driveId,
2341
- actions.setSharingType({ type: "LOCAL" }),
2342
- );
2343
- }
2344
-
2345
- async addListener(driveId: string, listener: Listener) {
2346
- await this.listenerManager.setListener(driveId, listener);
2347
- }
2348
-
2349
- async addInternalListener(
2350
- driveId: string,
2351
- receiver: IReceiver,
2352
- options: {
2353
- listenerId: string;
2354
- label: string;
2355
- block: boolean;
2356
- filter: ListenerFilter;
2357
- },
2358
- ) {
2359
- const listener: AddListenerInput["listener"] = {
2360
- callInfo: {
2361
- data: "",
2362
- name: "Interal",
2363
- transmitterType: "Internal",
2364
- },
2365
- system: true,
2366
- ...options,
2367
- };
2368
- await this.addDriveAction(driveId, actions.addListener({ listener }));
2369
- const transmitter = await this.getTransmitter(driveId, options.listenerId);
2370
- if (!transmitter) {
2371
- logger.error("Internal listener not found");
2372
- throw new Error("Internal listener not found");
2373
- }
2374
- if (!(transmitter instanceof InternalTransmitter)) {
2375
- logger.error("Listener is not an internal transmitter");
2376
- throw new Error("Listener is not an internal transmitter");
2377
- }
2378
-
2379
- transmitter.setReceiver(receiver);
2380
- return transmitter;
2381
- }
2382
-
2383
- private async removeListener(
2384
- driveId: string,
2385
- operation: Operation<Action<"REMOVE_LISTENER", RemoveListenerInput>>,
2386
- ) {
2387
- const { listenerId } = operation.input;
2388
- await this.listenerManager.removeListener(driveId, listenerId);
2389
- }
2390
-
2391
- async getTransmitter(
2392
- driveId: string,
2393
- listenerId: string,
2394
- ): Promise<ITransmitter | undefined> {
2395
- const listener = await this.listenerManager.getListenerState(
2396
- driveId,
2397
- listenerId,
2398
- );
2399
- return listener?.listener.transmitter;
2400
- }
2401
-
2402
- getListener(
2403
- driveId: string,
2404
- listenerId: string,
2405
- ): Promise<ListenerState | undefined> {
2406
- let listenerState;
2407
- try {
2408
- listenerState = this.listenerManager.getListenerState(
2409
- driveId,
2410
- listenerId,
2411
- );
2412
- } catch {
2413
- return Promise.resolve(undefined);
2414
- }
2415
-
2416
- return Promise.resolve(listenerState);
2417
- }
2418
-
2419
- getSyncStatus(
2420
- syncUnitId: string,
2421
- ): SyncStatus | SynchronizationUnitNotFoundError {
2422
- const status = this.syncStatus.get(syncUnitId);
2423
- if (!status) {
2424
- return new SynchronizationUnitNotFoundError(
2425
- `Sync status not found for syncUnitId: ${syncUnitId}`,
2426
- syncUnitId,
2427
- );
2428
- }
2429
- return this.getCombinedSyncUnitStatus(status);
2430
- }
2431
-
2432
- on<K extends keyof DriveEvents>(event: K, cb: DriveEvents[K]): Unsubscribe {
2433
- return this.emitter.on(event, cb);
2434
- }
2435
-
2436
- protected emit<K extends keyof DriveEvents>(
2437
- event: K,
2438
- ...args: Parameters<DriveEvents[K]>
2439
- ): void {
2440
- return this.emitter.emit(event, ...args);
2441
- }
2442
- }
2443
-
2444
- export const DocumentDriveServer = ReadModeServer(BaseDocumentDriveServer);