@powerhousedao/reactor 5.1.0-dev.4 → 5.1.0-dev.40

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 (222) 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 +77 -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 +16 -4
  24. package/dist/src/client/reactor-client.d.ts.map +1 -1
  25. package/dist/src/client/reactor-client.js +122 -5
  26. package/dist/src/client/reactor-client.js.map +1 -1
  27. package/dist/src/client/types.d.ts +22 -3
  28. package/dist/src/client/types.d.ts.map +1 -1
  29. package/dist/src/core/reactor-builder.d.ts +17 -11
  30. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  31. package/dist/src/core/reactor-builder.js +88 -24
  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 +11 -10
  38. package/dist/src/core/reactor.d.ts.map +1 -1
  39. package/dist/src/core/reactor.js +78 -91
  40. package/dist/src/core/reactor.js.map +1 -1
  41. package/dist/src/core/types.d.ts +40 -7
  42. package/dist/src/core/types.d.ts.map +1 -1
  43. package/dist/src/core/utils.d.ts +1 -2
  44. package/dist/src/core/utils.d.ts.map +1 -1
  45. package/dist/src/core/utils.js +2 -2
  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 +18 -3
  54. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  55. package/dist/src/executor/simple-job-executor.js +327 -221
  56. package/dist/src/executor/simple-job-executor.js.map +1 -1
  57. package/dist/src/executor/types.d.ts +2 -8
  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 +16 -5
  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 +16 -0
  116. package/dist/src/shared/errors.d.ts.map +1 -1
  117. package/dist/src/shared/errors.js +28 -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 +40 -3
  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.js +8 -8
  143. package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
  144. package/dist/src/storage/kysely/types.d.ts +6 -6
  145. package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -1
  146. package/dist/src/storage/migrations/001_create_operation_table.js +2 -1
  147. package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -1
  148. package/dist/src/storage/migrations/008_create_view_state_table.d.ts +1 -1
  149. package/dist/src/storage/migrations/008_create_view_state_table.d.ts.map +1 -1
  150. package/dist/src/storage/migrations/008_create_view_state_table.js +2 -1
  151. package/dist/src/storage/migrations/008_create_view_state_table.js.map +1 -1
  152. package/dist/src/storage/migrations/009_create_operation_index_tables.js +1 -1
  153. package/dist/src/storage/migrations/009_create_operation_index_tables.js.map +1 -1
  154. package/dist/src/storage/migrations/010_create_sync_tables.js +5 -5
  155. package/dist/src/storage/migrations/010_create_sync_tables.js.map +1 -1
  156. package/dist/src/storage/migrations/migrator.d.ts +3 -2
  157. package/dist/src/storage/migrations/migrator.d.ts.map +1 -1
  158. package/dist/src/storage/migrations/migrator.js +29 -6
  159. package/dist/src/storage/migrations/migrator.js.map +1 -1
  160. package/dist/src/storage/migrations/run-migrations.js +3 -3
  161. package/dist/src/storage/migrations/run-migrations.js.map +1 -1
  162. package/dist/src/storage/txn.d.ts.map +1 -1
  163. package/dist/src/storage/txn.js +2 -3
  164. package/dist/src/storage/txn.js.map +1 -1
  165. package/dist/src/subs/subscription-notification-read-model.d.ts +17 -0
  166. package/dist/src/subs/subscription-notification-read-model.d.ts.map +1 -0
  167. package/dist/src/subs/subscription-notification-read-model.js +62 -0
  168. package/dist/src/subs/subscription-notification-read-model.js.map +1 -0
  169. package/dist/src/sync/channels/composite-channel-factory.d.ts +30 -0
  170. package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -0
  171. package/dist/src/sync/channels/composite-channel-factory.js +87 -0
  172. package/dist/src/sync/channels/composite-channel-factory.js.map +1 -0
  173. package/dist/src/sync/channels/gql-channel-factory.d.ts +5 -2
  174. package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
  175. package/dist/src/sync/channels/gql-channel-factory.js +8 -2
  176. package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
  177. package/dist/src/sync/channels/gql-channel.d.ts +28 -1
  178. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
  179. package/dist/src/sync/channels/gql-channel.js +150 -22
  180. package/dist/src/sync/channels/gql-channel.js.map +1 -1
  181. package/dist/src/sync/channels/index.d.ts +2 -1
  182. package/dist/src/sync/channels/index.d.ts.map +1 -1
  183. package/dist/src/sync/channels/index.js +2 -1
  184. package/dist/src/sync/channels/index.js.map +1 -1
  185. package/dist/src/sync/channels/polling-channel.d.ts +39 -0
  186. package/dist/src/sync/channels/polling-channel.d.ts.map +1 -0
  187. package/dist/src/sync/channels/polling-channel.js +72 -0
  188. package/dist/src/sync/channels/polling-channel.js.map +1 -0
  189. package/dist/src/sync/channels/utils.d.ts +17 -2
  190. package/dist/src/sync/channels/utils.d.ts.map +1 -1
  191. package/dist/src/sync/channels/utils.js +76 -6
  192. package/dist/src/sync/channels/utils.js.map +1 -1
  193. package/dist/src/sync/errors.d.ts +1 -1
  194. package/dist/src/sync/errors.d.ts.map +1 -1
  195. package/dist/src/sync/errors.js +2 -2
  196. package/dist/src/sync/errors.js.map +1 -1
  197. package/dist/src/sync/index.d.ts +2 -2
  198. package/dist/src/sync/index.d.ts.map +1 -1
  199. package/dist/src/sync/index.js +2 -2
  200. package/dist/src/sync/index.js.map +1 -1
  201. package/dist/src/sync/interfaces.d.ts +16 -1
  202. package/dist/src/sync/interfaces.d.ts.map +1 -1
  203. package/dist/src/sync/sync-builder.d.ts +3 -2
  204. package/dist/src/sync/sync-builder.d.ts.map +1 -1
  205. package/dist/src/sync/sync-builder.js +4 -4
  206. package/dist/src/sync/sync-builder.js.map +1 -1
  207. package/dist/src/sync/sync-manager.d.ts +4 -1
  208. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  209. package/dist/src/sync/sync-manager.js +65 -8
  210. package/dist/src/sync/sync-manager.js.map +1 -1
  211. package/dist/src/sync/utils.d.ts +19 -0
  212. package/dist/src/sync/utils.d.ts.map +1 -1
  213. package/dist/src/sync/utils.js +44 -0
  214. package/dist/src/sync/utils.js.map +1 -1
  215. package/package.json +4 -4
  216. package/dist/src/core/builder.d.ts.map +0 -1
  217. package/dist/src/core/builder.js +0 -88
  218. package/dist/src/core/builder.js.map +0 -1
  219. package/dist/src/sync/channels/internal-channel.d.ts +0 -57
  220. package/dist/src/sync/channels/internal-channel.d.ts.map +0 -1
  221. package/dist/src/sync/channels/internal-channel.js +0 -106
  222. package/dist/src/sync/channels/internal-channel.js.map +0 -1
