@powerhousedao/reactor 5.1.0 → 5.2.0-dev.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 (225) hide show
  1. package/dist/src/actions/index.d.ts +24 -0
  2. package/dist/src/actions/index.d.ts.map +1 -0
  3. package/dist/src/actions/index.js +76 -0
  4. package/dist/src/actions/index.js.map +1 -0
  5. package/dist/src/cache/document-meta-cache-types.d.ts +114 -0
  6. package/dist/src/cache/document-meta-cache-types.d.ts.map +1 -0
  7. package/dist/src/cache/document-meta-cache-types.js +2 -0
  8. package/dist/src/cache/document-meta-cache-types.js.map +1 -0
  9. package/dist/src/cache/document-meta-cache.d.ts +30 -0
  10. package/dist/src/cache/document-meta-cache.d.ts.map +1 -0
  11. package/dist/src/cache/document-meta-cache.js +128 -0
  12. package/dist/src/cache/document-meta-cache.js.map +1 -0
  13. package/dist/src/cache/kysely-operation-index.d.ts +4 -2
  14. package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
  15. package/dist/src/cache/kysely-operation-index.js +67 -24
  16. package/dist/src/cache/kysely-operation-index.js.map +1 -1
  17. package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
  18. package/dist/src/cache/kysely-write-cache.js +3 -2
  19. package/dist/src/cache/kysely-write-cache.js.map +1 -1
  20. package/dist/src/cache/operation-index-types.d.ts +4 -3
  21. package/dist/src/cache/operation-index-types.d.ts.map +1 -1
  22. package/dist/src/cache/operation-index-types.js.map +1 -1
  23. package/dist/src/client/reactor-client.d.ts +22 -10
  24. package/dist/src/client/reactor-client.d.ts.map +1 -1
  25. package/dist/src/client/reactor-client.js +145 -48
  26. package/dist/src/client/reactor-client.js.map +1 -1
  27. package/dist/src/client/types.d.ts +32 -13
  28. package/dist/src/client/types.d.ts.map +1 -1
  29. package/dist/src/core/reactor-builder.d.ts +19 -12
  30. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  31. package/dist/src/core/reactor-builder.js +127 -37
  32. package/dist/src/core/reactor-builder.js.map +1 -1
  33. package/dist/src/core/{builder.d.ts → reactor-client-builder.d.ts} +20 -4
  34. package/dist/src/core/reactor-client-builder.d.ts.map +1 -0
  35. package/dist/src/core/reactor-client-builder.js +123 -0
  36. package/dist/src/core/reactor-client-builder.js.map +1 -0
  37. package/dist/src/core/reactor.d.ts +14 -16
  38. package/dist/src/core/reactor.d.ts.map +1 -1
  39. package/dist/src/core/reactor.js +101 -110
  40. package/dist/src/core/reactor.js.map +1 -1
  41. package/dist/src/core/types.d.ts +101 -22
  42. package/dist/src/core/types.d.ts.map +1 -1
  43. package/dist/src/core/utils.d.ts +9 -1
  44. package/dist/src/core/utils.d.ts.map +1 -1
  45. package/dist/src/core/utils.js +30 -0
  46. package/dist/src/core/utils.js.map +1 -1
  47. package/dist/src/events/types.d.ts +1 -0
  48. package/dist/src/events/types.d.ts.map +1 -1
  49. package/dist/src/executor/simple-job-executor-manager.d.ts +3 -1
  50. package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
  51. package/dist/src/executor/simple-job-executor-manager.js +10 -7
  52. package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
  53. package/dist/src/executor/simple-job-executor.d.ts +20 -2
  54. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  55. package/dist/src/executor/simple-job-executor.js +400 -219
  56. package/dist/src/executor/simple-job-executor.js.map +1 -1
  57. package/dist/src/executor/types.d.ts +2 -0
  58. package/dist/src/executor/types.d.ts.map +1 -1
  59. package/dist/src/executor/types.js.map +1 -1
  60. package/dist/src/executor/util.d.ts +14 -5
  61. package/dist/src/executor/util.d.ts.map +1 -1
  62. package/dist/src/executor/util.js +36 -9
  63. package/dist/src/executor/util.js.map +1 -1
  64. package/dist/src/index.d.ts +17 -6
  65. package/dist/src/index.d.ts.map +1 -1
  66. package/dist/src/index.js +15 -3
  67. package/dist/src/index.js.map +1 -1
  68. package/dist/src/logging/console.d.ts +23 -0
  69. package/dist/src/logging/console.d.ts.map +1 -0
  70. package/dist/src/logging/console.js +108 -0
  71. package/dist/src/logging/console.js.map +1 -0
  72. package/dist/src/logging/types.d.ts +12 -0
  73. package/dist/src/logging/types.d.ts.map +1 -0
  74. package/dist/src/logging/types.js +2 -0
  75. package/dist/src/logging/types.js.map +1 -0
  76. package/dist/src/processors/index.d.ts +3 -0
  77. package/dist/src/processors/index.d.ts.map +1 -0
  78. package/dist/src/processors/index.js +2 -0
  79. package/dist/src/processors/index.js.map +1 -0
  80. package/dist/src/processors/processor-manager.d.ts +38 -0
  81. package/dist/src/processors/processor-manager.d.ts.map +1 -0
  82. package/dist/src/processors/processor-manager.js +165 -0
  83. package/dist/src/processors/processor-manager.js.map +1 -0
  84. package/dist/src/processors/types.d.ts +63 -0
  85. package/dist/src/processors/types.d.ts.map +1 -0
  86. package/dist/src/processors/types.js +2 -0
  87. package/dist/src/processors/types.js.map +1 -0
  88. package/dist/src/processors/utils.d.ts +10 -0
  89. package/dist/src/processors/utils.d.ts.map +1 -0
  90. package/dist/src/processors/utils.js +58 -0
  91. package/dist/src/processors/utils.js.map +1 -0
  92. package/dist/src/queue/types.d.ts +2 -0
  93. package/dist/src/queue/types.d.ts.map +1 -1
  94. package/dist/src/queue/types.js.map +1 -1
  95. package/dist/src/read-models/base-read-model.d.ts +60 -0
  96. package/dist/src/read-models/base-read-model.d.ts.map +1 -0
  97. package/dist/src/read-models/base-read-model.js +143 -0
  98. package/dist/src/read-models/base-read-model.js.map +1 -0
  99. package/dist/src/read-models/coordinator.d.ts +3 -2
  100. package/dist/src/read-models/coordinator.d.ts.map +1 -1
  101. package/dist/src/read-models/coordinator.js +12 -13
  102. package/dist/src/read-models/coordinator.js.map +1 -1
  103. package/dist/src/read-models/document-view.d.ts +6 -7
  104. package/dist/src/read-models/document-view.d.ts.map +1 -1
  105. package/dist/src/read-models/document-view.js +18 -81
  106. package/dist/src/read-models/document-view.js.map +1 -1
  107. package/dist/src/read-models/types.d.ts +2 -1
  108. package/dist/src/read-models/types.d.ts.map +1 -1
  109. package/dist/src/registry/implementation.d.ts +42 -34
  110. package/dist/src/registry/implementation.d.ts.map +1 -1
  111. package/dist/src/registry/implementation.js +168 -48
  112. package/dist/src/registry/implementation.js.map +1 -1
  113. package/dist/src/registry/interfaces.d.ts +69 -8
  114. package/dist/src/registry/interfaces.d.ts.map +1 -1
  115. package/dist/src/shared/errors.d.ts +24 -0
  116. package/dist/src/shared/errors.d.ts.map +1 -1
  117. package/dist/src/shared/errors.js +42 -0
  118. package/dist/src/shared/errors.js.map +1 -1
  119. package/dist/src/shared/types.d.ts +4 -0
  120. package/dist/src/shared/types.d.ts.map +1 -1
  121. package/dist/src/shared/types.js.map +1 -1
  122. package/dist/src/signer/passthrough-signer.d.ts +9 -3
  123. package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
  124. package/dist/src/signer/passthrough-signer.js +13 -0
  125. package/dist/src/signer/passthrough-signer.js.map +1 -1
  126. package/dist/src/signer/types.d.ts +12 -10
  127. package/dist/src/signer/types.d.ts.map +1 -1
  128. package/dist/src/storage/consistency-aware-legacy-storage.d.ts +33 -0
  129. package/dist/src/storage/consistency-aware-legacy-storage.d.ts.map +1 -0
  130. package/dist/src/storage/consistency-aware-legacy-storage.js +65 -0
  131. package/dist/src/storage/consistency-aware-legacy-storage.js.map +1 -0
  132. package/dist/src/storage/interfaces.d.ts +94 -1
  133. package/dist/src/storage/interfaces.d.ts.map +1 -1
  134. package/dist/src/storage/interfaces.js +2 -2
  135. package/dist/src/storage/interfaces.js.map +1 -1
  136. package/dist/src/storage/kysely/store.d.ts +1 -0
  137. package/dist/src/storage/kysely/store.d.ts.map +1 -1
  138. package/dist/src/storage/kysely/store.js +41 -4
  139. package/dist/src/storage/kysely/store.js.map +1 -1
  140. package/dist/src/storage/kysely/sync-cursor-storage.js +2 -2
  141. package/dist/src/storage/kysely/sync-cursor-storage.js.map +1 -1
  142. package/dist/src/storage/kysely/sync-remote-storage.d.ts.map +1 -1
  143. package/dist/src/storage/kysely/sync-remote-storage.js +11 -12
  144. package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
  145. package/dist/src/storage/kysely/types.d.ts +6 -6
  146. package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -1
  147. package/dist/src/storage/migrations/001_create_operation_table.js +2 -1
  148. package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -1
  149. package/dist/src/storage/migrations/008_create_view_state_table.d.ts +1 -1
  150. package/dist/src/storage/migrations/008_create_view_state_table.d.ts.map +1 -1
  151. package/dist/src/storage/migrations/008_create_view_state_table.js +2 -1
  152. package/dist/src/storage/migrations/008_create_view_state_table.js.map +1 -1
  153. package/dist/src/storage/migrations/009_create_operation_index_tables.js +1 -1
  154. package/dist/src/storage/migrations/009_create_operation_index_tables.js.map +1 -1
  155. package/dist/src/storage/migrations/010_create_sync_tables.js +5 -5
  156. package/dist/src/storage/migrations/010_create_sync_tables.js.map +1 -1
  157. package/dist/src/storage/migrations/migrator.d.ts +3 -2
  158. package/dist/src/storage/migrations/migrator.d.ts.map +1 -1
  159. package/dist/src/storage/migrations/migrator.js +29 -6
  160. package/dist/src/storage/migrations/migrator.js.map +1 -1
  161. package/dist/src/storage/migrations/run-migrations.js +3 -3
  162. package/dist/src/storage/migrations/run-migrations.js.map +1 -1
  163. package/dist/src/storage/txn.d.ts.map +1 -1
  164. package/dist/src/storage/txn.js +2 -3
  165. package/dist/src/storage/txn.js.map +1 -1
  166. package/dist/src/subs/subscription-notification-read-model.d.ts +17 -0
  167. package/dist/src/subs/subscription-notification-read-model.d.ts.map +1 -0
  168. package/dist/src/subs/subscription-notification-read-model.js +62 -0
  169. package/dist/src/subs/subscription-notification-read-model.js.map +1 -0
  170. package/dist/src/sync/channels/composite-channel-factory.d.ts +30 -0
  171. package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -0
  172. package/dist/src/sync/channels/composite-channel-factory.js +87 -0
  173. package/dist/src/sync/channels/composite-channel-factory.js.map +1 -0
  174. package/dist/src/sync/channels/gql-channel-factory.d.ts +25 -0
  175. package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -0
  176. package/dist/src/sync/channels/gql-channel-factory.js +76 -0
  177. package/dist/src/sync/channels/gql-channel-factory.js.map +1 -0
  178. package/dist/src/sync/channels/gql-channel.d.ts +118 -0
  179. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -0
  180. package/dist/src/sync/channels/gql-channel.js +423 -0
  181. package/dist/src/sync/channels/gql-channel.js.map +1 -0
  182. package/dist/src/sync/channels/index.d.ts +4 -1
  183. package/dist/src/sync/channels/index.d.ts.map +1 -1
  184. package/dist/src/sync/channels/index.js +4 -1
  185. package/dist/src/sync/channels/index.js.map +1 -1
  186. package/dist/src/sync/channels/polling-channel.d.ts +39 -0
  187. package/dist/src/sync/channels/polling-channel.d.ts.map +1 -0
  188. package/dist/src/sync/channels/polling-channel.js +72 -0
  189. package/dist/src/sync/channels/polling-channel.js.map +1 -0
  190. package/dist/src/sync/channels/utils.d.ts +17 -2
  191. package/dist/src/sync/channels/utils.d.ts.map +1 -1
  192. package/dist/src/sync/channels/utils.js +76 -6
  193. package/dist/src/sync/channels/utils.js.map +1 -1
  194. package/dist/src/sync/errors.d.ts +1 -1
  195. package/dist/src/sync/errors.d.ts.map +1 -1
  196. package/dist/src/sync/errors.js +2 -2
  197. package/dist/src/sync/errors.js.map +1 -1
  198. package/dist/src/sync/index.d.ts +2 -2
  199. package/dist/src/sync/index.d.ts.map +1 -1
  200. package/dist/src/sync/index.js +2 -2
  201. package/dist/src/sync/index.js.map +1 -1
  202. package/dist/src/sync/interfaces.d.ts +33 -3
  203. package/dist/src/sync/interfaces.d.ts.map +1 -1
  204. package/dist/src/sync/sync-builder.d.ts +4 -2
  205. package/dist/src/sync/sync-builder.d.ts.map +1 -1
  206. package/dist/src/sync/sync-builder.js +12 -2
  207. package/dist/src/sync/sync-builder.js.map +1 -1
  208. package/dist/src/sync/sync-manager.d.ts +7 -3
  209. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  210. package/dist/src/sync/sync-manager.js +79 -10
  211. package/dist/src/sync/sync-manager.js.map +1 -1
  212. package/dist/src/sync/types.d.ts +1 -2
  213. package/dist/src/sync/types.d.ts.map +1 -1
  214. package/dist/src/sync/utils.d.ts +19 -0
  215. package/dist/src/sync/utils.d.ts.map +1 -1
  216. package/dist/src/sync/utils.js +44 -0
  217. package/dist/src/sync/utils.js.map +1 -1
  218. package/package.json +6 -5
  219. package/dist/src/core/builder.d.ts.map +0 -1
  220. package/dist/src/core/builder.js +0 -88
  221. package/dist/src/core/builder.js.map +0 -1
  222. package/dist/src/sync/channels/internal-channel.d.ts +0 -57
  223. package/dist/src/sync/channels/internal-channel.d.ts.map +0 -1
  224. package/dist/src/sync/channels/internal-channel.js +0 -106
  225. package/dist/src/sync/channels/internal-channel.js.map +0 -1
