@powerhousedao/reactor 6.0.0-dev.4 → 6.0.0-dev.41

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 (227) hide show
  1. package/dist/src/cache/collection-membership-cache.d.ts +13 -0
  2. package/dist/src/cache/collection-membership-cache.d.ts.map +1 -0
  3. package/dist/src/cache/collection-membership-cache.js +33 -0
  4. package/dist/src/cache/collection-membership-cache.js.map +1 -0
  5. package/dist/src/cache/document-meta-cache.d.ts.map +1 -1
  6. package/dist/src/cache/document-meta-cache.js +4 -4
  7. package/dist/src/cache/document-meta-cache.js.map +1 -1
  8. package/dist/src/cache/kysely-operation-index.d.ts +5 -1
  9. package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
  10. package/dist/src/cache/kysely-operation-index.js +96 -6
  11. package/dist/src/cache/kysely-operation-index.js.map +1 -1
  12. package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
  13. package/dist/src/cache/kysely-write-cache.js +11 -11
  14. package/dist/src/cache/kysely-write-cache.js.map +1 -1
  15. package/dist/src/cache/operation-index-types.d.ts +13 -1
  16. package/dist/src/cache/operation-index-types.d.ts.map +1 -1
  17. package/dist/src/cache/operation-index-types.js.map +1 -1
  18. package/dist/src/client/reactor-client.d.ts +13 -10
  19. package/dist/src/client/reactor-client.d.ts.map +1 -1
  20. package/dist/src/client/reactor-client.js +134 -43
  21. package/dist/src/client/reactor-client.js.map +1 -1
  22. package/dist/src/client/types.d.ts +25 -6
  23. package/dist/src/client/types.d.ts.map +1 -1
  24. package/dist/src/client/types.js.map +1 -1
  25. package/dist/src/core/reactor-builder.d.ts +11 -7
  26. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  27. package/dist/src/core/reactor-builder.js +61 -22
  28. package/dist/src/core/reactor-builder.js.map +1 -1
  29. package/dist/src/core/reactor-client-builder.d.ts +5 -4
  30. package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
  31. package/dist/src/core/reactor-client-builder.js +14 -5
  32. package/dist/src/core/reactor-client-builder.js.map +1 -1
  33. package/dist/src/core/reactor.d.ts +20 -80
  34. package/dist/src/core/reactor.d.ts.map +1 -1
  35. package/dist/src/core/reactor.js +235 -576
  36. package/dist/src/core/reactor.js.map +1 -1
  37. package/dist/src/core/types.d.ts +63 -28
  38. package/dist/src/core/types.d.ts.map +1 -1
  39. package/dist/src/core/utils.d.ts +37 -2
  40. package/dist/src/core/utils.d.ts.map +1 -1
  41. package/dist/src/core/utils.js +57 -8
  42. package/dist/src/core/utils.js.map +1 -1
  43. package/dist/src/events/types.d.ts +35 -10
  44. package/dist/src/events/types.d.ts.map +1 -1
  45. package/dist/src/events/types.js +7 -5
  46. package/dist/src/events/types.js.map +1 -1
  47. package/dist/src/executor/document-action-handler.d.ts +37 -0
  48. package/dist/src/executor/document-action-handler.d.ts.map +1 -0
  49. package/dist/src/executor/document-action-handler.js +349 -0
  50. package/dist/src/executor/document-action-handler.js.map +1 -0
  51. package/dist/src/executor/signature-verifier.d.ts +9 -0
  52. package/dist/src/executor/signature-verifier.d.ts.map +1 -0
  53. package/dist/src/executor/signature-verifier.js +70 -0
  54. package/dist/src/executor/signature-verifier.js.map +1 -0
  55. package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
  56. package/dist/src/executor/simple-job-executor-manager.js +20 -10
  57. package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
  58. package/dist/src/executor/simple-job-executor.d.ts +6 -46
  59. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  60. package/dist/src/executor/simple-job-executor.js +64 -565
  61. package/dist/src/executor/simple-job-executor.js.map +1 -1
  62. package/dist/src/executor/types.d.ts +0 -2
  63. package/dist/src/executor/types.d.ts.map +1 -1
  64. package/dist/src/executor/types.js.map +1 -1
  65. package/dist/src/executor/util.d.ts +11 -1
  66. package/dist/src/executor/util.d.ts.map +1 -1
  67. package/dist/src/executor/util.js +47 -1
  68. package/dist/src/executor/util.js.map +1 -1
  69. package/dist/src/index.d.ts +10 -7
  70. package/dist/src/index.d.ts.map +1 -1
  71. package/dist/src/index.js +6 -4
  72. package/dist/src/index.js.map +1 -1
  73. package/dist/src/job-tracker/in-memory-job-tracker.d.ts +4 -3
  74. package/dist/src/job-tracker/in-memory-job-tracker.d.ts.map +1 -1
  75. package/dist/src/job-tracker/in-memory-job-tracker.js +20 -18
  76. package/dist/src/job-tracker/in-memory-job-tracker.js.map +1 -1
  77. package/dist/src/job-tracker/interfaces.d.ts +3 -1
  78. package/dist/src/job-tracker/interfaces.d.ts.map +1 -1
  79. package/dist/src/logging/console.d.ts +1 -22
  80. package/dist/src/logging/console.d.ts.map +1 -1
  81. package/dist/src/logging/console.js +1 -107
  82. package/dist/src/logging/console.js.map +1 -1
  83. package/dist/src/logging/types.d.ts +1 -11
  84. package/dist/src/logging/types.d.ts.map +1 -1
  85. package/dist/src/processors/index.d.ts.map +1 -1
  86. package/dist/src/processors/relational/relational-db-processor.d.ts +47 -0
  87. package/dist/src/processors/relational/relational-db-processor.d.ts.map +1 -0
  88. package/dist/src/processors/relational/relational-db-processor.js +45 -0
  89. package/dist/src/processors/relational/relational-db-processor.js.map +1 -0
  90. package/dist/src/processors/relational/types.d.ts +27 -0
  91. package/dist/src/processors/relational/types.d.ts.map +1 -0
  92. package/dist/src/processors/relational/types.js +2 -0
  93. package/dist/src/processors/relational/types.js.map +1 -0
  94. package/dist/src/processors/relational/utils.d.ts +29 -0
  95. package/dist/src/processors/relational/utils.d.ts.map +1 -0
  96. package/dist/src/processors/relational/utils.js +67 -0
  97. package/dist/src/processors/relational/utils.js.map +1 -0
  98. package/dist/src/processors/types.d.ts +10 -2
  99. package/dist/src/processors/types.d.ts.map +1 -1
  100. package/dist/src/processors/utils.d.ts.map +1 -1
  101. package/dist/src/processors/utils.js +2 -1
  102. package/dist/src/processors/utils.js.map +1 -1
  103. package/dist/src/queue/queue.d.ts +25 -0
  104. package/dist/src/queue/queue.d.ts.map +1 -1
  105. package/dist/src/queue/queue.js +56 -0
  106. package/dist/src/queue/queue.js.map +1 -1
  107. package/dist/src/queue/types.d.ts +3 -3
  108. package/dist/src/queue/types.d.ts.map +1 -1
  109. package/dist/src/read-models/base-read-model.js +4 -4
  110. package/dist/src/read-models/base-read-model.js.map +1 -1
  111. package/dist/src/read-models/coordinator.d.ts +2 -2
  112. package/dist/src/read-models/coordinator.d.ts.map +1 -1
  113. package/dist/src/read-models/coordinator.js +8 -8
  114. package/dist/src/read-models/coordinator.js.map +1 -1
  115. package/dist/src/read-models/document-view.d.ts +5 -2
  116. package/dist/src/read-models/document-view.d.ts.map +1 -1
  117. package/dist/src/read-models/document-view.js +130 -48
  118. package/dist/src/read-models/document-view.js.map +1 -1
  119. package/dist/src/shared/awaiter.d.ts +2 -2
  120. package/dist/src/shared/awaiter.d.ts.map +1 -1
  121. package/dist/src/shared/awaiter.js +11 -11
  122. package/dist/src/shared/awaiter.js.map +1 -1
  123. package/dist/src/shared/collect-all-pages.d.ts +7 -0
  124. package/dist/src/shared/collect-all-pages.d.ts.map +1 -0
  125. package/dist/src/shared/collect-all-pages.js +17 -0
  126. package/dist/src/shared/collect-all-pages.js.map +1 -0
  127. package/dist/src/shared/drive-url.d.ts +15 -0
  128. package/dist/src/shared/drive-url.d.ts.map +1 -0
  129. package/dist/src/shared/drive-url.js +17 -0
  130. package/dist/src/shared/drive-url.js.map +1 -0
  131. package/dist/src/shared/factories.d.ts +6 -2
  132. package/dist/src/shared/factories.d.ts.map +1 -1
  133. package/dist/src/shared/factories.js +10 -2
  134. package/dist/src/shared/factories.js.map +1 -1
  135. package/dist/src/shared/types.d.ts +32 -6
  136. package/dist/src/shared/types.d.ts.map +1 -1
  137. package/dist/src/shared/types.js +4 -4
  138. package/dist/src/shared/types.js.map +1 -1
  139. package/dist/src/signer/passthrough-signer.d.ts +1 -1
  140. package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
  141. package/dist/src/signer/passthrough-signer.js +1 -3
  142. package/dist/src/signer/passthrough-signer.js.map +1 -1
  143. package/dist/src/storage/interfaces.d.ts +52 -94
  144. package/dist/src/storage/interfaces.d.ts.map +1 -1
  145. package/dist/src/storage/interfaces.js.map +1 -1
  146. package/dist/src/storage/kysely/document-indexer.d.ts +6 -6
  147. package/dist/src/storage/kysely/document-indexer.d.ts.map +1 -1
  148. package/dist/src/storage/kysely/document-indexer.js +123 -52
  149. package/dist/src/storage/kysely/document-indexer.js.map +1 -1
  150. package/dist/src/storage/kysely/store.d.ts +4 -3
  151. package/dist/src/storage/kysely/store.d.ts.map +1 -1
  152. package/dist/src/storage/kysely/store.js +52 -21
  153. package/dist/src/storage/kysely/store.js.map +1 -1
  154. package/dist/src/storage/kysely/sync-remote-storage.js +1 -1
  155. package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
  156. package/dist/src/subs/subscription-notification-read-model.d.ts +1 -1
  157. package/dist/src/subs/subscription-notification-read-model.js +1 -1
  158. package/dist/src/sync/buffered-mailbox.d.ts +30 -0
  159. package/dist/src/sync/buffered-mailbox.d.ts.map +1 -0
  160. package/dist/src/sync/buffered-mailbox.js +136 -0
  161. package/dist/src/sync/buffered-mailbox.js.map +1 -0
  162. package/dist/src/sync/channels/composite-channel-factory.d.ts +9 -3
  163. package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -1
  164. package/dist/src/sync/channels/composite-channel-factory.js +16 -12
  165. package/dist/src/sync/channels/composite-channel-factory.js.map +1 -1
  166. package/dist/src/sync/channels/gql-channel-factory.d.ts +9 -3
  167. package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
  168. package/dist/src/sync/channels/gql-channel-factory.js +14 -10
  169. package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
  170. package/dist/src/sync/channels/gql-channel.d.ts +22 -18
  171. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
  172. package/dist/src/sync/channels/gql-channel.js +128 -64
  173. package/dist/src/sync/channels/gql-channel.js.map +1 -1
  174. package/dist/src/sync/channels/index.d.ts +2 -0
  175. package/dist/src/sync/channels/index.d.ts.map +1 -1
  176. package/dist/src/sync/channels/index.js +2 -0
  177. package/dist/src/sync/channels/index.js.map +1 -1
  178. package/dist/src/sync/channels/interval-poll-timer.d.ts +24 -0
  179. package/dist/src/sync/channels/interval-poll-timer.d.ts.map +1 -0
  180. package/dist/src/sync/channels/interval-poll-timer.js +69 -0
  181. package/dist/src/sync/channels/interval-poll-timer.js.map +1 -0
  182. package/dist/src/sync/channels/poll-timer.d.ts +14 -0
  183. package/dist/src/sync/channels/poll-timer.d.ts.map +1 -0
  184. package/dist/src/sync/channels/poll-timer.js +2 -0
  185. package/dist/src/sync/channels/poll-timer.js.map +1 -0
  186. package/dist/src/sync/channels/utils.d.ts.map +1 -1
  187. package/dist/src/sync/channels/utils.js +2 -2
  188. package/dist/src/sync/channels/utils.js.map +1 -1
  189. package/dist/src/sync/index.d.ts +6 -4
  190. package/dist/src/sync/index.d.ts.map +1 -1
  191. package/dist/src/sync/index.js +4 -3
  192. package/dist/src/sync/index.js.map +1 -1
  193. package/dist/src/sync/interfaces.d.ts +18 -6
  194. package/dist/src/sync/interfaces.d.ts.map +1 -1
  195. package/dist/src/sync/mailbox.d.ts +21 -3
  196. package/dist/src/sync/mailbox.d.ts.map +1 -1
  197. package/dist/src/sync/mailbox.js +55 -2
  198. package/dist/src/sync/mailbox.js.map +1 -1
  199. package/dist/src/sync/sync-awaiter.d.ts +34 -0
  200. package/dist/src/sync/sync-awaiter.d.ts.map +1 -0
  201. package/dist/src/sync/sync-awaiter.js +124 -0
  202. package/dist/src/sync/sync-awaiter.js.map +1 -0
  203. package/dist/src/sync/sync-manager.d.ts +19 -5
  204. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  205. package/dist/src/sync/sync-manager.js +318 -33
  206. package/dist/src/sync/sync-manager.js.map +1 -1
  207. package/dist/src/sync/sync-operation.d.ts +3 -1
  208. package/dist/src/sync/sync-operation.d.ts.map +1 -1
  209. package/dist/src/sync/sync-operation.js +5 -1
  210. package/dist/src/sync/sync-operation.js.map +1 -1
  211. package/dist/src/sync/types.d.ts +73 -1
  212. package/dist/src/sync/types.d.ts.map +1 -1
  213. package/dist/src/sync/types.js +10 -0
  214. package/dist/src/sync/types.js.map +1 -1
  215. package/dist/src/sync/utils.d.ts +11 -0
  216. package/dist/src/sync/utils.d.ts.map +1 -1
  217. package/dist/src/sync/utils.js +17 -0
  218. package/dist/src/sync/utils.js.map +1 -1
  219. package/dist/src/utils/reshuffle.d.ts +15 -5
  220. package/dist/src/utils/reshuffle.d.ts.map +1 -1
  221. package/dist/src/utils/reshuffle.js +29 -6
  222. package/dist/src/utils/reshuffle.js.map +1 -1
  223. package/package.json +11 -13
  224. package/dist/src/storage/consistency-aware-legacy-storage.d.ts +0 -33
  225. package/dist/src/storage/consistency-aware-legacy-storage.d.ts.map +0 -1
  226. package/dist/src/storage/consistency-aware-legacy-storage.js +0 -65
  227. package/dist/src/storage/consistency-aware-legacy-storage.js.map +0 -1