@@ -1,9 +1,17 @@
1
+ import { deriveOperationId } from "document-model/core";
1
2
  import { driveCollectionId } from "../cache/operation-index-types.js";
2
3
  import { OperationEventTypes, } from "../events/types.js";
3
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,9 +28,11 @@ export class SimpleJobExecutor {
19
28
  eventBus;
20
29
  writeCache;
21
30
  operationIndex;
31
+ documentMetaCache;
22
32
  signatureVerifier;
23
33
  config;
24
- constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, config, signatureVerifier) {
34
+ constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
35
+ this.logger = logger;
25
36
  this.registry = registry;
26
37
  this.documentStorage = documentStorage;
27
38
  this.operationStorage = operationStorage;
@@ -29,8 +40,10 @@ export class SimpleJobExecutor {
29
40
  this.eventBus = eventBus;
30
41
  this.writeCache = writeCache;
31
42
  this.operationIndex = operationIndex;
43
+ this.documentMetaCache = documentMetaCache;
32
44
  this.signatureVerifier = signatureVerifier;
33
45
  this.config = {
46
+ maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
34
47
  maxConcurrency: config.maxConcurrency ?? 1,
35
48
  jobTimeoutMs: config.jobTimeoutMs ?? 30000,
36
49
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
@@ -47,8 +60,23 @@ export class SimpleJobExecutor {
47
60
  const indexTxn = this.operationIndex.start();
48
61
  if (job.kind === "load") {
49
62
  const result = await this.executeLoadJob(job, startTime, indexTxn);
50
- if (result.success) {
51
- 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
+ }
52
80
  }
53
81
  return result;
54
82
  }
@@ -61,11 +89,15 @@ export class SimpleJobExecutor {
61
89
  duration: Date.now() - startTime,
62
90
  };
63
91
  }
64
- await this.operationIndex.commit(indexTxn);
92
+ const ordinals = await this.operationIndex.commit(indexTxn);
65
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
+ }
66
97
  const event = {
67
98
  jobId: job.id,
68
99
  operations: result.operationsWithContext,
100
+ jobMeta: job.meta,
69
101
  };
70
102
  this.eventBus
71
103
  .emit(OperationEventTypes.OPERATION_WRITTEN, event)
@@ -81,7 +113,7 @@ export class SimpleJobExecutor {
81
113
  duration: Date.now() - startTime,
82
114
  };
83
115
  }