@@ -1,9 +1,17 @@
1
+ import { deriveOperationId, isUndoRedo } from "document-model/core";
1
2
  import { driveCollectionId } from "../cache/operation-index-types.js";
2
3
  import { OperationEventTypes, } from "../events/types.js";
3
- import { DocumentDeletedError } from "../shared/errors.js";
4
+ import { DocumentDeletedError, InvalidSignatureError, } from "../shared/errors.js";
4
5
  import { reshuffleByTimestampAndIndex } from "../utils/reshuffle.js";
5
6
  import { applyDeleteDocumentAction, applyUpgradeDocumentAction, createDocumentFromAction, getNextIndexForScope, } from "./util.js";
6
7
  const MAX_SKIP_THRESHOLD = 100;
8
+ const documentScopeActions = [
9
+ "CREATE_DOCUMENT",
10
+ "DELETE_DOCUMENT",
11
+ "UPGRADE_DOCUMENT",
12
+ "ADD_RELATIONSHIP",
13
+ "REMOVE_RELATIONSHIP",
14
+ ];
7
15
  /**
8
16
  * Simple job executor that processes a job by applying actions through document model reducers.
9
17
  *
@@ -12,6 +20,7 @@ const MAX_SKIP_THRESHOLD = 100;
12
20
  * @see docs/planning/Jobs/reshuffle.md for skip mechanism details
13
21
  */