@@ -1,10 +1,11 @@
1
- import { deriveOperationId, isUndoRedo } from "document-model/core";
2
- import { driveCollectionId } from "../cache/operation-index-types.js";
3
- import { OperationEventTypes, } from "../events/types.js";
4
- import { DocumentDeletedError, InvalidSignatureError, } from "../shared/errors.js";
1
+ import { isUndoRedo } from "document-model/core";
2
+ import { ReactorEventTypes } from "../events/types.js";
3
+ import { DocumentDeletedError } from "../shared/errors.js";
5
4
  import { reshuffleByTimestamp } from "../utils/reshuffle.js";
6
- import { applyDeleteDocumentAction, applyUpgradeDocumentAction, createDocumentFromAction, getNextIndexForScope, } from "./util.js";
7
- const MAX_SKIP_THRESHOLD = 100;
5
+ import { DocumentActionHandler } from "./document-action-handler.js";
6
+ import { SignatureVerifier } from "./signature-verifier.js";
7
+ import { buildErrorResult } from "./util.js";
8
+ const MAX_SKIP_THRESHOLD = 1000;
8
9
  const documentScopeActions = [
9
10
  "CREATE_DOCUMENT",
10
11
  "DELETE_DOCUMENT",
@@ -14,42 +15,37 @@ const documentScopeActions = [
14
15
  ];
15
16
  /**
16
17
  * Simple job executor that processes a job by applying actions through document model reducers.
17
- *
18
- * @see docs/planning/Storage/IOperationStore.md for storage schema
19
- * @see docs/planning/Operations/index.md for operation structure
20
- * @see docs/planning/Jobs/reshuffle.md for skip mechanism details
21
18
  */
22
19
  export class SimpleJobExecutor {
23
20
  logger;
24
21
  registry;
25
- documentStorage;
26
- operationStorage;
27
22
  operationStore;
28
23
  eventBus;
29
24
  writeCache;
30
25
  operationIndex;
31
26
  documentMetaCache;
32
- signatureVerifier;
27
+ collectionMembershipCache;
33
28
  config;
34
- constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
29
+ signatureVerifierModule;
30
+ documentActionHandler;
31
+ constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, config, signatureVerifier) {
35
32
  this.logger = logger;
36
33
  this.registry = registry;
37
- this.documentStorage = documentStorage;
38
- this.operationStorage = operationStorage;
39
34
  this.operationStore = operationStore;
40
35
  this.eventBus = eventBus;
41
36
  this.writeCache = writeCache;
42
37
  this.operationIndex = operationIndex;
43
38
  this.documentMetaCache = documentMetaCache;
44
- this.signatureVerifier = signatureVerifier;
39
+ this.collectionMembershipCache = collectionMembershipCache;
45
40
  this.config = {
46
41
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
47
42
  maxConcurrency: config.maxConcurrency ?? 1,
48
43
  jobTimeoutMs: config.jobTimeoutMs ?? 30000,
49
44
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
50
45
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5000,
51
- legacyStorageEnabled: config.legacyStorageEnabled ?? true,
52
46
  };
47
+ this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
48
+ this.documentActionHandler = new DocumentActionHandler(writeCache, operationStore, documentMetaCache, collectionMembershipCache, registry, logger);
53
49
  }
54
50
  /**
55
51
  * Execute a single job by applying all its actions through the appropriate reducers.
@@ -66,13 +62,15 @@ export class SimpleJobExecutor {
66
62
  result.operationsWithContext[i].context.ordinal = ordinals[i];
67
63
  }
68
64
  if (result.operationsWithContext.length > 0) {
65
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(result.operationsWithContext);
69
66
  const event = {
70
67
  jobId: job.id,
71
68
  operations: result.operationsWithContext,
72
69
  jobMeta: job.meta,
70
+ collectionMemberships,
73
71
  };
74
72
  this.eventBus
75
- .emit(OperationEventTypes.OPERATION_WRITTEN, event)
73
+ .emit(ReactorEventTypes.JOB_WRITE_READY, event)
76
74
  .catch(() => {
77
75
  // TODO: Log error
78
76
  });
@@ -94,14 +92,14 @@ export class SimpleJobExecutor {
94
92
  for (let i = 0; i < result.operationsWithContext.length; i++) {
95
93
  result.operationsWithContext[i].context.ordinal = ordinals[i];
96
94
  }
95
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(result.operationsWithContext);
97
96
  const event = {
98
97
  jobId: job.id,
99
98
  operations: result.operationsWithContext,
100
99
  jobMeta: job.meta,
100
+ collectionMemberships,
101
101
  };
102
- this.eventBus
103
- .emit(OperationEventTypes.OPERATION_WRITTEN, event)
104
- .catch(() => {
102
+ this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, event).catch(() => {
105
103
  // TODO: Log error
106
104
  });
107
105
  }
@@ -113,11 +111,17 @@ export class SimpleJobExecutor {
113
111
  duration: Date.now() - startTime,
114
112
  };
115
113
  }
114
+ async getCollectionMembershipsForOperations(operations) {
115
+ const documentIds = [
116
+ ...new Set(operations.map((op) => op.context.documentId)),
117
+ ];
118
+ return this.collectionMembershipCache.getCollectionsForDocuments(documentIds);
119
+ }
116
120
  async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
117
121
  const generatedOperations = [];
118
122
  const operationsWithContext = [];
119
123
  try {
120
- await this.verifyActionSignatures(job, actions);
124
+ await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
121
125
  }
122
126
  catch (error) {
123
127
  return {
@@ -133,7 +137,7 @@ export class SimpleJobExecutor {
133
137
  const sourceOperation = sourceOperations?.[actionIndex];
134
138
  const isDocumentAction = documentScopeActions.includes(action.type);
135
139
  const result = isDocumentAction
136
- ? await this.executeDocumentAction(job, action, startTime, indexTxn, skip)
140
+ ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, skip)
137
141
  : await this.executeRegularAction(job, action, startTime, indexTxn, skip, sourceOperation);
138
142
  const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
139
143
  if (error !== null) {
@@ -151,414 +155,31 @@ export class SimpleJobExecutor {
151
155
  operationsWithContext,
152
156
  };
153
157
  }
154
- /**
155
- * Execute a document scope action (CREATE_DOCUMENT, DELETE_DOCUMENT, UPGRADE_DOCUMENT,
156
- * ADD_RELATIONSHIP, REMOVE_RELATIONSHIP) by dispatching to the appropriate handler.
157
- */
158
- async executeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
159
- switch (action.type) {
160
- case "CREATE_DOCUMENT":
161
- return this.executeCreateDocumentAction(job, action, startTime, indexTxn, skip);
162
- case "DELETE_DOCUMENT":
163
- return this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
164
- case "UPGRADE_DOCUMENT":
165
- return this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip);
166
- case "ADD_RELATIONSHIP":
167
- return this.executeAddRelationshipAction(job, action, startTime, indexTxn);
168
- case "REMOVE_RELATIONSHIP":
169
- return this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
170
- default:
171
- return this.buildErrorResult(job, new Error(`Unknown document action type: ${action.type}`), startTime);
172
- }
173
- }
174
- /**
175
- * Execute a CREATE_DOCUMENT system action.
176
- * This creates a new document in storage along with its initial operation.
177
- * For a new document, the operation index is always 0.
178
- */
179
- async executeCreateDocumentAction(job, action, startTime, indexTxn, skip = 0) {
180
- if (job.scope !== "document") {
181
- return {
182
- job,
183
- success: false,
184
- error: new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
185
- duration: Date.now() - startTime,
186
- };
187
- }
188
- const document = createDocumentFromAction(action);
189
- // Legacy: Store the document in storage
190
- if (this.config.legacyStorageEnabled) {
191
- try {
192
- await this.documentStorage.create(document);
193
- }
194
- catch (error) {
195
- return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
196
- }
197
- }
198
- const operation = this.createOperation(action, 0, skip, {
199
- documentId: document.header.id,
200
- scope: job.scope,
201
- branch: job.branch,
202
- });
203
- // Legacy: Write the CREATE_DOCUMENT operation to legacy storage
204
- if (this.config.legacyStorageEnabled) {
205
- try {
206
- await this.operationStorage.addDocumentOperations(document.header.id, [operation], document);
207
- }
208
- catch (error) {
209
- return this.buildErrorResult(job, new Error(`Failed to write CREATE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
210
- }
211
- }
212
- // Compute resultingState for passing via context (not persisted)
213
- // Include header and all scopes present in the document state (auth, document, etc.)
214
- // but not global/local which aren't initialized by CREATE_DOCUMENT
215
- const resultingStateObj = {
216
- header: document.header,
217
- ...document.state,
218
- };
219
- const resultingState = JSON.stringify(resultingStateObj);
220
- const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime);
221
- if (writeError !== null) {
222
- return writeError;
223
- }
224
- this.updateDocumentRevision(document, job.scope, operation.index);
225
- this.writeCacheState(document.header.id, job.scope, job.branch, operation.index, document);
226
- indexTxn.write([
227
- {
228
- ...operation,
229
- documentId: document.header.id,
230
- documentType: document.header.documentType,
231
- branch: job.branch,
232
- scope: job.scope,
233
- },
234
- ]);
235
- // collection membership has to be _after_ the write, as it requires the
236
- // ordinal of the operation to be set
237
- if (document.header.documentType === "powerhouse/document-drive") {
238
- const collectionId = driveCollectionId(job.branch, document.header.id);
239
- indexTxn.createCollection(collectionId);
240
- indexTxn.addToCollection(collectionId, document.header.id);
241
- }
242
- this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
243
- state: document.state.document,
244
- documentType: document.header.documentType,
245
- documentScopeRevision: 1,
246
- });
247
- return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
248
- }
249
- /**
250
- * Execute a DELETE_DOCUMENT system action.
251
- * This deletes a document from legacy storage and writes the operation to IOperationStore.
252
- * The operation index is determined from the document's current operation count.
253
- */
254
- async executeDeleteDocumentAction(job, action, startTime, indexTxn) {
255
- const input = action.input;
256
- if (!input.documentId) {
257
- return this.buildErrorResult(job, new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
258
- }
259
- const documentId = input.documentId;
260
- let document;
261
- try {
262
- document = await this.writeCache.getState(documentId, job.scope, job.branch);
263
- }
264
- catch (error) {
265
- return this.buildErrorResult(job, new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
266
- }
267
- // Check if document is already deleted
268
- const documentState = document.state.document;
269
- if (documentState.isDeleted) {
270
- return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
271
- }
272
- const nextIndex = getNextIndexForScope(document, job.scope);
273
- const operation = this.createOperation(action, nextIndex, 0, {
274
- documentId,
275
- scope: job.scope,
276
- branch: job.branch,
277
- });
278
- if (this.config.legacyStorageEnabled) {
279
- try {
280
- await this.documentStorage.delete(documentId);
281
- }
282
- catch (error) {
283
- return this.buildErrorResult(job, new Error(`Failed to delete document from legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
284
- }
285
- }
286
- // Mark the document as deleted in the state for read model indexing
287
- applyDeleteDocumentAction(document, action);
288
- // Compute resultingState for passing via context (not persisted)
289
- // DELETE_DOCUMENT only affects header and document scopes
290
- const resultingStateObj = {
291
- header: document.header,
292
- document: document.state.document,
293
- };
294
- const resultingState = JSON.stringify(resultingStateObj);
295
- const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
296
- if (writeError !== null) {
297
- return writeError;
298
- }
299
- indexTxn.write([
300
- {
301
- ...operation,
302
- documentId: documentId,
303
- documentType: document.header.documentType,
304
- branch: job.branch,
305
- scope: job.scope,
306
- },
307
- ]);
308
- this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
309
- state: document.state.document,
310
- documentType: document.header.documentType,
311
- documentScopeRevision: operation.index + 1,
312
- });
313
- return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
314
- }
315
- /**
316
- * Execute an UPGRADE_DOCUMENT system action.
317
- * Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
318
- * The operation index is determined from the document's current operation count.
319
- */
320
- async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
321
- const input = action.input;
322
- if (!input.documentId) {
323
- return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
324
- }
325
- const documentId = input.documentId;
326
- const fromVersion = input.fromVersion;
327
- const toVersion = input.toVersion;
328
- let document;
329
- try {
330
- document = await this.writeCache.getState(documentId, job.scope, job.branch);
331
- }
332
- catch (error) {
333
- return this.buildErrorResult(job, new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
334
- }
335
- const documentState = document.state.document;
336
- if (documentState.isDeleted) {
337
- return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
338
- }
339
- const nextIndex = getNextIndexForScope(document, job.scope);
340
- let upgradePath;
341
- if (fromVersion > 0 && fromVersion < toVersion) {
342
- try {
343
- upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
344
- }
345
- catch (error) {
346
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
347
- }
348
- }
349
- if (fromVersion === toVersion && fromVersion > 0) {
350
- return {
351
- job,
352
- success: true,
353
- operations: [],
354
- operationsWithContext: [],
355
- duration: Date.now() - startTime,
356
- };
357
- }
358
- try {
359
- document = applyUpgradeDocumentAction(document, action, upgradePath);
360
- }
361
- catch (error) {
362
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
363
- }
364
- const operation = this.createOperation(action, nextIndex, skip, {
365
- documentId,
366
- scope: job.scope,
367
- branch: job.branch,
368
- });
369
- // Write the updated document to legacy storage
370
- if (this.config.legacyStorageEnabled) {
371
- try {
372
- await this.operationStorage.addDocumentOperations(documentId, [operation], document);
373
- }
374
- catch (error) {
375
- return this.buildErrorResult(job, new Error(`Failed to write UPGRADE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
376
- }
377
- }
378
- // Compute resultingState for passing via context (not persisted)
379
- const resultingStateObj = {
380
- header: document.header,
381
- ...document.state,
382
- };
383
- const resultingState = JSON.stringify(resultingStateObj);
384
- const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
385
- if (writeError !== null) {
386
- return writeError;
387
- }
388
- this.updateDocumentRevision(document, job.scope, operation.index);
389
- this.writeCacheState(documentId, job.scope, job.branch, operation.index, document);
390
- indexTxn.write([
391
- {
392
- ...operation,
393
- documentId: documentId,
394
- documentType: document.header.documentType,
395
- branch: job.branch,
396
- scope: job.scope,
397
- },
398
- ]);
399
- this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
400
- state: document.state.document,
401
- documentType: document.header.documentType,
402
- documentScopeRevision: operation.index + 1,
403
- });
404
- return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
405
- }
406
- async executeAddRelationshipAction(job, action, startTime, indexTxn) {
407
- if (job.scope !== "document") {
408
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
409
- }
410
- const input = action.input;
411
- if (!input.sourceId || !input.targetId || !input.relationshipType) {
412
- return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
413
- }
414
- if (input.sourceId === input.targetId) {
415
- return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)"), startTime);
416
- }
417
- let sourceDoc;
418
- try {
419
- sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
420
- }
421
- catch (error) {
422
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
423
- }
424
- const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
425
- const operation = this.createOperation(action, nextIndex, 0, {
426
- documentId: input.sourceId,
427
- scope: job.scope,
428
- branch: job.branch,
429
- });
430
- const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
431
- if (writeError !== null) {
432
- return writeError;
433
- }
434
- sourceDoc.header.lastModifiedAtUtcIso =
435
- operation.timestampUtcMs || new Date().toISOString();
436
- this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
437
- sourceDoc.operations = {
438
- ...sourceDoc.operations,
439
- [job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
440
- };
441
- const scopeState = sourceDoc.state[job.scope];
442
- const resultingStateObj = {
443
- header: structuredClone(sourceDoc.header),
444
- [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
445
- };
446
- const resultingState = JSON.stringify(resultingStateObj);
447
- this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
448
- indexTxn.write([
449
- {
450
- ...operation,
451
- documentId: input.sourceId,
452
- documentType: sourceDoc.header.documentType,
453
- branch: job.branch,
454
- scope: job.scope,
455
- },
456
- ]);
457
- // collection membership has to be _after_ the write, as it requires the
458
- // ordinal of the operation to be set
459
- if (sourceDoc.header.documentType === "powerhouse/document-drive") {
460
- const collectionId = driveCollectionId(job.branch, input.sourceId);
461
- indexTxn.addToCollection(collectionId, input.targetId);
462
- }
463
- this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
464
- state: sourceDoc.state.document,
465
- documentType: sourceDoc.header.documentType,
466
- documentScopeRevision: operation.index + 1,
467
- });
468
- return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
469
- }
470
- async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
471
- if (job.scope !== "document") {
472
- return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
473
- }
474
- const input = action.input;
475
- if (!input.sourceId || !input.targetId || !input.relationshipType) {
476
- return this.buildErrorResult(job, new Error("REMOVE_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
477
- }
478
- let sourceDoc;
479
- try {
480
- sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
481
- }
482
- catch (error) {
483
- return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
484
- }
485
- const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
486
- const operation = this.createOperation(action, nextIndex, 0, {
487
- documentId: input.sourceId,
488
- scope: job.scope,
489
- branch: job.branch,
490
- });
491
- const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
492
- if (writeError !== null) {
493
- return writeError;
494
- }
495
- sourceDoc.header.lastModifiedAtUtcIso =
496
- operation.timestampUtcMs || new Date().toISOString();
497
- this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
498
- sourceDoc.operations = {
499
- ...sourceDoc.operations,
500
- [job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
501
- };
502
- const scopeState = sourceDoc.state[job.scope];
503
- const resultingStateObj = {
504
- header: structuredClone(sourceDoc.header),
505
- [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
506
- };
507
- const resultingState = JSON.stringify(resultingStateObj);
508
- this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
509
- indexTxn.write([
510
- {
511
- ...operation,
512
- documentId: input.sourceId,
513
- documentType: sourceDoc.header.documentType,
514
- branch: job.branch,
515
- scope: job.scope,
516
- },
517
- ]);
518
- // collection membership has to be _after_ the write, as it requires the
519
- // ordinal of the operation to be set
520
- if (sourceDoc.header.documentType === "powerhouse/document-drive") {
521
- const collectionId = driveCollectionId(job.branch, input.sourceId);
522
- indexTxn.removeFromCollection(collectionId, input.targetId);
523
- }
524
- this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
525
- state: sourceDoc.state.document,
526
- documentType: sourceDoc.header.documentType,
527
- documentScopeRevision: operation.index + 1,
528
- });
529
- return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
530
- }
531
- /**
532
- * Execute a regular document action by applying it through the document model reducer.
533
- * If sourceOperation is provided (for load jobs), its id and timestamp are preserved.
534
- */
535
158
  async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
536
159
  let docMeta;
537
160
  try {
538
161
  docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
539
162
  }
540
163
  catch (error) {
541
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
164
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
542
165
  }
543
166
  if (docMeta.state.isDeleted) {
544
- return this.buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
167
+ return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
545
168
  }
546
169
  let document;
547
170
  try {
548
171
  document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
549
172
  }
550
173
  catch (error) {
551
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
174
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
552
175
  }
553
176
  let module;
554
177
  try {
555
- // Use document version to get the correct module
556
- // Version 0 means not yet upgraded - use latest version
557
178
  const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
558
179
  module = this.registry.getModule(document.header.documentType, moduleVersion);
559
180
  }
560
181
  catch (error) {
561
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
182
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
562
183
  }
563
184
  let updatedDocument;
564
185
  try {
@@ -579,32 +200,35 @@ export class SimpleJobExecutor {
579
200
  if (error instanceof Error && error.stack) {
580
201
  enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
581
202
  }
582
- return this.buildErrorResult(job, enhancedError, startTime);
203
+ return buildErrorResult(job, enhancedError, startTime);
583
204
  }
584
205
  const scope = job.scope;
585
206
  const operations = updatedDocument.operations[scope];
586
207
  if (operations.length === 0) {
587
- return this.buildErrorResult(job, new Error("No operation generated from action"), startTime);
208
+ return buildErrorResult(job, new Error("No operation generated from action"), startTime);
588
209
  }
589
210
  const newOperation = operations[operations.length - 1];
590
211
  if (!isUndoRedo(action)) {
591
212
  newOperation.skip = skip;
592
213
  }
593
- if (this.config.legacyStorageEnabled) {
594
- try {
595
- await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
596
- }
597
- catch (error) {
598
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
599
- }
600
- }
601
214
  const resultingState = JSON.stringify({
602
215
  ...updatedDocument.state,
603
216
  header: updatedDocument.header,
604
217
  });
605
- const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
606
- if (writeFailResult !== null) {
607
- return writeFailResult;
218
+ try {
219
+ await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
220
+ txn.addOperations(newOperation);
221
+ });
222
+ }
223
+ catch (error) {
224
+ this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
225
+ this.writeCache.invalidate(job.documentId, scope, job.branch);
226
+ return {
227
+ job,
228
+ success: false,
229
+ error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
230
+ duration: Date.now() - startTime,
231
+ };
608
232
  }
609
233
  updatedDocument.header.revision = {
610
234
  ...updatedDocument.header.revision,
@@ -640,20 +264,9 @@ export class SimpleJobExecutor {
640
264
  duration: Date.now() - startTime,
641
265
  };
642
266
  }
643
- createOperation(action, index, skip = 0, context) {
644
- const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
645
- return {
646
- id,
647
- index: index,
648
- timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
649
- hash: "",
650
- skip: skip,
651
- action: action,
652
- };
653
- }
654
267
  async executeLoadJob(job, startTime, indexTxn) {
655
268
  if (job.operations.length === 0) {
656
- return this.buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
269
+ return buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
657
270
  }
658
271
  const scope = job.scope;
659
272
  let latestRevision = 0;
@@ -675,8 +288,8 @@ export class SimpleJobExecutor {
675
288
  }
676
289
  let conflictingOps = [];
677
290
  try {
678
- const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
679
- if (conflictingResult.hasMore) {
291
+ const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { cursor: "0", limit: this.config.maxSkipThreshold + 1 });
292
+ if (conflictingResult.nextCursor !== undefined) {
680
293
  return {
681
294
  job,
682
295
  success: false,
@@ -685,16 +298,24 @@ export class SimpleJobExecutor {
685
298
  duration: Date.now() - startTime,
686
299
  };
687
300
  }
688
- conflictingOps = conflictingResult.items;
301
+ conflictingOps = conflictingResult.results;
689
302
  }
690
303
  catch {
691
304
  conflictingOps = [];
692
305
  }
693
- // Filter out operations that have been superseded by later operations with skip values.
694
- // An operation at index N is superseded if there exists an operation at index M > N
695
- // where (M - skip_M) <= N, meaning the later operation's logical index covers N.
306
+ let allOpsFromMinConflictingIndex = conflictingOps;
307
+ if (conflictingOps.length > 0) {
308
+ const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
309
+ try {
310
+ const allOpsResult = await this.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, undefined, { cursor: "0", limit: this.config.maxSkipThreshold * 2 });
311
+ allOpsFromMinConflictingIndex = allOpsResult.results;
312
+ }
313
+ catch {
314
+ allOpsFromMinConflictingIndex = conflictingOps;
315
+ }
316
+ }
696
317
  const nonSupersededOps = conflictingOps.filter((op) => {
697
- for (const laterOp of conflictingOps) {
318
+ for (const laterOp of allOpsFromMinConflictingIndex) {
698
319
  if (laterOp.index > op.index && laterOp.skip > 0) {
699
320
  const logicalIndex = laterOp.index - laterOp.skip;
700
321
  if (logicalIndex <= op.index) {
@@ -704,9 +325,7 @@ export class SimpleJobExecutor {
704
325
  }
705
326
  return true;
706
327
  });
707
- // All non-superseded conflicting operations need to be reshuffled
708
328
  const existingOpsToReshuffle = nonSupersededOps;
709
- // Skip count is the number of existing operations that need to be rewound
710
329
  const skipCount = existingOpsToReshuffle.length;
711
330
  if (skipCount > this.config.maxSkipThreshold) {
712
331
  return {
@@ -717,8 +336,6 @@ export class SimpleJobExecutor {
717
336
  duration: Date.now() - startTime,
718
337
  };
719
338
  }
720
- // Filter out incoming operations that are duplicates (action already exists locally
721
- // or appears multiple times in incoming)
722
339
  const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
723
340
  const seenIncomingActionIds = new Set();
724
341
  const incomingOpsToApply = job.operations.filter((op) => {
@@ -736,7 +353,6 @@ export class SimpleJobExecutor {
736
353
  ...operation,
737
354
  id: operation.id,
738
355
  })));
739
- // For v2, all NOOPs have skip=1 - consecutive NOOPs are handled during state rebuild
740
356
  for (const operation of reshuffledOperations) {
741
357
  if (operation.action.type === "NOOP") {
742
358
  operation.skip = 1;
@@ -765,123 +381,6 @@ export class SimpleJobExecutor {
765
381
  duration: Date.now() - startTime,
766
382
  };
767
383
  }
768
- async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime) {
769
- try {
770
- await this.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
771
- txn.addOperations(operation);
772
- });
773
- return null;
774
- }
775
- catch (error) {
776
- this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
777
- this.writeCache.invalidate(documentId, scope, branch);
778
- return {
779
- job,
780
- success: false,
781
- error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
782
- duration: Date.now() - startTime,
783
- };
784
- }
785
- }
786
- updateDocumentRevision(document, scope, operationIndex) {
787
- document.header.revision = {
788
- ...document.header.revision,
789
- [scope]: operationIndex + 1,
790
- };
791
- }
792
- writeCacheState(documentId, scope, branch, operationIndex, document) {
793
- this.writeCache.putState(documentId, scope, branch, operationIndex, document);
794
- }
795
- buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
796
- return {
797
- job,
798
- success: true,
799
- operations: [operation],
800
- operationsWithContext: [
801
- {
802
- operation,
803
- context: {
804
- documentId: documentId,
805
- scope: job.scope,
806
- branch: job.branch,
807
- documentType: documentType,
808
- resultingState,
809
- ordinal: 0,
810
- },
811
- },
812
- ],
813
- duration: Date.now() - startTime,
814
- };
815
- }
816
- buildErrorResult(job, error, startTime) {
817
- return {
818
- job,
819
- success: false,
820
- error: error,
821
- duration: Date.now() - startTime,
822
- };
823
- }
824
- async verifyOperationSignatures(job, operations) {
825
- if (!this.signatureVerifier) {
826
- return;
827
- }
828
- for (let i = 0; i < operations.length; i++) {
829
- const operation = operations[i];
830
- const signer = operation.action.context?.signer;
831
- if (!signer) {
832
- continue;
833
- }
834
- if (signer.signatures.length === 0) {
835
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
836
- }
837
- const publicKey = signer.app.key;
838
- let isValid = false;
839
- try {
840
- isValid = await this.signatureVerifier(operation, publicKey);
841
- }
842
- catch (error) {
843
- const errorMessage = error instanceof Error ? error.message : String(error);
844
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
845
- }
846
- if (!isValid) {
847
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
848
- }
849
- }
850
- }
851
- async verifyActionSignatures(job, actions) {
852
- if (!this.signatureVerifier) {
853
- return;
854
- }
855
- for (const action of actions) {
856
- const signer = action.context?.signer;
857
- if (!signer) {
858
- continue;
859
- }
860
- if (signer.signatures.length === 0) {
861
- throw new InvalidSignatureError(job.documentId, `Action ${action.id} has signer but no signatures`);
862
- }
863
- const publicKey = signer.app.key;
864
- let isValid = false;
865
- try {
866
- const tempOperation = {
867
- id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
868
- index: 0,
869
- timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
870
- hash: "",
871
- skip: 0,
872
- action: action,
873
- };
874
- isValid = await this.signatureVerifier(tempOperation, publicKey);
875
- }
876
- catch (error) {
877
- const errorMessage = error instanceof Error ? error.message : String(error);
878
- throw new InvalidSignatureError(job.documentId, `Action ${action.id} verification failed: ${errorMessage}`);
879
- }
880
- if (!isValid) {
881
- throw new InvalidSignatureError(job.documentId, `Action ${action.id} signature verification returned false`);
882
- }
883
- }
884
- }
885
384
  accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
886
385
  if (!result.success) {
887
386
  return result;