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
@@ -0,0 +1,1280 @@
1
+ import { addListener, removeListener, removeTrigger, setSharingType, } from "#drive-document-model/gen/creators";
2
+ import { createDocument } from "#drive-document-model/gen/utils";
3
+ import { isActionJob, isOperationJob, } from "#queue/types";
4
+ import { ReadModeServer } from "#read-mode/server";
5
+ import { DefaultDrivesManager, } from "#utils/default-drives-manager";
6
+ import { requestPublicDrive } from "#utils/graphql";
7
+ import { logger } from "#utils/logger";
8
+ import { generateUUID, isDocumentDrive, runAsapAsync } from "#utils/misc";
9
+ import { RunAsap } from "#utils/run-asap";
10
+ import { attachBranch, garbageCollect, garbageCollectDocumentOperations, groupOperationsByScope, merge, precedes, removeExistingOperations, replayDocument, reshuffleByTimestamp, skipHeaderOperations, sortOperations } from "document-model";
11
+ import { ClientError } from "graphql-request";
12
+ import { ConflictOperationError, DriveAlreadyExistsError, OperationError, } from "./error.js";
13
+ import { InternalTransmitter, } from "./listener/transmitter/internal.js";
14
+ import { PullResponderTransmitter, } from "./listener/transmitter/pull-responder.js";
15
+ import { DefaultListenerManagerOptions, } from "./types.js";
16
+ import { filterOperationsByRevision, isAtRevision } from "./utils.js";
17
+ export class BaseDocumentDriveServer {
18
+ // external dependencies
19
+ documentModelModules;
20
+ storage;
21
+ cache;
22
+ queueManager;
23
+ eventEmitter;
24
+ options;
25
+ // waiting to move to external dependencies
26
+ listenerManager;
27
+ transmitterFactory;
28
+ synchronizationManager;
29
+ // internal dependencies
30
+ defaultDrivesManager;
31
+ // internal state
32
+ triggerMap = new Map();
33
+ initializePromise;
34
+ constructor(documentModelModules, storage, cache, queueManager, eventEmitter, synchronizationManager, listenerManager, transmitterFactory, options) {
35
+ this.documentModelModules = documentModelModules;
36
+ this.storage = storage;
37
+ this.cache = cache;
38
+ this.queueManager = queueManager;
39
+ this.eventEmitter = eventEmitter;
40
+ this.synchronizationManager = synchronizationManager;
41
+ this.listenerManager = listenerManager;
42
+ this.transmitterFactory = transmitterFactory;
43
+ this.options = {
44
+ ...options,
45
+ defaultDrives: {
46
+ ...options?.defaultDrives,
47
+ },
48
+ listenerManager: {
49
+ ...DefaultListenerManagerOptions,
50
+ ...options?.listenerManager,
51
+ },
52
+ taskQueueMethod: options?.taskQueueMethod === undefined
53
+ ? RunAsap.runAsap
54
+ : options.taskQueueMethod,
55
+ };
56
+ this.defaultDrivesManager = new DefaultDrivesManager(this, this.defaultDrivesManagerDelegate, options);
57
+ this.storage.setStorageDelegate?.({
58
+ getCachedOperations: async (drive, id) => {
59
+ try {
60
+ const document = await this.cache.getDocument(drive, id);
61
+ return document?.operations;
62
+ }
63
+ catch (error) {
64
+ logger.error(error);
65
+ return undefined;
66
+ }
67
+ },
68
+ });
69
+ this.initializePromise = this._initialize();
70
+ }
71
+ initialize() {
72
+ return this.initializePromise;
73
+ }
74
+ async _initialize() {
75
+ await this.listenerManager.initialize(this.handleListenerError);
76
+ await this.queueManager.init(this.queueDelegate, (error) => {
77
+ logger.error(`Error initializing queue manager`, error);
78
+ errors.push(error);
79
+ });
80
+ try {
81
+ await this.defaultDrivesManager.removeOldremoteDrives();
82
+ }
83
+ catch (error) {
84
+ logger.error(error);
85
+ }
86
+ const errors = [];
87
+ const drives = await this.getDrives();
88
+ for (const drive of drives) {
89
+ await this._initializeDrive(drive).catch((error) => {
90
+ logger.error(`Error initializing drive ${drive}`, error);
91
+ errors.push(error);
92
+ });
93
+ }
94
+ if (this.options.defaultDrives.loadOnInit !== false) {
95
+ await this.defaultDrivesManager.initializeDefaultRemoteDrives();
96
+ }
97
+ return errors.length === 0 ? null : errors;
98
+ }
99
+ setDocumentModelModules(modules) {
100
+ this.documentModelModules = [...modules];
101
+ this.eventEmitter.emit("documentModelModules", [...modules]);
102
+ }
103
+ initializeDefaultRemoteDrives() {
104
+ return this.defaultDrivesManager.initializeDefaultRemoteDrives();
105
+ }
106
+ getDefaultRemoteDrives() {
107
+ return this.defaultDrivesManager.getDefaultRemoteDrives();
108
+ }
109
+ setDefaultDriveAccessLevel(url, level) {
110
+ return this.defaultDrivesManager.setDefaultDriveAccessLevel(url, level);
111
+ }
112
+ setAllDefaultDrivesAccessLevel(level) {
113
+ return this.defaultDrivesManager.setAllDefaultDrivesAccessLevel(level);
114
+ }
115
+ getOperationSource(source) {
116
+ return source.type === "local" ? "push" : "pull";
117
+ }
118
+ handleListenerError(error, driveId, listener) {
119
+ logger.error(`Listener ${listener.listener.label ?? listener.listener.listenerId} error:`, error);
120
+ const status = error instanceof OperationError ? error.status : "ERROR";
121
+ this.synchronizationManager.updateSyncStatus(driveId, { push: status }, error);
122
+ }
123
+ shouldSyncRemoteDrive(drive) {
124
+ return (drive.state.local.availableOffline &&
125
+ drive.state.local.triggers.length > 0);
126
+ }
127
+ async startSyncRemoteDrive(driveId) {
128
+ let driveTriggers = this.triggerMap.get(driveId);
129
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
130
+ const drive = await this.getDrive(driveId);
131
+ for (const trigger of drive.state.local.triggers) {
132
+ if (driveTriggers?.get(trigger.id)) {
133
+ continue;
134
+ }
135
+ if (!driveTriggers) {
136
+ driveTriggers = new Map();
137
+ }
138
+ this.synchronizationManager.updateSyncStatus(driveId, {
139
+ pull: "SYNCING",
140
+ });
141
+ for (const syncUnit of syncUnits) {
142
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
143
+ pull: "SYNCING",
144
+ });
145
+ }
146
+ if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
147
+ let firstPull = true;
148
+ const cancelPullLoop = PullResponderTransmitter.setupPull(driveId, trigger, this.saveStrand.bind(this), (error) => {
149
+ const statusError = error instanceof OperationError ? error.status : "ERROR";
150
+ this.synchronizationManager.updateSyncStatus(driveId, { pull: statusError }, error);
151
+ if (error instanceof ClientError) {
152
+ this.eventEmitter.emit("clientStrandsError", driveId, trigger, error.response.status, error.message);
153
+ }
154
+ }, (revisions) => {
155
+ const errorRevision = revisions.filter((r) => r.status !== "SUCCESS");
156
+ if (errorRevision.length < 1) {
157
+ this.synchronizationManager.updateSyncStatus(driveId, {
158
+ pull: "SUCCESS",
159
+ });
160
+ }
161
+ const documentIdsFromRevision = revisions
162
+ .filter((rev) => rev.documentId !== "")
163
+ .map((rev) => rev.documentId);
164
+ this.getSynchronizationUnitsIds(driveId, documentIdsFromRevision)
165
+ .then((revSyncUnits) => {
166
+ for (const syncUnit of revSyncUnits) {
167
+ const fileErrorRevision = errorRevision.find((r) => r.documentId === syncUnit.documentId);
168
+ if (fileErrorRevision) {
169
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, { pull: fileErrorRevision.status }, fileErrorRevision.error);
170
+ }
171
+ else {
172
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
173
+ pull: "SUCCESS",
174
+ });
175
+ }
176
+ }
177
+ })
178
+ .catch(console.error);
179
+ // if it is the first pull and returns empty
180
+ // then updates corresponding push transmitter
181
+ if (firstPull) {
182
+ firstPull = false;
183
+ const pushListener = drive.state.local.listeners.find((listener) => trigger.data.url === listener.callInfo?.data);
184
+ if (pushListener) {
185
+ this.getSynchronizationUnitsRevision(driveId, syncUnits)
186
+ .then((syncUnitRevisions) => {
187
+ for (const revision of syncUnitRevisions) {
188
+ this.listenerManager
189
+ .updateListenerRevision(pushListener.listenerId, driveId, revision.syncId, revision.revision)
190
+ .catch(logger.error);
191
+ }
192
+ })
193
+ .catch(logger.error);
194
+ }
195
+ }
196
+ });
197
+ driveTriggers.set(trigger.id, cancelPullLoop);
198
+ this.triggerMap.set(driveId, driveTriggers);
199
+ }
200
+ }
201
+ }
202
+ async stopSyncRemoteDrive(driveId) {
203
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId);
204
+ const filesNodeSyncId = syncUnits
205
+ .filter((syncUnit) => syncUnit.documentId !== "")
206
+ .map((syncUnit) => syncUnit.syncId);
207
+ const triggers = this.triggerMap.get(driveId);
208
+ triggers?.forEach((cancel) => cancel());
209
+ this.synchronizationManager.updateSyncStatus(driveId, null);
210
+ for (const fileNodeSyncId of filesNodeSyncId) {
211
+ this.synchronizationManager.updateSyncStatus(fileNodeSyncId, null);
212
+ }
213
+ return this.triggerMap.delete(driveId);
214
+ }
215
+ defaultDrivesManagerDelegate = {
216
+ detachDrive: this.detachDrive.bind(this),
217
+ emit: (...args) => this.eventEmitter.emit("defaultRemoteDrive", ...args),
218
+ };
219
+ queueDelegate = {
220
+ checkDocumentExists: (driveId, documentId) => this.storage.checkDocumentExists(driveId, documentId),
221
+ processOperationJob: async ({ driveId, documentId, operations, options, }) => {
222
+ return documentId
223
+ ? this.addOperations(driveId, documentId, operations, options)
224
+ : this.addDriveOperations(driveId, operations, options);
225
+ },
226
+ processActionJob: async ({ driveId, documentId, actions, options, }) => {
227
+ return documentId
228
+ ? this.addActions(driveId, documentId, actions, options)
229
+ : this.addDriveActions(driveId, actions, options);
230
+ },
231
+ processJob: async (job) => {
232
+ if (isOperationJob(job)) {
233
+ return this.queueDelegate.processOperationJob(job);
234
+ }
235
+ else if (isActionJob(job)) {
236
+ return this.queueDelegate.processActionJob(job);
237
+ }
238
+ else {
239
+ throw new Error("Unknown job type", job);
240
+ }
241
+ },
242
+ };
243
+ async _initializeDrive(driveId) {
244
+ const drive = await this.getDrive(driveId);
245
+ await this.synchronizationManager.initializeDriveSyncStatus(driveId, drive);
246
+ if (this.shouldSyncRemoteDrive(drive)) {
247
+ await this.startSyncRemoteDrive(driveId);
248
+ }
249
+ for (const zodListener of drive.state.local.listeners) {
250
+ const transmitter = this.transmitterFactory.instance(zodListener.callInfo?.transmitterType ?? "", {
251
+ driveId,
252
+ listenerId: zodListener.listenerId,
253
+ block: zodListener.block,
254
+ filter: zodListener.filter,
255
+ system: zodListener.system,
256
+ label: zodListener.label ?? "",
257
+ callInfo: zodListener.callInfo ?? undefined,
258
+ }, this);
259
+ await this.listenerManager.setListener(driveId, {
260
+ block: zodListener.block,
261
+ driveId: drive.state.global.id,
262
+ filter: {
263
+ branch: zodListener.filter.branch ?? [],
264
+ documentId: zodListener.filter.documentId ?? [],
265
+ documentType: zodListener.filter.documentType,
266
+ scope: zodListener.filter.scope ?? [],
267
+ },
268
+ listenerId: zodListener.listenerId,
269
+ callInfo: zodListener.callInfo ?? undefined,
270
+ system: zodListener.system,
271
+ label: zodListener.label ?? "",
272
+ transmitter,
273
+ });
274
+ }
275
+ }
276
+ // Delegate synchronization methods to synchronizationManager
277
+ getSynchronizationUnits(driveId, documentId, scope, branch, documentType) {
278
+ return this.synchronizationManager.getSynchronizationUnits(driveId, documentId, scope, branch, documentType);
279
+ }
280
+ getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType) {
281
+ return this.synchronizationManager.getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType);
282
+ }
283
+ getOperationData(driveId, syncId, filter) {
284
+ return this.synchronizationManager.getOperationData(driveId, syncId, filter);
285
+ }
286
+ getSynchronizationUnitsRevision(driveId, syncUnitsQuery) {
287
+ return this.synchronizationManager.getSynchronizationUnitsRevision(driveId, syncUnitsQuery);
288
+ }
289
+ getDocumentModelModule(documentType) {
290
+ const documentModelModule = this.documentModelModules.find((module) => module.documentModel.id === documentType);
291
+ if (!documentModelModule) {
292
+ throw new Error(`Document type ${documentType} not supported`);
293
+ }
294
+ return documentModelModule;
295
+ }
296
+ getDocumentModelModules() {
297
+ return [...this.documentModelModules];
298
+ }
299
+ async addDrive(input, preferredEditor) {
300
+ const id = input.global.id || generateUUID();
301
+ if (!id) {
302
+ throw new Error("Invalid Drive Id");
303
+ }
304
+ const drives = await this.storage.getDrives();
305
+ if (drives.includes(id)) {
306
+ throw new DriveAlreadyExistsError(id);
307
+ }
308
+ const document = createDocument({
309
+ state: input,
310
+ });
311
+ document.meta = {
312
+ preferredEditor: preferredEditor,
313
+ };
314
+ await this.storage.createDrive(id, document);
315
+ if (input.global.slug) {
316
+ await this.cache.deleteDocument("drives-slug", input.global.slug);
317
+ }
318
+ await this._initializeDrive(id);
319
+ this.eventEmitter.emit("driveAdded", document);
320
+ return document;
321
+ }
322
+ async addRemoteDrive(url, options) {
323
+ const { id, name, slug, icon, meta } = options.expectedDriveInfo || (await requestPublicDrive(url));
324
+ const { pullFilter, pullInterval, availableOffline, sharingType, listeners, triggers, } = options;
325
+ const pullTrigger = await PullResponderTransmitter.createPullResponderTrigger(id, url, {
326
+ pullFilter,
327
+ pullInterval,
328
+ });
329
+ return await this.addDrive({
330
+ global: {
331
+ id: id,
332
+ name,
333
+ slug,
334
+ icon: icon ?? null,
335
+ },
336
+ local: {
337
+ triggers: [...triggers, pullTrigger],
338
+ listeners: listeners,
339
+ availableOffline,
340
+ sharingType,
341
+ },
342
+ }, meta?.preferredEditor);
343
+ }
344
+ async registerPullResponderTrigger(driveId, url, options) {
345
+ const pullTrigger = await PullResponderTransmitter.createPullResponderTrigger(driveId, url, options);
346
+ return pullTrigger;
347
+ }
348
+ async deleteDrive(driveId) {
349
+ const result = await Promise.allSettled([
350
+ this.stopSyncRemoteDrive(driveId),
351
+ this.listenerManager.removeDrive(driveId),
352
+ this.cache.deleteDocument("drives", driveId),
353
+ this.storage.deleteDrive(driveId),
354
+ ]);
355
+ result.forEach((r) => {
356
+ if (r.status === "rejected") {
357
+ throw r.reason;
358
+ }
359
+ });
360
+ }
361
+ getDrives() {
362
+ return this.storage.getDrives();
363
+ }
364
+ async getDrive(driveId, options) {
365
+ let document;
366
+ try {
367
+ const cachedDocument = await this.cache.getDocument("drives", driveId); // TODO support GetDocumentOptions
368
+ if (cachedDocument && isDocumentDrive(cachedDocument)) {
369
+ document = cachedDocument;
370
+ if (isAtRevision(document, options?.revisions)) {
371
+ return document;
372
+ }
373
+ }
374
+ }
375
+ catch (e) {
376
+ logger.error("Error getting drive from cache", e);
377
+ }
378
+ const driveStorage = document ?? (await this.storage.getDrive(driveId));
379
+ const result = this._buildDocument(driveStorage, options);
380
+ if (!isDocumentDrive(result)) {
381
+ throw new Error(`Document with id ${driveId} is not a Document Drive`);
382
+ }
383
+ else {
384
+ if (!options?.revisions) {
385
+ this.cache.setDocument("drives", driveId, result).catch(logger.error);
386
+ }
387
+ return result;
388
+ }
389
+ }
390
+ async getDriveBySlug(slug, options) {
391
+ try {
392
+ const document = await this.cache.getDocument("drives-slug", slug);
393
+ if (document && isDocumentDrive(document)) {
394
+ return document;
395
+ }
396
+ }
397
+ catch (e) {
398
+ logger.error("Error getting drive from cache", e);
399
+ }
400
+ const driveStorage = await this.storage.getDriveBySlug(slug);
401
+ const document = this._buildDocument(driveStorage, options);
402
+ if (!isDocumentDrive(document)) {
403
+ throw new Error(`Document with slug ${slug} is not a Document Drive`);
404
+ }
405
+ else {
406
+ this.cache.setDocument("drives-slug", slug, document).catch(logger.error);
407
+ return document;
408
+ }
409
+ }
410
+ async getDocument(driveId, documentId, options) {
411
+ let cachedDocument;
412
+ try {
413
+ cachedDocument = await this.cache.getDocument(driveId, documentId); // TODO support GetDocumentOptions
414
+ if (cachedDocument && isAtRevision(cachedDocument, options?.revisions)) {
415
+ return cachedDocument;
416
+ }
417
+ }
418
+ catch (e) {
419
+ logger.error("Error getting document from cache", e);
420
+ }
421
+ const documentStorage = cachedDocument ??
422
+ (await this.storage.getDocument(driveId, documentId));
423
+ const document = this._buildDocument(documentStorage, options);
424
+ if (!options?.revisions) {
425
+ this.cache.setDocument(driveId, documentId, document).catch(logger.error);
426
+ }
427
+ return document;
428
+ }
429
+ getDocuments(driveId) {
430
+ return this.storage.getDocuments(driveId);
431
+ }
432
+ async createDocument(driveId, input) {
433
+ // if a document was provided then checks if it's valid
434
+ let state = undefined;
435
+ if (input.document) {
436
+ if (input.documentType !== input.document.documentType) {
437
+ throw new Error(`Provided document is not ${input.documentType}`);
438
+ }
439
+ const doc = this._buildDocument(input.document);
440
+ state = doc.state;
441
+ }
442
+ // if no document was provided then create a new one
443
+ const document = input.document ??
444
+ this.getDocumentModelModule(input.documentType).utils.createDocument();
445
+ // stores document information
446
+ const documentStorage = {
447
+ name: document.name,
448
+ revision: document.revision,
449
+ documentType: document.documentType,
450
+ created: document.created,
451
+ lastModified: document.lastModified,
452
+ operations: { global: [], local: [] },
453
+ initialState: document.initialState,
454
+ clipboard: [],
455
+ state: state ?? document.state,
456
+ };
457
+ await this.storage.createDocument(driveId, input.id, documentStorage);
458
+ // set initial state for new syncUnits
459
+ for (const syncUnit of input.synchronizationUnits) {
460
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
461
+ pull: this.triggerMap.get(driveId) ? "INITIAL_SYNC" : undefined,
462
+ push: this.listenerManager.driveHasListeners(driveId)
463
+ ? "SUCCESS"
464
+ : undefined,
465
+ });
466
+ }
467
+ // if the document contains operations then
468
+ // stores the operations in the storage
469
+ const operations = Object.values(document.operations).flat();
470
+ if (operations.length) {
471
+ if (isDocumentDrive(document)) {
472
+ await this.storage.addDriveOperations(driveId, operations, document);
473
+ }
474
+ else {
475
+ await this.storage.addDocumentOperations(driveId, input.id, operations, document);
476
+ }
477
+ }
478
+ return document;
479
+ }
480
+ async deleteDocument(driveId, documentId) {
481
+ try {
482
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
483
+ documentId,
484
+ ]);
485
+ // remove document sync units status when a document is deleted
486
+ for (const syncUnit of syncUnits) {
487
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, null);
488
+ }
489
+ await this.listenerManager.removeSyncUnits(driveId, syncUnits);
490
+ }
491
+ catch (error) {
492
+ logger.warn("Error deleting document", error);
493
+ }
494
+ await this.cache.deleteDocument(driveId, documentId);
495
+ return this.storage.deleteDocument(driveId, documentId);
496
+ }
497
+ async _processOperations(driveId, documentId, documentStorage, operations) {
498
+ const operationsApplied = [];
499
+ const signals = [];
500
+ const documentStorageWithState = await this._addDocumentResultingStage(documentStorage, driveId, documentId);
501
+ let document = this._buildDocument(documentStorageWithState);
502
+ let error; // TODO: replace with an array of errors/consistency issues
503
+ const operationsByScope = groupOperationsByScope(operations);
504
+ for (const scope of Object.keys(operationsByScope)) {
505
+ const storageDocumentOperations = documentStorage.operations[scope];
506
+ // TODO two equal operations done by two clients will be considered the same, ie: { type: "INCREMENT" }
507
+ const branch = removeExistingOperations(operationsByScope[scope] || [], storageDocumentOperations);
508
+ // No operations to apply
509
+ if (branch.length < 1) {
510
+ continue;
511
+ }
512
+ const trunk = garbageCollect(sortOperations(storageDocumentOperations));
513
+ const [invertedTrunk, tail] = attachBranch(trunk, branch);
514
+ const newHistory = tail.length < 1
515
+ ? invertedTrunk
516
+ : merge(trunk, invertedTrunk, reshuffleByTimestamp);
517
+ const newOperations = newHistory.filter((op) => trunk.length < 1 || precedes(trunk[trunk.length - 1], op));
518
+ for (const nextOperation of newOperations) {
519
+ let skipHashValidation = false;
520
+ // when dealing with a merge (tail.length > 0) we have to skip hash validation
521
+ // for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
522
+ if (tail.length > 0) {
523
+ const sourceOperation = operations.find((op) => op.hash === nextOperation.hash);
524
+ skipHashValidation =
525
+ !sourceOperation ||
526
+ sourceOperation.index !== nextOperation.index ||
527
+ sourceOperation.skip !== nextOperation.skip;
528
+ }
529
+ try {
530
+ // runs operation on next available tick, to avoid blocking the main thread
531
+ const taskQueueMethod = this.options.taskQueueMethod;
532
+ const task = () => this._performOperation(driveId, documentId, document, nextOperation, skipHashValidation);
533
+ const appliedResult = await (taskQueueMethod
534
+ ? runAsapAsync(task, taskQueueMethod)
535
+ : task());
536
+ document = appliedResult.document;
537
+ signals.push(...appliedResult.signals);
538
+ operationsApplied.push(appliedResult.operation);
539
+ // TODO what to do if one of the applied operations has an error?
540
+ }
541
+ catch (e) {
542
+ error =
543
+ e instanceof OperationError
544
+ ? e
545
+ : new OperationError("ERROR", nextOperation, e.message, e.cause);
546
+ // TODO: don't break on errors...
547
+ break;
548
+ }
549
+ }
550
+ }
551
+ return {
552
+ document,
553
+ operationsApplied,
554
+ signals,
555
+ error,
556
+ };
557
+ }
558
+ async _addDocumentResultingStage(document, driveId, documentId, options) {
559
+ // apply skip header operations to all scopes
560
+ const operations = options?.revisions !== undefined
561
+ ? filterOperationsByRevision(document.operations, options.revisions)
562
+ : document.operations;
563
+ const documentOperations = garbageCollectDocumentOperations(operations);
564
+ for (const scope of Object.keys(documentOperations)) {
565
+ const lastRemainingOperation = documentOperations[scope].at(-1);
566
+ // if the latest operation doesn't have a resulting state then tries
567
+ // to retrieve it from the db to avoid rerunning all the operations
568
+ if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
569
+ lastRemainingOperation.resultingState = await (documentId
570
+ ? this.storage.getOperationResultingState?.(driveId, documentId, lastRemainingOperation.index, lastRemainingOperation.scope, "main")
571
+ : this.storage.getDriveOperationResultingState?.(driveId, lastRemainingOperation.index, lastRemainingOperation.scope, "main"));
572
+ }
573
+ }
574
+ return {
575
+ ...document,
576
+ operations: documentOperations,
577
+ };
578
+ }
579
+ _buildDocument(documentStorage, options) {
580
+ if (documentStorage.state &&
581
+ (!options || options.checkHashes === false) &&
582
+ isAtRevision(documentStorage, options?.revisions)) {
583
+ return documentStorage;
584
+ }
585
+ const documentModelModule = this.getDocumentModelModule(documentStorage.documentType);
586
+ const revisionOperations = options?.revisions !== undefined
587
+ ? filterOperationsByRevision(documentStorage.operations, options.revisions)
588
+ : documentStorage.operations;
589
+ const operations = garbageCollectDocumentOperations(revisionOperations);
590
+ return replayDocument(documentStorage.initialState, operations, documentModelModule.reducer, undefined, documentStorage, undefined, {
591
+ ...options,
592
+ checkHashes: options?.checkHashes ?? true,
593
+ reuseOperationResultingState: options?.checkHashes ?? true,
594
+ });
595
+ }
596
+ async _performOperation(driveId, documentId, document, operation, skipHashValidation = false) {
597
+ const documentModelModule = this.getDocumentModelModule(document.documentType);
598
+ const signalResults = [];
599
+ let newDocument = document;
600
+ const scope = operation.scope;
601
+ const documentOperations = garbageCollectDocumentOperations({
602
+ ...document.operations,
603
+ [scope]: skipHeaderOperations(document.operations[scope], operation),
604
+ });
605
+ const lastRemainingOperation = documentOperations[scope].at(-1);
606
+ // if the latest operation doesn't have a resulting state then tries
607
+ // to retrieve it from the db to avoid rerunning all the operations
608
+ if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
609
+ lastRemainingOperation.resultingState = await (documentId
610
+ ? this.storage.getOperationResultingState?.(driveId, documentId, lastRemainingOperation.index, lastRemainingOperation.scope, "main")
611
+ : this.storage.getDriveOperationResultingState?.(driveId, lastRemainingOperation.index, lastRemainingOperation.scope, "main"));
612
+ }
613
+ const operationSignals = [];
614
+ newDocument = documentModelModule.reducer(newDocument, operation, (signal) => {
615
+ let handler = undefined;
616
+ switch (signal.type) {
617
+ case "CREATE_CHILD_DOCUMENT":
618
+ handler = () => this.createDocument(driveId, signal.input);
619
+ break;
620
+ case "DELETE_CHILD_DOCUMENT":
621
+ handler = () => this.deleteDocument(driveId, signal.input.id);
622
+ break;
623
+ case "COPY_CHILD_DOCUMENT":
624
+ handler = () => this.getDocument(driveId, signal.input.id).then((documentToCopy) => this.createDocument(driveId, {
625
+ id: signal.input.newId,
626
+ documentType: documentToCopy.documentType,
627
+ document: documentToCopy,
628
+ synchronizationUnits: signal.input.synchronizationUnits,
629
+ }));
630
+ break;
631
+ }
632
+ if (handler) {
633
+ operationSignals.push(() => handler().then((result) => ({ signal, result })));
634
+ }
635
+ }, { skip: operation.skip, reuseOperationResultingState: true });
636
+ const appliedOperations = newDocument.operations[operation.scope].filter((op) => op.index == operation.index && op.skip == operation.skip);
637
+ const appliedOperation = appliedOperations.at(0);
638
+ if (!appliedOperation) {
639
+ throw new OperationError("ERROR", operation, `Operation with index ${operation.index}:${operation.skip} was not applied.`);
640
+ }
641
+ if (!appliedOperation.error &&
642
+ appliedOperation.hash !== operation.hash &&
643
+ !skipHashValidation) {
644
+ throw new ConflictOperationError(operation, appliedOperation);
645
+ }
646
+ for (const signalHandler of operationSignals) {
647
+ const result = await signalHandler();
648
+ signalResults.push(result);
649
+ }
650
+ return {
651
+ document: newDocument,
652
+ signals: signalResults,
653
+ operation: appliedOperation,
654
+ };
655
+ }
656
+ addOperation(driveId, documentId, operation, options) {
657
+ return this.addOperations(driveId, documentId, [operation], options);
658
+ }
659
+ async _addOperations(driveId, documentId, callback) {
660
+ if (!this.storage.addDocumentOperationsWithTransaction) {
661
+ const documentStorage = await this.storage.getDocument(driveId, documentId);
662
+ const result = await callback(documentStorage);
663
+ // saves the applied operations to storage
664
+ if (result.operations.length > 0) {
665
+ await this.storage.addDocumentOperations(driveId, documentId, result.operations, result.header);
666
+ }
667
+ }
668
+ else {
669
+ await this.storage.addDocumentOperationsWithTransaction(driveId, documentId, callback);
670
+ }
671
+ }
672
+ queueOperation(driveId, documentId, operation, options) {
673
+ return this.queueOperations(driveId, documentId, [operation], options);
674
+ }
675
+ async resultIfExistingOperations(drive, id, operations) {
676
+ try {
677
+ const document = await this.getDocument(drive, id);
678
+ const newOperation = operations.find((op) => !op.id ||
679
+ !document.operations[op.scope].find((existingOp) => existingOp.id === op.id &&
680
+ existingOp.index === op.index &&
681
+ existingOp.type === op.type &&
682
+ existingOp.hash === op.hash));
683
+ if (!newOperation) {
684
+ return {
685
+ status: "SUCCESS",
686
+ document,
687
+ operations,
688
+ signals: [],
689
+ };
690
+ }
691
+ else {
692
+ return undefined;
693
+ }
694
+ }
695
+ catch (error) {
696
+ if (!error.message.includes(`Document with id ${id} not found`)) {
697
+ console.error(error);
698
+ }
699
+ return undefined;
700
+ }
701
+ }
702
+ async queueOperations(driveId, documentId, operations, options) {
703
+ // if operations are already stored then returns cached document
704
+ const result = await this.resultIfExistingOperations(driveId, documentId, operations);
705
+ if (result) {
706
+ return result;
707
+ }
708
+ try {
709
+ const jobId = await this.queueManager.addJob({
710
+ driveId: driveId,
711
+ documentId: documentId,
712
+ operations,
713
+ options,
714
+ });
715
+ return new Promise((resolve, reject) => {
716
+ const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
717
+ if (job.jobId === jobId) {
718
+ unsubscribe();
719
+ unsubscribeError();
720
+ resolve(result);
721
+ }
722
+ });
723
+ const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
724
+ if (job.jobId === jobId) {
725
+ unsubscribe();
726
+ unsubscribeError();
727
+ reject(error);
728
+ }
729
+ });
730
+ });
731
+ }
732
+ catch (error) {
733
+ logger.error("Error adding job", error);
734
+ throw error;
735
+ }
736
+ }
737
+ async queueAction(driveId, documentId, action, options) {
738
+ return this.queueActions(driveId, documentId, [action], options);
739
+ }
740
+ async queueActions(driveId, documentId, actions, options) {
741
+ try {
742
+ const jobId = await this.queueManager.addJob({
743
+ driveId: driveId,
744
+ documentId: documentId,
745
+ actions,
746
+ options,
747
+ });
748
+ return new Promise((resolve, reject) => {
749
+ const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
750
+ if (job.jobId === jobId) {
751
+ unsubscribe();
752
+ unsubscribeError();
753
+ resolve(result);
754
+ }
755
+ });
756
+ const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
757
+ if (job.jobId === jobId) {
758
+ unsubscribe();
759
+ unsubscribeError();
760
+ reject(error);
761
+ }
762
+ });
763
+ });
764
+ }
765
+ catch (error) {
766
+ logger.error("Error adding job", error);
767
+ throw error;
768
+ }
769
+ }
770
+ async queueDriveAction(driveId, action, options) {
771
+ return this.queueDriveActions(driveId, [action], options);
772
+ }
773
+ async queueDriveActions(driveId, actions, options) {
774
+ try {
775
+ const jobId = await this.queueManager.addJob({
776
+ driveId: driveId,
777
+ actions,
778
+ options,
779
+ });
780
+ return new Promise((resolve, reject) => {
781
+ const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
782
+ if (job.jobId === jobId) {
783
+ unsubscribe();
784
+ unsubscribeError();
785
+ resolve(result);
786
+ }
787
+ });
788
+ const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
789
+ if (job.jobId === jobId) {
790
+ unsubscribe();
791
+ unsubscribeError();
792
+ reject(error);
793
+ }
794
+ });
795
+ });
796
+ }
797
+ catch (error) {
798
+ logger.error("Error adding drive job", error);
799
+ throw error;
800
+ }
801
+ }
802
+ async addOperations(driveId, documentId, operations, options) {
803
+ // if operations are already stored then returns the result
804
+ const result = await this.resultIfExistingOperations(driveId, documentId, operations);
805
+ if (result) {
806
+ return result;
807
+ }
808
+ let document;
809
+ const operationsApplied = [];
810
+ const signals = [];
811
+ let error;
812
+ try {
813
+ await this._addOperations(driveId, documentId, async (documentStorage) => {
814
+ const result = await this._processOperations(driveId, documentId, documentStorage, operations);
815
+ if (!result.document) {
816
+ logger.error("Invalid document");
817
+ throw result.error ?? new Error("Invalid document");
818
+ }
819
+ document = result.document;
820
+ error = result.error;
821
+ signals.push(...result.signals);
822
+ operationsApplied.push(...result.operationsApplied);
823
+ return {
824
+ operations: result.operationsApplied,
825
+ header: result.document,
826
+ newState: document.state,
827
+ };
828
+ });
829
+ if (document) {
830
+ this.cache
831
+ .setDocument(driveId, documentId, document)
832
+ .catch(logger.error);
833
+ }
834
+ // gets all the different scopes and branches combinations from the operations
835
+ const { scopes, branches } = operationsApplied.reduce((acc, operation) => {
836
+ if (!acc.scopes.includes(operation.scope)) {
837
+ acc.scopes.push(operation.scope);
838
+ }
839
+ return acc;
840
+ }, { scopes: [], branches: ["main"] });
841
+ const syncUnits = await this.getSynchronizationUnits(driveId, [documentId], scopes, branches);
842
+ // checks if any of the provided operations where reshufled
843
+ const newOp = operationsApplied.find((appliedOp) => !operations.find((o) => o.id === appliedOp.id &&
844
+ o.index === appliedOp.index &&
845
+ o.skip === appliedOp.skip &&
846
+ o.hash === appliedOp.hash));
847
+ // if there are no new operations then reuses the provided source
848
+ // otherwise sets it to local so listeners know that there were
849
+ // new changes originating from this document drive server
850
+ const source = newOp
851
+ ? { type: "local" }
852
+ : (options?.source ?? { type: "local" });
853
+ // update listener cache
854
+ const operationSource = this.getOperationSource(source);
855
+ this.listenerManager
856
+ .updateSynchronizationRevisions(driveId, syncUnits, source, () => {
857
+ this.synchronizationManager.updateSyncStatus(driveId, {
858
+ [operationSource]: "SYNCING",
859
+ });
860
+ for (const syncUnit of syncUnits) {
861
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
862
+ [operationSource]: "SYNCING",
863
+ });
864
+ }
865
+ }, this.handleListenerError.bind(this), options?.forceSync ?? source.type === "local")
866
+ .then((updates) => {
867
+ if (updates.length) {
868
+ this.synchronizationManager.updateSyncStatus(driveId, {
869
+ [operationSource]: "SUCCESS",
870
+ });
871
+ }
872
+ for (const syncUnit of syncUnits) {
873
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
874
+ [operationSource]: "SUCCESS",
875
+ });
876
+ }
877
+ })
878
+ .catch((error) => {
879
+ logger.error("Non handled error updating sync revision", error);
880
+ this.synchronizationManager.updateSyncStatus(driveId, {
881
+ [operationSource]: "ERROR",
882
+ }, error);
883
+ for (const syncUnit of syncUnits) {
884
+ this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
885
+ [operationSource]: "ERROR",
886
+ }, error);
887
+ }
888
+ });
889
+ // after applying all the valid operations,throws
890
+ // an error if there was an invalid operation
891
+ if (error) {
892
+ throw error;
893
+ }
894
+ return {
895
+ status: "SUCCESS",
896
+ document,
897
+ operations: operationsApplied,
898
+ signals,
899
+ };
900
+ }
901
+ catch (error) {
902
+ const operationError = error instanceof OperationError
903
+ ? error
904
+ : new OperationError("ERROR", undefined, error.message, error.cause);
905
+ return {
906
+ status: operationError.status,
907
+ error: operationError,
908
+ document,
909
+ operations: operationsApplied,
910
+ signals,
911
+ };
912
+ }
913
+ }
914
+ addDriveOperation(driveId, operation, options) {
915
+ return this.addDriveOperations(driveId, [operation], options);
916
+ }
917
+ async clearStorage() {
918
+ for (const drive of await this.getDrives()) {
919
+ await this.deleteDrive(drive);
920
+ }
921
+ await this.storage.clearStorage?.();
922
+ }
923
+ async _addDriveOperations(driveId, callback) {
924
+ if (!this.storage.addDriveOperationsWithTransaction) {
925
+ const documentStorage = await this.storage.getDrive(driveId);
926
+ const result = await callback(documentStorage);
927
+ // saves the applied operations to storage
928
+ if (result.operations.length > 0) {
929
+ await this.storage.addDriveOperations(driveId, result.operations, result.header);
930
+ }
931
+ return result;
932
+ }
933
+ else {
934
+ return this.storage.addDriveOperationsWithTransaction(driveId, callback);
935
+ }
936
+ }
937
+ queueDriveOperation(driveId, operation, options) {
938
+ return this.queueDriveOperations(driveId, [operation], options);
939
+ }
940
+ async resultIfExistingDriveOperations(driveId, operations) {
941
+ try {
942
+ const drive = await this.getDrive(driveId);
943
+ const newOperation = operations.find((op) => !op.id ||
944
+ !drive.operations[op.scope].find((existingOp) => existingOp.id === op.id &&
945
+ existingOp.index === op.index &&
946
+ existingOp.type === op.type &&
947
+ existingOp.hash === op.hash));
948
+ if (!newOperation) {
949
+ return {
950
+ status: "SUCCESS",
951
+ document: drive,
952
+ operations: operations,
953
+ signals: [],
954
+ };
955
+ }
956
+ else {
957
+ return undefined;
958
+ }
959
+ }
960
+ catch (error) {
961
+ console.error(error); // TODO error
962
+ return undefined;
963
+ }
964
+ }
965
+ async queueDriveOperations(driveId, operations, options) {
966
+ // if operations are already stored then returns cached document
967
+ const result = await this.resultIfExistingDriveOperations(driveId, operations);
968
+ if (result) {
969
+ return result;
970
+ }
971
+ try {
972
+ const jobId = await this.queueManager.addJob({
973
+ driveId: driveId,
974
+ operations,
975
+ options,
976
+ });
977
+ return new Promise((resolve, reject) => {
978
+ const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
979
+ if (job.jobId === jobId) {
980
+ unsubscribe();
981
+ unsubscribeError();
982
+ resolve(result);
983
+ }
984
+ });
985
+ const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
986
+ if (job.jobId === jobId) {
987
+ unsubscribe();
988
+ unsubscribeError();
989
+ reject(error);
990
+ }
991
+ });
992
+ });
993
+ }
994
+ catch (error) {
995
+ logger.error("Error adding drive job", error);
996
+ throw error;
997
+ }
998
+ }
999
+ async addDriveOperations(driveId, operations, options) {
1000
+ let document;
1001
+ const operationsApplied = [];
1002
+ const signals = [];
1003
+ let error;
1004
+ // if operations are already stored then returns cached drive
1005
+ const result = await this.resultIfExistingDriveOperations(driveId, operations);
1006
+ if (result) {
1007
+ return result;
1008
+ }
1009
+ try {
1010
+ await this._addDriveOperations(driveId, async (documentStorage) => {
1011
+ const result = await this._processOperations(driveId, undefined, documentStorage, operations.slice());
1012
+ document = result.document;
1013
+ operationsApplied.push(...result.operationsApplied);
1014
+ signals.push(...result.signals);
1015
+ error = result.error;
1016
+ return {
1017
+ operations: result.operationsApplied,
1018
+ header: result.document,
1019
+ };
1020
+ });
1021
+ if (!document || !isDocumentDrive(document)) {
1022
+ throw error ?? new Error("Invalid Document Drive document");
1023
+ }
1024
+ this.cache.setDocument("drives", driveId, document).catch(logger.error);
1025
+ for (const operation of operationsApplied) {
1026
+ switch (operation.type) {
1027
+ case "ADD_LISTENER": {
1028
+ const zodListener = operation.input.listener;
1029
+ // create the transmitter
1030
+ const transmitter = this.transmitterFactory.instance(zodListener.callInfo?.transmitterType ?? "", {
1031
+ driveId,
1032
+ listenerId: zodListener.listenerId,
1033
+ block: zodListener.block,
1034
+ filter: zodListener.filter,
1035
+ system: zodListener.system,
1036
+ label: zodListener.label ?? "",
1037
+ callInfo: zodListener.callInfo ?? undefined,
1038
+ }, this);
1039
+ // create the listener
1040
+ const listener = {
1041
+ ...zodListener,
1042
+ driveId: driveId,
1043
+ label: zodListener.label ?? "",
1044
+ system: zodListener.system ?? false,
1045
+ filter: {
1046
+ branch: zodListener.filter.branch ?? [],
1047
+ documentId: zodListener.filter.documentId ?? [],
1048
+ documentType: zodListener.filter.documentType ?? [],
1049
+ scope: zodListener.filter.scope ?? [],
1050
+ },
1051
+ callInfo: {
1052
+ data: zodListener.callInfo?.data ?? "",
1053
+ name: zodListener.callInfo?.name ?? "PullResponder",
1054
+ transmitterType: zodListener.callInfo?.transmitterType ?? "PullResponder",
1055
+ },
1056
+ transmitter,
1057
+ };
1058
+ await this.addListener(driveId, listener);
1059
+ break;
1060
+ }
1061
+ case "REMOVE_LISTENER": {
1062
+ await this.removeListener(driveId, operation);
1063
+ break;
1064
+ }
1065
+ }
1066
+ }
1067
+ // update listener cache
1068
+ const lastOperation = operationsApplied
1069
+ .filter((op) => op.scope === "global")
1070
+ .slice()
1071
+ .pop();
1072
+ if (lastOperation) {
1073
+ // checks if any of the provided operations where reshufled
1074
+ const newOp = operationsApplied.find((appliedOp) => !operations.find((o) => o.id === appliedOp.id &&
1075
+ o.index === appliedOp.index &&
1076
+ o.skip === appliedOp.skip &&
1077
+ o.hash === appliedOp.hash));
1078
+ // if there are no new operations then reuses the provided source
1079
+ // otherwise sets it to local so listeners know that there were
1080
+ // new changes originating from this document drive server
1081
+ const source = newOp
1082
+ ? { type: "local" }
1083
+ : (options?.source ?? { type: "local" });
1084
+ const operationSource = this.getOperationSource(source);
1085
+ this.listenerManager
1086
+ .updateSynchronizationRevisions(driveId, [
1087
+ {
1088
+ syncId: "0",
1089
+ driveId: driveId,
1090
+ documentId: "",
1091
+ scope: "global",
1092
+ branch: "main",
1093
+ documentType: "powerhouse/document-drive",
1094
+ lastUpdated: lastOperation.timestamp,
1095
+ revision: lastOperation.index,
1096
+ },
1097
+ ], source, () => {
1098
+ this.synchronizationManager.updateSyncStatus(driveId, {
1099
+ [operationSource]: "SYNCING",
1100
+ });
1101
+ }, this.handleListenerError.bind(this), options?.forceSync ?? source.type === "local")
1102
+ .then((updates) => {
1103
+ if (updates.length) {
1104
+ this.synchronizationManager.updateSyncStatus(driveId, {
1105
+ [operationSource]: "SUCCESS",
1106
+ });
1107
+ }
1108
+ })
1109
+ .catch((error) => {
1110
+ logger.error("Non handled error updating sync revision", error);
1111
+ this.synchronizationManager.updateSyncStatus(driveId, {
1112
+ [operationSource]: "ERROR",
1113
+ }, error);
1114
+ });
1115
+ }
1116
+ if (this.shouldSyncRemoteDrive(document)) {
1117
+ this.startSyncRemoteDrive(driveId);
1118
+ }
1119
+ else {
1120
+ this.stopSyncRemoteDrive(driveId);
1121
+ }
1122
+ // after applying all the valid operations,throws
1123
+ // an error if there was an invalid operation
1124
+ if (error) {
1125
+ throw error;
1126
+ }
1127
+ return {
1128
+ status: "SUCCESS",
1129
+ document,
1130
+ operations: operationsApplied,
1131
+ signals,
1132
+ };
1133
+ }
1134
+ catch (error) {
1135
+ const operationError = error instanceof OperationError
1136
+ ? error
1137
+ : new OperationError("ERROR", undefined, error.message, error.cause);
1138
+ return {
1139
+ status: operationError.status,
1140
+ error: operationError,
1141
+ document,
1142
+ operations: operationsApplied,
1143
+ signals,
1144
+ };
1145
+ }
1146
+ }
1147
+ _buildOperations(documentId, actions) {
1148
+ const operations = [];
1149
+ const { reducer } = this.getDocumentModelModule(documentId.documentType);
1150
+ for (const action of actions) {
1151
+ documentId = reducer(documentId, action);
1152
+ const operation = documentId.operations[action.scope].slice().pop();
1153
+ if (!operation) {
1154
+ throw new Error("Error creating operations");
1155
+ }
1156
+ operations.push(operation);
1157
+ }
1158
+ return operations;
1159
+ }
1160
+ async addAction(driveId, documentId, action, options) {
1161
+ return this.addActions(driveId, documentId, [action], options);
1162
+ }
1163
+ async addActions(driveId, documentId, actions, options) {
1164
+ const document = await this.getDocument(driveId, documentId);
1165
+ const operations = this._buildOperations(document, actions);
1166
+ return this.addOperations(driveId, documentId, operations, options);
1167
+ }
1168
+ async addDriveAction(driveId, action, options) {
1169
+ return this.addDriveActions(driveId, [action], options);
1170
+ }
1171
+ async addDriveActions(driveId, actions, options) {
1172
+ const document = await this.getDrive(driveId);
1173
+ const operations = this._buildOperations(document, actions);
1174
+ const result = await this.addDriveOperations(driveId, operations, options);
1175
+ return result;
1176
+ }
1177
+ async detachDrive(driveId) {
1178
+ const documentDrive = await this.getDrive(driveId);
1179
+ const listeners = documentDrive.state.local.listeners || [];
1180
+ const triggers = documentDrive.state.local.triggers || [];
1181
+ for (const listener of listeners) {
1182
+ await this.addDriveAction(driveId, removeListener({ listenerId: listener.listenerId }));
1183
+ }
1184
+ for (const trigger of triggers) {
1185
+ await this.addDriveAction(driveId, removeTrigger({ triggerId: trigger.id }));
1186
+ }
1187
+ await this.addDriveAction(driveId, setSharingType({ type: "LOCAL" }));
1188
+ }
1189
+ async addListener(driveId, listener) {
1190
+ await this.listenerManager.setListener(driveId, listener);
1191
+ }
1192
+ async addInternalListener(driveId, receiver, options) {
1193
+ const listener = {
1194
+ callInfo: {
1195
+ data: "",
1196
+ name: "Interal",
1197
+ transmitterType: "Internal",
1198
+ },
1199
+ system: true,
1200
+ ...options,
1201
+ };
1202
+ await this.addDriveAction(driveId, addListener({ listener }));
1203
+ const transmitter = await this.getTransmitter(driveId, options.listenerId);
1204
+ if (!transmitter) {
1205
+ logger.error("Internal listener not found");
1206
+ throw new Error("Internal listener not found");
1207
+ }
1208
+ if (!(transmitter instanceof InternalTransmitter)) {
1209
+ logger.error("Listener is not an internal transmitter");
1210
+ throw new Error("Listener is not an internal transmitter");
1211
+ }
1212
+ transmitter.setReceiver(receiver);
1213
+ return transmitter;
1214
+ }
1215
+ async removeListener(driveId, operation) {
1216
+ const { listenerId } = operation.input;
1217
+ await this.listenerManager.removeListener(driveId, listenerId);
1218
+ }
1219
+ async getTransmitter(driveId, listenerId) {
1220
+ const listener = this.listenerManager.getListenerState(driveId, listenerId);
1221
+ return listener.listener.transmitter;
1222
+ }
1223
+ getListener(driveId, listenerId) {
1224
+ let listenerState;
1225
+ try {
1226
+ listenerState = this.listenerManager.getListenerState(driveId, listenerId);
1227
+ }
1228
+ catch {
1229
+ return Promise.resolve(undefined);
1230
+ }
1231
+ return Promise.resolve(listenerState);
1232
+ }
1233
+ getSyncStatus(syncUnitId) {
1234
+ return this.synchronizationManager.getSyncStatus(syncUnitId);
1235
+ }
1236
+ on(event, cb) {
1237
+ return this.eventEmitter.on(event, cb);
1238
+ }
1239
+ emit(event, ...args) {
1240
+ return this.eventEmitter.emit(event, ...args);
1241
+ }
1242
+ getSynchronizationUnit(driveId, syncId) {
1243
+ return this.synchronizationManager.getSynchronizationUnit(driveId, syncId);
1244
+ }
1245
+ // Add delegated methods to properly implement ISynchronizationManager
1246
+ updateSyncStatus(syncUnitId, status, error) {
1247
+ this.synchronizationManager.updateSyncStatus(syncUnitId, status, error);
1248
+ }
1249
+ initializeDriveSyncStatus(driveId, drive) {
1250
+ return this.synchronizationManager.initializeDriveSyncStatus(driveId, drive);
1251
+ }
1252
+ getCombinedSyncUnitStatus(syncUnitStatus) {
1253
+ return this.synchronizationManager.getCombinedSyncUnitStatus(syncUnitStatus);
1254
+ }
1255
+ // Add back the saveStrand method that was accidentally removed
1256
+ async saveStrand(strand, source) {
1257
+ const operations = strand.operations.map((op) => ({
1258
+ ...op,
1259
+ scope: strand.scope,
1260
+ branch: strand.branch,
1261
+ }));
1262
+ const result = await (!strand.documentId
1263
+ ? this.queueDriveOperations(strand.driveId, operations, { source })
1264
+ : this.queueOperations(strand.driveId, strand.documentId, operations, {
1265
+ source,
1266
+ }));
1267
+ if (result.status === "ERROR") {
1268
+ const syncUnits = strand.documentId !== ""
1269
+ ? (await this.getSynchronizationUnitsIds(strand.driveId, [strand.documentId], [strand.scope], [strand.branch])).map((s) => s.syncId)
1270
+ : [strand.driveId];
1271
+ const operationSource = this.getOperationSource(source);
1272
+ for (const syncUnit of syncUnits) {
1273
+ this.synchronizationManager.updateSyncStatus(syncUnit, { [operationSource]: result.status }, result.error);
1274
+ }
1275
+ }
1276
+ this.eventEmitter.emit("strandUpdate", strand);
1277
+ return result;
1278
+ }
1279
+ }
1280
+ export const DocumentDriveServer = ReadModeServer(BaseDocumentDriveServer);