14
22
  export class SimpleJobExecutor {
23
+ logger;
15
24
  registry;
16
25
  documentStorage;
17
26
  operationStorage;
@@ -19,8 +28,11 @@ export class SimpleJobExecutor {
19
28
  eventBus;
20
29
  writeCache;
21
30
  operationIndex;
31
+ documentMetaCache;
32
+ signatureVerifier;
22
33
  config;
23
- constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, config) {
34
+ constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
35
+ this.logger = logger;
24
36
  this.registry = registry;
25
37
  this.documentStorage = documentStorage;
26
38
  this.operationStorage = operationStorage;
@@ -28,7 +40,10 @@ export class SimpleJobExecutor {
28
40
  this.eventBus = eventBus;
29
41
  this.writeCache = writeCache;
30
42
  this.operationIndex = operationIndex;
43
+ this.documentMetaCache = documentMetaCache;
44
+ this.signatureVerifier = signatureVerifier;
31
45
  this.config = {
46
+ maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
32
47
  maxConcurrency: config.maxConcurrency ?? 1,
33
48
  jobTimeoutMs: config.jobTimeoutMs ?? 30000,
34
49
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
@@ -45,8 +60,23 @@ export class SimpleJobExecutor {
45
60
  const indexTxn = this.operationIndex.start();
46
61
  if (job.kind === "load") {
47
62
  const result = await this.executeLoadJob(job, startTime, indexTxn);
48
- if (result.success) {
49
- await this.operationIndex.commit(indexTxn);
63
+ if (result.success && result.operationsWithContext) {
64
+ const ordinals = await this.operationIndex.commit(indexTxn);
65
+ for (let i = 0; i < result.operationsWithContext.length; i++) {
66
+ result.operationsWithContext[i].context.ordinal = ordinals[i];
67
+ }
68
+ if (result.operationsWithContext.length > 0) {
69
+ const event = {
70
+ jobId: job.id,
71
+ operations: result.operationsWithContext,
72
+ jobMeta: job.meta,
73
+ };
74
+ this.eventBus
75
+ .emit(OperationEventTypes.OPERATION_WRITTEN, event)
76
+ .catch(() => {
77
+ // TODO: Log error
78
+ });
79
+ }
50
80
  }
51
81
  return result;
52
82
  }
@@ -59,11 +89,15 @@ export class SimpleJobExecutor {
59
89
  duration: Date.now() - startTime,
60
90
  };
61
91
  }
62
- await this.operationIndex.commit(indexTxn);
92
+ const ordinals = await this.operationIndex.commit(indexTxn);
63
93
  if (result.operationsWithContext.length > 0) {
94
+ for (let i = 0; i < result.operationsWithContext.length; i++) {
95
+ result.operationsWithContext[i].context.ordinal = ordinals[i];
96
+ }
64
97
  const event = {
65
98
  jobId: job.id,
66
99
  operations: result.operationsWithContext,
100
+ jobMeta: job.meta,
67
101
  };
68
102
  this.eventBus
69
103
  .emit(OperationEventTypes.OPERATION_WRITTEN, event)
@@ -79,189 +113,37 @@ export class SimpleJobExecutor {
79
113
  duration: Date.now() - startTime,
80
114
  };
81
115
  }
82
- async processActions(job, actions, startTime, indexTxn, skipValues) {
116
+ async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
83
117
  const generatedOperations = [];
84
118
  const operationsWithContext = [];
85
- let actionIndex = 0;
86
- for (const action of actions) {
87
- if (action.type === "CREATE_DOCUMENT") {
88
- const result = await this.executeCreateDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
89
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
90
- if (error !== null) {
91
- return {
92
- success: false,
93
- generatedOperations,
94
- operationsWithContext,
95
- error: error.error,
96
- };
97
- }
98
- actionIndex++;
99
- continue;
100
- }
101
- if (action.type === "DELETE_DOCUMENT") {
102
- const result = await this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
103
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
104
- if (error !== null) {
105
- return {
106
- success: false,
107
- generatedOperations,
108
- operationsWithContext,
109
- error: error.error,
110
- };
111
- }
112
- actionIndex++;
113
- continue;
114
- }
115
- if (action.type === "UPGRADE_DOCUMENT") {
116
- const result = await this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
117
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
118
- if (error !== null) {
119
- return {
120
- success: false,
121
- generatedOperations,
122
- operationsWithContext,
123
- error: error.error,
124
- };
125
- }
126
- actionIndex++;
127
- continue;
128
- }
129
- if (action.type === "ADD_RELATIONSHIP") {
130
- const result = await this.executeAddRelationshipAction(job, action, startTime, indexTxn);
131
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
132
- if (error !== null) {
133
- return {
134
- success: false,
135
- generatedOperations,
136
- operationsWithContext,
137
- error: error.error,
138
- };
139
- }
140
- actionIndex++;
141
- continue;
142
- }
143
- if (action.type === "REMOVE_RELATIONSHIP") {
144
- const result = await this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
145
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
146
- if (error !== null) {
147
- return {
148
- success: false,
149
- generatedOperations,
150
- operationsWithContext,
151
- error: error.error,
152
- };
153
- }
154
- actionIndex++;
155
- continue;
156
- }
157
- let document;
158
- try {
159
- document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
160
- }
161
- catch (error) {
162
- return {
163
- success: false,
164
- generatedOperations,
165
- operationsWithContext,
166
- error: error instanceof Error ? error : new Error(String(error)),
167
- };
168
- }
169
- const documentState = document.state.document;
170
- if (documentState.isDeleted) {
171
- return {
172
- success: false,
173
- generatedOperations,
174
- operationsWithContext,
175
- error: new DocumentDeletedError(job.documentId, documentState.deletedAtUtcIso),
176
- };
177
- }
178
- let module;
179
- try {
180
- module = this.registry.getModule(document.header.documentType);
181
- }
182
- catch (error) {
183
- return {
184
- success: false,
185
- generatedOperations,
186
- operationsWithContext,
187
- error: error instanceof Error ? error : new Error(String(error)),
188
- };
189
- }
190
- let updatedDocument;
191
- try {
192
- updatedDocument = module.reducer(document, action);
193
- }
194
- catch (error) {
195
- const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
196
- const enhancedError = new Error(contextMessage);
197
- if (error instanceof Error && error.stack) {
198
- enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
199
- }
200
- return {
201
- success: false,
202
- generatedOperations,
203
- operationsWithContext,
204
- error: enhancedError,
205
- };
206
- }
207
- const scope = job.scope;
208
- const operations = updatedDocument.operations[scope];
209
- if (operations.length === 0) {
210
- return {
211
- success: false,
212
- generatedOperations,
213
- operationsWithContext,
214
- error: new Error("No operation generated from action"),
215
- };
216
- }
217
- const newOperation = operations[operations.length - 1];
218
- if (skipValues && actionIndex < skipValues.length) {
219
- newOperation.skip = skipValues[actionIndex];
220
- }
221
- generatedOperations.push(newOperation);
222
- if (this.config.legacyStorageEnabled) {
223
- try {
224
- await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
225
- }
226
- catch (error) {
227
- return {
228
- success: false,
229
- generatedOperations,
230
- operationsWithContext,
231
- error: error instanceof Error ? error : new Error(String(error)),
232
- };
233
- }
234
- }
235
- const resultingState = JSON.stringify(updatedDocument.state);
236
- try {
237
- await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
238
- txn.addOperations(newOperation);
239
- });
240
- }
241
- catch (error) {
119
+ try {
120
+ await this.verifyActionSignatures(job, actions);
121
+ }
122
+ catch (error) {
123
+ return {
124
+ success: false,
125
+ generatedOperations,
126
+ operationsWithContext,
127
+ error: error instanceof Error ? error : new Error(String(error)),
128
+ };
129
+ }
130
+ for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
131
+ const action = actions[actionIndex];
132
+ const skip = skipValues?.[actionIndex] ?? 0;
133
+ const sourceOperation = sourceOperations?.[actionIndex];
134
+ const isDocumentAction = documentScopeActions.includes(action.type);
135
+ const result = isDocumentAction
136
+ ? await this.executeDocumentAction(job, action, startTime, indexTxn, skip)
137
+ : await this.executeRegularAction(job, action, startTime, indexTxn, skip, sourceOperation);
138
+ const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
139
+ if (error !== null) {
242
140
  return {
243
141
  success: false,
244
142
  generatedOperations,
245
143
  operationsWithContext,
246
- error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
144
+ error: error.error,
247
145
  };
248
146
  }
249
- updatedDocument.header.revision = {
250
- ...updatedDocument.header.revision,
251
- [scope]: newOperation.index + 1,
252
- };
253
- this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
254
- operationsWithContext.push({
255
- operation: newOperation,
256
- context: {
257
- documentId: job.documentId,
258
- scope,
259
- branch: job.branch,
260
- documentType: document.header.documentType,
261
- resultingState,
262
- },
263
- });
264
- actionIndex++;
265
147
  }
266
148
  return {
267
149
  success: true,
@@ -269,6 +151,26 @@ export class SimpleJobExecutor {
269
151
  operationsWithContext,
270
152
  };
271
153
  }
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
+ }
272
174
  /**
273
175
  * Execute a CREATE_DOCUMENT system action.
274
176
  * This creates a new document in storage along with its initial operation.
@@ -293,7 +195,11 @@ export class SimpleJobExecutor {
293
195
  return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
294
196
  }
295
197
  }
296
- const operation = this.createOperation(action, 0, skip);
198
+ const operation = this.createOperation(action, 0, skip, {
199
+ documentId: document.header.id,
200
+ scope: job.scope,
201
+ branch: job.branch,
202
+ });
297
203
  // Legacy: Write the CREATE_DOCUMENT operation to legacy storage
298
204
  if (this.config.legacyStorageEnabled) {
299
205
  try {
@@ -333,6 +239,11 @@ export class SimpleJobExecutor {
333
239
  indexTxn.createCollection(collectionId);
334
240
  indexTxn.addToCollection(collectionId, document.header.id);
335
241
  }
242
+ this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
243
+ state: document.state.document,
244
+ documentType: document.header.documentType,
245
+ documentScopeRevision: 1,
246
+ });
336
247
  return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
337
248
  }
338
249
  /**
@@ -359,7 +270,11 @@ export class SimpleJobExecutor {
359
270
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
360
271
  }
361
272
  const nextIndex = getNextIndexForScope(document, job.scope);
362
- const operation = this.createOperation(action, nextIndex);
273
+ const operation = this.createOperation(action, nextIndex, 0, {
274
+ documentId,
275
+ scope: job.scope,
276
+ branch: job.branch,
277
+ });
363
278
  if (this.config.legacyStorageEnabled) {
364
279
  try {
365
280
  await this.documentStorage.delete(documentId);
@@ -390,11 +305,16 @@ export class SimpleJobExecutor {
390
305
  scope: job.scope,
391
306
  },
392
307
  ]);
308
+ this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
309
+ state: document.state.document,
310
+ documentType: document.header.documentType,
311
+ documentScopeRevision: operation.index + 1,
312
+ });
393
313
  return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
394
314
  }
395
315
  /**
396
316
  * Execute an UPGRADE_DOCUMENT system action.
397
- * This sets the document's initial state from the upgrade action.
317
+ * Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
398
318
  * The operation index is determined from the document's current operation count.
399
319
  */
400
320
  async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
@@ -403,6 +323,8 @@ export class SimpleJobExecutor {
403
323
  return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
404
324
  }
405
325
  const documentId = input.documentId;
326
+ const fromVersion = input.fromVersion;
327
+ const toVersion = input.toVersion;
406
328
  let document;
407
329
  try {
408
330
  document = await this.writeCache.getState(documentId, job.scope, job.branch);
@@ -415,8 +337,35 @@ export class SimpleJobExecutor {
415
337
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
416
338
  }
417
339
  const nextIndex = getNextIndexForScope(document, job.scope);
418
- applyUpgradeDocumentAction(document, action);
419
- const operation = this.createOperation(action, nextIndex, skip);
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
+ });
420
369
  // Write the updated document to legacy storage
421
370
  if (this.config.legacyStorageEnabled) {
422
371
  try {
@@ -447,6 +396,11 @@ export class SimpleJobExecutor {
447
396
  scope: job.scope,
448
397
  },
449
398
  ]);
399
+ this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
400
+ state: document.state.document,
401
+ documentType: document.header.documentType,
402
+ documentScopeRevision: operation.index + 1,
403
+ });
450
404
  return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
451
405
  }