84
- async processActions(job, actions, startTime, indexTxn, skipValues) {
116
+ async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
85
117
  const generatedOperations = [];
86
118
  const operationsWithContext = [];
87
119
  try {
@@ -95,186 +127,23 @@ export class SimpleJobExecutor {
95
127
  error: error instanceof Error ? error : new Error(String(error)),
96
128
  };
97
129
  }
98
- let actionIndex = 0;
99
- for (const action of actions) {
100
- if (action.type === "CREATE_DOCUMENT") {
101
- const result = await this.executeCreateDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
102
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
103
- if (error !== null) {
104
- return {
105
- success: false,
106
- generatedOperations,
107
- operationsWithContext,
108
- error: error.error,
109
- };
110
- }
111
- actionIndex++;
112
- continue;
113
- }
114
- if (action.type === "DELETE_DOCUMENT") {
115
- const result = await this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
116
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
117
- if (error !== null) {
118
- return {
119
- success: false,
120
- generatedOperations,
121
- operationsWithContext,
122
- error: error.error,
123
- };
124
- }
125
- actionIndex++;
126
- continue;
127
- }
128
- if (action.type === "UPGRADE_DOCUMENT") {
129
- const result = await this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
130
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
131
- if (error !== null) {
132
- return {
133
- success: false,
134
- generatedOperations,
135
- operationsWithContext,
136
- error: error.error,
137
- };
138
- }
139
- actionIndex++;
140
- continue;
141
- }
142
- if (action.type === "ADD_RELATIONSHIP") {
143
- const result = await this.executeAddRelationshipAction(job, action, startTime, indexTxn);
144
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
145
- if (error !== null) {
146
- return {
147
- success: false,
148
- generatedOperations,
149
- operationsWithContext,
150
- error: error.error,
151
- };
152
- }
153
- actionIndex++;
154
- continue;
155
- }
156
- if (action.type === "REMOVE_RELATIONSHIP") {
157
- const result = await this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
158
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
159
- if (error !== null) {
160
- return {
161
- success: false,
162
- generatedOperations,
163
- operationsWithContext,
164
- error: error.error,
165
- };
166
- }
167
- actionIndex++;
168
- continue;
169
- }
170
- let document;
171
- try {
172
- document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
173
- }
174
- catch (error) {
175
- return {
176
- success: false,
177
- generatedOperations,
178
- operationsWithContext,
179
- error: error instanceof Error ? error : new Error(String(error)),
180
- };
181
- }
182
- const documentState = document.state.document;
183
- if (documentState.isDeleted) {
184
- return {
185
- success: false,
186
- generatedOperations,
187
- operationsWithContext,
188
- error: new DocumentDeletedError(job.documentId, documentState.deletedAtUtcIso),
189
- };
190
- }
191
- let module;
192
- try {
193
- module = this.registry.getModule(document.header.documentType);
194
- }
195
- catch (error) {
196
- return {
197
- success: false,
198
- generatedOperations,
199
- operationsWithContext,
200
- error: error instanceof Error ? error : new Error(String(error)),
201
- };
202
- }
203
- let updatedDocument;
204
- try {
205
- updatedDocument = module.reducer(document, action);
206
- }
207
- catch (error) {
208
- 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)}`;
209
- const enhancedError = new Error(contextMessage);
210
- if (error instanceof Error && error.stack) {
211
- enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
212
- }
213
- return {
214
- success: false,
215
- generatedOperations,
216
- operationsWithContext,
217
- error: enhancedError,
218
- };
219
- }
220
- const scope = job.scope;
221
- const operations = updatedDocument.operations[scope];
222
- if (operations.length === 0) {
223
- return {
224
- success: false,
225
- generatedOperations,
226
- operationsWithContext,
227
- error: new Error("No operation generated from action"),
228
- };
229
- }
230
- const newOperation = operations[operations.length - 1];
231
- if (skipValues && actionIndex < skipValues.length) {
232
- newOperation.skip = skipValues[actionIndex];
233
- }
234
- generatedOperations.push(newOperation);
235
- if (this.config.legacyStorageEnabled) {
236
- try {
237
- await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
238
- }
239
- catch (error) {
240
- return {
241
- success: false,
242
- generatedOperations,
243
- operationsWithContext,
244
- error: error instanceof Error ? error : new Error(String(error)),
245
- };
246
- }
247
- }
248
- const resultingState = JSON.stringify(updatedDocument.state);
249
- try {
250
- await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
251
- txn.addOperations(newOperation);
252
- });
253
- }
254
- catch (error) {
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) {
255
140
  return {
256
141
  success: false,
257
142
  generatedOperations,
258
143
  operationsWithContext,
259
- error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
144
+ error: error.error,
260
145
  };
261
146
  }
262
- updatedDocument.header.revision = {
263
- ...updatedDocument.header.revision,
264
- [scope]: newOperation.index + 1,
265
- };
266
- this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
267
- operationsWithContext.push({
268
- operation: newOperation,
269
- context: {
270
- documentId: job.documentId,
271
- scope,
272
- branch: job.branch,
273
- documentType: document.header.documentType,
274
- resultingState,
275
- },
276
- });
277
- actionIndex++;
278
147
  }
279
148
  return {
280
149
  success: true,
@@ -282,6 +151,26 @@ export class SimpleJobExecutor {
282
151
  operationsWithContext,
283
152
  };
284
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
+ }
285
174
  /**
286
175
  * Execute a CREATE_DOCUMENT system action.
287
176
  * This creates a new document in storage along with its initial operation.
@@ -306,7 +195,11 @@ export class SimpleJobExecutor {
306
195
  return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
307
196
  }
308
197
  }
309
- 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
+ });
310
203
  // Legacy: Write the CREATE_DOCUMENT operation to legacy storage
311
204
  if (this.config.legacyStorageEnabled) {
312
205
  try {
@@ -346,6 +239,11 @@ export class SimpleJobExecutor {
346
239
  indexTxn.createCollection(collectionId);
347
240
  indexTxn.addToCollection(collectionId, document.header.id);
348
241
  }
242
+ this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
243
+ state: document.state.document,
244
+ documentType: document.header.documentType,
245
+ documentScopeRevision: 1,
246
+ });
349
247
  return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
350
248
  }
351
249
  /**
@@ -372,7 +270,11 @@ export class SimpleJobExecutor {
372
270
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
373
271
  }
374
272
  const nextIndex = getNextIndexForScope(document, job.scope);
375
- 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
+ });
376
278
  if (this.config.legacyStorageEnabled) {
377
279
  try {
378
280
  await this.documentStorage.delete(documentId);
@@ -403,11 +305,16 @@ export class SimpleJobExecutor {
403
305
  scope: job.scope,
404
306
  },
405
307
  ]);
308
+ this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
309
+ state: document.state.document,
310
+ documentType: document.header.documentType,
311
+ documentScopeRevision: operation.index + 1,
312
+ });
406
313
  return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
407
314
  }
408
315
  /**
409
316
  * Execute an UPGRADE_DOCUMENT system action.
410
- * 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.
411
318
  * The operation index is determined from the document's current operation count.
412
319
  */
413
320
  async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
@@ -416,6 +323,8 @@ export class SimpleJobExecutor {
416
323
  return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
417
324
  }
418
325
  const documentId = input.documentId;
326
+ const fromVersion = input.fromVersion;
327
+ const toVersion = input.toVersion;
419
328
  let document;
420
329
  try {
421
330
  document = await this.writeCache.getState(documentId, job.scope, job.branch);
@@ -428,8 +337,35 @@ export class SimpleJobExecutor {
428
337
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
429
338
  }
430
339
  const nextIndex = getNextIndexForScope(document, job.scope);
431
- applyUpgradeDocumentAction(document, action);
432
- 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
+ });
433
369
  // Write the updated document to legacy storage
434
370
  if (this.config.legacyStorageEnabled) {
435
371
  try {
@@ -460,6 +396,11 @@ export class SimpleJobExecutor {
460
396
  scope: job.scope,
461
397
  },
462
398
  ]);
399
+ this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
400
+ state: document.state.document,
401
+ documentType: document.header.documentType,
402
+ documentScopeRevision: operation.index + 1,
403
+ });
463
404
  return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
464
405
  }
465
406
  async executeAddRelationshipAction(job, action, startTime, indexTxn) {
@@ -480,19 +421,12 @@ export class SimpleJobExecutor {
480
421
  catch (error) {
481
422
  return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
482
423
  }
483
- let targetDoc;
484
- try {
485
- targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
486
- }
487
- catch (error) {
488
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
489
- }
490
- const targetDocState = targetDoc.state.document;
491
- if (targetDocState.isDeleted) {
492
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
493
- }
494
424
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
495
- 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
+ });
496
430
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
497
431
  if (writeError !== null) {
498
432
  return writeError;
@@ -526,6 +460,11 @@ export class SimpleJobExecutor {
526
460
  const collectionId = driveCollectionId(job.branch, input.sourceId);
527
461
  indexTxn.addToCollection(collectionId, input.targetId);
528
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
+ });
529
468
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
530
469
  }
531
470
  async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
@@ -544,7 +483,11 @@ export class SimpleJobExecutor {
544
483
  return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
545
484
  }
546
485
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
547
- 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
+ });
548
491
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
549
492
  if (writeError !== null) {
550
493
  return writeError;
@@ -578,10 +521,125 @@ export class SimpleJobExecutor {
578
521
  const collectionId = driveCollectionId(job.branch, input.sourceId);
579
522
  indexTxn.removeFromCollection(collectionId, input.targetId);
580
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
+ });
581
529
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
582
530
  }
583
- 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
+ newOperation.skip = skip;
589
+ if (this.config.legacyStorageEnabled) {
590
+ try {
591
+ await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
592
+ }
593
+ catch (error) {
594
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
595
+ }
596
+ }
597
+ const resultingState = JSON.stringify({
598
+ ...updatedDocument.state,
599
+ header: updatedDocument.header,
600
+ });
601
+ const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
602
+ if (writeFailResult !== null) {
603
+ return writeFailResult;
604
+ }
605
+ updatedDocument.header.revision = {
606
+ ...updatedDocument.header.revision,
607
+ [scope]: newOperation.index + 1,
608
+ };
609
+ this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
610
+ indexTxn.write([
611
+ {
612
+ ...newOperation,
613
+ documentId: job.documentId,
614
+ documentType: document.header.documentType,
615
+ branch: job.branch,
616
+ scope,
617
+ },
618
+ ]);
619
+ return {
620
+ job,
621
+ success: true,
622
+ operations: [newOperation],
623
+ operationsWithContext: [
624
+ {
625
+ operation: newOperation,
626
+ context: {
627
+ documentId: job.documentId,
628
+ scope,
629
+ branch: job.branch,
630
+ documentType: document.header.documentType,
631
+ resultingState,
632
+ ordinal: 0,
633
+ },
634
+ },
635
+ ],
636
+ duration: Date.now() - startTime,
637
+ };
638
+ }
639
+ createOperation(action, index, skip = 0, context) {
640
+ const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
584
641
  return {
642
+ id,
585
643
  index: index,
586
644
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
587
645
  hash: "",
@@ -602,29 +660,81 @@ export class SimpleJobExecutor {
602
660
  catch {
603
661
  latestRevision = 0;
604
662
  }
605
- const minIncomingIndex = job.operations.reduce((min, operation) => Math.min(min, operation.index), Number.POSITIVE_INFINITY);
606
- const skipCount = minIncomingIndex === Number.POSITIVE_INFINITY
607
- ? 0
608
- : Math.max(0, latestRevision - minIncomingIndex);
609
- if (skipCount > MAX_SKIP_THRESHOLD) {
663
+ let minIncomingIndex = Number.POSITIVE_INFINITY;
664
+ let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
665
+ for (const operation of job.operations) {
666
+ minIncomingIndex = Math.min(minIncomingIndex, operation.index);
667
+ const ts = operation.timestampUtcMs || "";
668
+ if (ts < minIncomingTimestamp) {
669
+ minIncomingTimestamp = ts;
670
+ }
671
+ }
672
+ let conflictingOps = [];
673
+ try {
674
+ const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
675
+ if (conflictingResult.hasMore) {
676
+ return {
677
+ job,
678
+ success: false,
679
+ error: new Error(`Excessive reshuffle detected: more than ${this.config.maxSkipThreshold} conflicting operations found. ` +
680
+ `This indicates a significant divergence between local and incoming operations.`),
681
+ duration: Date.now() - startTime,
682
+ };
683
+ }
684
+ conflictingOps = conflictingResult.items;
685
+ }
686
+ catch {
687
+ conflictingOps = [];
688
+ }
689
+ // Filter out operations that have been superseded by later operations with skip values.
690
+ // An operation at index N is superseded if there exists an operation at index M > N
691
+ // where (M - skip_M) <= N, meaning the later operation's logical index covers N.
692
+ const nonSupersededOps = conflictingOps.filter((op) => {
693
+ for (const laterOp of conflictingOps) {
694
+ if (laterOp.index > op.index && laterOp.skip > 0) {
695
+ const logicalIndex = laterOp.index - laterOp.skip;
696
+ if (logicalIndex <= op.index) {
697
+ return false;
698
+ }
699
+ }
700
+ }
701
+ return true;
702
+ });
703
+ // All non-superseded conflicting operations need to be reshuffled
704
+ const existingOpsToReshuffle = nonSupersededOps;
705
+ // Skip count is the number of existing operations that need to be rewound
706
+ const skipCount = existingOpsToReshuffle.length;
707
+ if (skipCount > this.config.maxSkipThreshold) {
610
708
  return {
611
709
  job,
612
710
  success: false,
613
- error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${MAX_SKIP_THRESHOLD}. ` +
614
- `This indicates an attempt to insert an operation at index ${minIncomingIndex} when the latest revision is ${latestRevision}.`),
711
+ error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${this.config.maxSkipThreshold}. ` +
712
+ `This indicates a significant divergence between local and incoming operations.`),
615
713
  duration: Date.now() - startTime,
616
714
  };
617
715
  }
716
+ // Filter out incoming operations that are duplicates (action already exists locally
717
+ // or appears multiple times in incoming)
718
+ const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
719
+ const seenIncomingActionIds = new Set();
720
+ const incomingOpsToApply = job.operations.filter((op) => {
721
+ if (existingActionIds.has(op.action.id))
722
+ return false;
723
+ if (seenIncomingActionIds.has(op.action.id))
724
+ return false;
725
+ seenIncomingActionIds.add(op.action.id);
726
+ return true;
727
+ });
618
728
  const reshuffledOperations = reshuffleByTimestampAndIndex({
619
729
  index: latestRevision,
620
730
  skip: skipCount,
621
- }, [], job.operations.map((operation) => ({
731
+ }, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
622
732
  ...operation,
623
733
  id: operation.id,
624
734
  })));
625
735
  const actions = reshuffledOperations.map((operation) => operation.action);
626
736
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
627
- const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
737
+ const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
628
738
  if (!result.success) {
629
739
  return {
630
740
  job,
@@ -633,18 +743,10 @@ export class SimpleJobExecutor {
633
743
  duration: Date.now() - startTime,
634
744
  };
635
745
  }
636
- if (result.operationsWithContext.length > 0) {
637
- const event = {
638
- jobId: job.id,
639
- operations: result.operationsWithContext,
640
- };
641
- this.eventBus
642
- .emit(OperationEventTypes.OPERATION_WRITTEN, event)
643
- .catch(() => {
644
- // TODO: log error channel once logging is wired
645
- });
646
- }
647
746
  this.writeCache.invalidate(job.documentId, scope, job.branch);
747
+ if (scope === "document") {
748
+ this.documentMetaCache.invalidate(job.documentId, job.branch);
749
+ }
648
750
  return {
649
751
  job,
650
752
  success: true,
@@ -661,6 +763,8 @@ export class SimpleJobExecutor {
661
763
  return null;
662
764
  }
663
765
  catch (error) {
766
+ this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
767
+ this.writeCache.invalidate(documentId, scope, branch);
664
768
  return {
665
769
  job,
666
770
  success: false,
@@ -692,6 +796,7 @@ export class SimpleJobExecutor {
692
796
  branch: job.branch,
693
797
  documentType: documentType,
694
798
  resultingState,
799
+ ordinal: 0,
695
800
  },
696
801
  },
697
802
  ],
@@ -717,7 +822,7 @@ export class SimpleJobExecutor {
717
822
  continue;
718
823
  }
719
824
  if (signer.signatures.length === 0) {
720
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} has signer but no signatures`);
825
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
721
826
  }
722
827
  const publicKey = signer.app.key;
723
828
  let isValid = false;
@@ -726,10 +831,10 @@ export class SimpleJobExecutor {
726
831
  }
727
832
  catch (error) {
728
833
  const errorMessage = error instanceof Error ? error.message : String(error);
729
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} verification failed: ${errorMessage}`);
834
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
730
835
  }
731
836
  if (!isValid) {
732
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} signature verification returned false`);
837
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
733
838
  }
734
839
  }
735
840
  }
@@ -749,6 +854,7 @@ export class SimpleJobExecutor {
749
854
  let isValid = false;
750
855
  try {
751
856
  const tempOperation = {
857
+ id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
752
858
  index: 0,
753
859
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
754
860
  hash: "",