452
406
  async executeAddRelationshipAction(job, action, startTime, indexTxn) {
@@ -467,19 +421,12 @@ export class SimpleJobExecutor {
467
421
  catch (error) {
468
422
  return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
469
423
  }
470
- let targetDoc;
471
- try {
472
- targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
473
- }
474
- catch (error) {
475
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
476
- }
477
- const targetDocState = targetDoc.state.document;
478
- if (targetDocState.isDeleted) {
479
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
480
- }
481
424
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
482
- const operation = this.createOperation(action, nextIndex);
425
+ const operation = this.createOperation(action, nextIndex, 0, {
426
+ documentId: input.sourceId,
427
+ scope: job.scope,
428
+ branch: job.branch,
429
+ });
483
430
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
484
431
  if (writeError !== null) {
485
432
  return writeError;
@@ -513,6 +460,11 @@ export class SimpleJobExecutor {
513
460
  const collectionId = driveCollectionId(job.branch, input.sourceId);
514
461
  indexTxn.addToCollection(collectionId, input.targetId);
515
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
+ });
516
468
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
517
469
  }
518
470
  async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
@@ -531,7 +483,11 @@ export class SimpleJobExecutor {
531
483
  return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
532
484
  }
533
485
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
534
- const operation = this.createOperation(action, nextIndex);
486
+ const operation = this.createOperation(action, nextIndex, 0, {
487
+ documentId: input.sourceId,
488
+ scope: job.scope,
489
+ branch: job.branch,
490
+ });
535
491
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
536
492
  if (writeError !== null) {
537
493
  return writeError;
@@ -565,10 +521,127 @@ export class SimpleJobExecutor {
565
521
  const collectionId = driveCollectionId(job.branch, input.sourceId);
566
522
  indexTxn.removeFromCollection(collectionId, input.targetId);
567
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
+ });
568
529
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
569
530
  }
570
- createOperation(action, index, skip = 0) {
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
+ async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
536
+ let docMeta;
537
+ try {
538
+ docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
539
+ }
540
+ catch (error) {
541
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
542
+ }
543
+ if (docMeta.state.isDeleted) {
544
+ return this.buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
545
+ }
546
+ let document;
547
+ try {
548
+ document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
549
+ }
550
+ catch (error) {
551
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
552
+ }
553
+ let module;
554
+ try {
555
+ // Use document version to get the correct module
556
+ // Version 0 means not yet upgraded - use latest version
557
+ const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
558
+ module = this.registry.getModule(document.header.documentType, moduleVersion);
559
+ }
560
+ catch (error) {
561
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
562
+ }
563
+ let updatedDocument;
564
+ try {
565
+ const reducerOptions = sourceOperation
566
+ ? {
567
+ skip,
568
+ branch: job.branch,
569
+ replayOptions: { operation: sourceOperation },
570
+ }
571
+ : { skip, branch: job.branch };
572
+ updatedDocument = module.reducer(document, action, undefined, reducerOptions);
573
+ }
574
+ catch (error) {
575
+ const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
576
+ const enhancedError = new Error(contextMessage);
577
+ if (error instanceof Error && error.stack) {
578
+ enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
579
+ }
580
+ return this.buildErrorResult(job, enhancedError, startTime);
581
+ }
582
+ const scope = job.scope;
583
+ const operations = updatedDocument.operations[scope];
584
+ if (operations.length === 0) {
585
+ return this.buildErrorResult(job, new Error("No operation generated from action"), startTime);
586
+ }
587
+ const newOperation = operations[operations.length - 1];
588
+ if (!isUndoRedo(action)) {
589
+ newOperation.skip = skip;
590
+ }
591
+ if (this.config.legacyStorageEnabled) {
592
+ try {
593
+ await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
594
+ }
595
+ catch (error) {
596
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
597
+ }
598
+ }
599
+ const resultingState = JSON.stringify({
600
+ ...updatedDocument.state,
601
+ header: updatedDocument.header,
602
+ });
603
+ const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
604
+ if (writeFailResult !== null) {
605
+ return writeFailResult;
606
+ }
607
+ updatedDocument.header.revision = {
608
+ ...updatedDocument.header.revision,
609
+ [scope]: newOperation.index + 1,
610
+ };
611
+ this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
612
+ indexTxn.write([
613
+ {
614
+ ...newOperation,
615
+ documentId: job.documentId,
616
+ documentType: document.header.documentType,
617
+ branch: job.branch,
618
+ scope,
619
+ },
620
+ ]);
621
+ return {
622
+ job,
623
+ success: true,
624
+ operations: [newOperation],
625
+ operationsWithContext: [
626
+ {
627
+ operation: newOperation,
628
+ context: {
629
+ documentId: job.documentId,
630
+ scope,
631
+ branch: job.branch,
632
+ documentType: document.header.documentType,
633
+ resultingState,
634
+ ordinal: 0,
635
+ },
636
+ },
637
+ ],
638
+ duration: Date.now() - startTime,
639
+ };
640
+ }
641
+ createOperation(action, index, skip = 0, context) {
642
+ const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
571
643
  return {
644
+ id,
572
645
  index: index,
573
646
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
574
647
  hash: "",
@@ -589,29 +662,81 @@ export class SimpleJobExecutor {
589
662
  catch {
590
663
  latestRevision = 0;
591
664
  }
592
- const minIncomingIndex = job.operations.reduce((min, operation) => Math.min(min, operation.index), Number.POSITIVE_INFINITY);
593
- const skipCount = minIncomingIndex === Number.POSITIVE_INFINITY
594
- ? 0
595
- : Math.max(0, latestRevision - minIncomingIndex);
596
- if (skipCount > MAX_SKIP_THRESHOLD) {
665
+ let minIncomingIndex = Number.POSITIVE_INFINITY;
666
+ let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
667
+ for (const operation of job.operations) {
668
+ minIncomingIndex = Math.min(minIncomingIndex, operation.index);
669
+ const ts = operation.timestampUtcMs || "";
670
+ if (ts < minIncomingTimestamp) {
671
+ minIncomingTimestamp = ts;
672
+ }
673
+ }
674
+ let conflictingOps = [];
675
+ try {
676
+ const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
677
+ if (conflictingResult.hasMore) {
678
+ return {
679
+ job,
680
+ success: false,
681
+ error: new Error(`Excessive reshuffle detected: more than ${this.config.maxSkipThreshold} conflicting operations found. ` +
682
+ `This indicates a significant divergence between local and incoming operations.`),
683
+ duration: Date.now() - startTime,
684
+ };
685
+ }
686
+ conflictingOps = conflictingResult.items;
687
+ }
688
+ catch {
689
+ conflictingOps = [];
690
+ }
691
+ // Filter out operations that have been superseded by later operations with skip values.
692
+ // An operation at index N is superseded if there exists an operation at index M > N
693
+ // where (M - skip_M) <= N, meaning the later operation's logical index covers N.
694
+ const nonSupersededOps = conflictingOps.filter((op) => {
695
+ for (const laterOp of conflictingOps) {
696
+ if (laterOp.index > op.index && laterOp.skip > 0) {
697
+ const logicalIndex = laterOp.index - laterOp.skip;
698
+ if (logicalIndex <= op.index) {
699
+ return false;
700
+ }
701
+ }
702
+ }
703
+ return true;
704
+ });
705
+ // All non-superseded conflicting operations need to be reshuffled
706
+ const existingOpsToReshuffle = nonSupersededOps;
707
+ // Skip count is the number of existing operations that need to be rewound
708
+ const skipCount = existingOpsToReshuffle.length;
709
+ if (skipCount > this.config.maxSkipThreshold) {
597
710
  return {
598
711
  job,
599
712
  success: false,
600
- error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${MAX_SKIP_THRESHOLD}. ` +
601
- `This indicates an attempt to insert an operation at index ${minIncomingIndex} when the latest revision is ${latestRevision}.`),
713
+ error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${this.config.maxSkipThreshold}. ` +
714
+ `This indicates a significant divergence between local and incoming operations.`),
602
715
  duration: Date.now() - startTime,
603
716
  };
604
717
  }
718
+ // Filter out incoming operations that are duplicates (action already exists locally
719
+ // or appears multiple times in incoming)
720
+ const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
721
+ const seenIncomingActionIds = new Set();
722
+ const incomingOpsToApply = job.operations.filter((op) => {
723
+ if (existingActionIds.has(op.action.id))
724
+ return false;
725
+ if (seenIncomingActionIds.has(op.action.id))
726
+ return false;
727
+ seenIncomingActionIds.add(op.action.id);
728
+ return true;
729
+ });
605
730
  const reshuffledOperations = reshuffleByTimestampAndIndex({
606
731
  index: latestRevision,
607
732
  skip: skipCount,
608
- }, [], job.operations.map((operation) => ({
733
+ }, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
609
734
  ...operation,
610
735
  id: operation.id,
611
736
  })));
612
737
  const actions = reshuffledOperations.map((operation) => operation.action);
613
738
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
614
- const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
739
+ const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
615
740
  if (!result.success) {
616
741
  return {
617
742
  job,
@@ -620,18 +745,10 @@ export class SimpleJobExecutor {
620
745
  duration: Date.now() - startTime,
621
746
  };
622
747
  }
623
- if (result.operationsWithContext.length > 0) {
624
- const event = {
625
- jobId: job.id,
626
- operations: result.operationsWithContext,
627
- };
628
- this.eventBus
629
- .emit(OperationEventTypes.OPERATION_WRITTEN, event)
630
- .catch(() => {
631
- // TODO: log error channel once logging is wired
632
- });
633
- }
634
748
  this.writeCache.invalidate(job.documentId, scope, job.branch);
749
+ if (scope === "document") {
750
+ this.documentMetaCache.invalidate(job.documentId, job.branch);
751
+ }
635
752
  return {
636
753
  job,
637
754
  success: true,
@@ -648,6 +765,8 @@ export class SimpleJobExecutor {
648
765
  return null;
649
766
  }
650
767
  catch (error) {
768
+ this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
769
+ this.writeCache.invalidate(documentId, scope, branch);
651
770
  return {
652
771
  job,
653
772
  success: false,
@@ -679,6 +798,7 @@ export class SimpleJobExecutor {
679
798
  branch: job.branch,
680
799
  documentType: documentType,
681
800
  resultingState,
801
+ ordinal: 0,
682
802
  },
683
803
  },
684
804
  ],
@@ -693,6 +813,67 @@ export class SimpleJobExecutor {
693
813
  duration: Date.now() - startTime,
694
814
  };
695
815
  }
816
+ async verifyOperationSignatures(job, operations) {
817
+ if (!this.signatureVerifier) {
818
+ return;
819
+ }
820
+ for (let i = 0; i < operations.length; i++) {
821
+ const operation = operations[i];
822
+ const signer = operation.action.context?.signer;
823
+ if (!signer) {
824
+ continue;
825
+ }
826
+ if (signer.signatures.length === 0) {
827
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
828
+ }
829
+ const publicKey = signer.app.key;
830
+ let isValid = false;
831
+ try {
832
+ isValid = await this.signatureVerifier(operation, publicKey);
833
+ }
834
+ catch (error) {
835
+ const errorMessage = error instanceof Error ? error.message : String(error);
836
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
837
+ }
838
+ if (!isValid) {
839
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
840
+ }
841
+ }
842
+ }
843
+ async verifyActionSignatures(job, actions) {
844
+ if (!this.signatureVerifier) {
845
+ return;
846
+ }
847
+ for (const action of actions) {
848
+ const signer = action.context?.signer;
849
+ if (!signer) {
850
+ continue;
851
+ }
852
+ if (signer.signatures.length === 0) {
853
+ throw new InvalidSignatureError(job.documentId, `Action ${action.id} has signer but no signatures`);
854
+ }
855
+ const publicKey = signer.app.key;
856
+ let isValid = false;
857
+ try {
858
+ const tempOperation = {
859
+ id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
860
+ index: 0,
861
+ timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
862
+ hash: "",
863
+ skip: 0,
864
+ action: action,
865
+ };
866
+ isValid = await this.signatureVerifier(tempOperation, publicKey);
867
+ }
868
+ catch (error) {
869
+ const errorMessage = error instanceof Error ? error.message : String(error);
870
+ throw new InvalidSignatureError(job.documentId, `Action ${action.id} verification failed: ${errorMessage}`);
871
+ }
872
+ if (!isValid) {
873
+ throw new InvalidSignatureError(job.documentId, `Action ${action.id} signature verification returned false`);
874
+ }
875
+ }
876
+ }
696
877
  accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
697
878
  if (!result.success) {
698
879
  return result;