@powerhousedao/reactor 5.1.0-staging.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 (215) 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 +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 +10 -9
  30. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  31. package/dist/src/core/reactor-builder.js +73 -17
  32. package/dist/src/core/reactor-builder.js.map +1 -1
  33. package/dist/src/core/reactor-client-builder.d.ts +10 -1
  34. package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
  35. package/dist/src/core/reactor-client-builder.js +16 -1
  36. package/dist/src/core/reactor-client-builder.js.map +1 -1
  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 +22 -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 +17 -3
  54. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  55. package/dist/src/executor/simple-job-executor.js +329 -221
  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 +12 -5
  65. package/dist/src/index.d.ts.map +1 -1
  66. package/dist/src/index.js +10 -2
  67. package/dist/src/index.js.map +1 -1
  68. package/dist/src/logging/console.d.ts +11 -1
  69. package/dist/src/logging/console.d.ts.map +1 -1
  70. package/dist/src/logging/console.js +45 -14
  71. package/dist/src/logging/console.js.map +1 -1
  72. package/dist/src/logging/types.d.ts +1 -0
  73. package/dist/src/logging/types.d.ts.map +1 -1
  74. package/dist/src/processors/index.d.ts +3 -0
  75. package/dist/src/processors/index.d.ts.map +1 -0
  76. package/dist/src/processors/index.js +2 -0
  77. package/dist/src/processors/index.js.map +1 -0
  78. package/dist/src/processors/processor-manager.d.ts +38 -0
  79. package/dist/src/processors/processor-manager.d.ts.map +1 -0
  80. package/dist/src/processors/processor-manager.js +165 -0
  81. package/dist/src/processors/processor-manager.js.map +1 -0
  82. package/dist/src/processors/types.d.ts +63 -0
  83. package/dist/src/processors/types.d.ts.map +1 -0
  84. package/dist/src/processors/types.js +2 -0
  85. package/dist/src/processors/types.js.map +1 -0
  86. package/dist/src/processors/utils.d.ts +10 -0
  87. package/dist/src/processors/utils.d.ts.map +1 -0
  88. package/dist/src/processors/utils.js +58 -0
  89. package/dist/src/processors/utils.js.map +1 -0
  90. package/dist/src/queue/types.d.ts +2 -0
  91. package/dist/src/queue/types.d.ts.map +1 -1
  92. package/dist/src/queue/types.js.map +1 -1
  93. package/dist/src/read-models/base-read-model.d.ts +60 -0
  94. package/dist/src/read-models/base-read-model.d.ts.map +1 -0
  95. package/dist/src/read-models/base-read-model.js +143 -0
  96. package/dist/src/read-models/base-read-model.js.map +1 -0
  97. package/dist/src/read-models/coordinator.d.ts +3 -2
  98. package/dist/src/read-models/coordinator.d.ts.map +1 -1
  99. package/dist/src/read-models/coordinator.js +12 -13
  100. package/dist/src/read-models/coordinator.js.map +1 -1
  101. package/dist/src/read-models/document-view.d.ts +6 -7
  102. package/dist/src/read-models/document-view.d.ts.map +1 -1
  103. package/dist/src/read-models/document-view.js +18 -81
  104. package/dist/src/read-models/document-view.js.map +1 -1
  105. package/dist/src/read-models/types.d.ts +2 -1
  106. package/dist/src/read-models/types.d.ts.map +1 -1
  107. package/dist/src/registry/implementation.d.ts +42 -34
  108. package/dist/src/registry/implementation.d.ts.map +1 -1
  109. package/dist/src/registry/implementation.js +168 -48
  110. package/dist/src/registry/implementation.js.map +1 -1
  111. package/dist/src/registry/interfaces.d.ts +69 -8
  112. package/dist/src/registry/interfaces.d.ts.map +1 -1
  113. package/dist/src/shared/errors.d.ts +16 -0
  114. package/dist/src/shared/errors.d.ts.map +1 -1
  115. package/dist/src/shared/errors.js +28 -0
  116. package/dist/src/shared/errors.js.map +1 -1
  117. package/dist/src/shared/types.d.ts +4 -0
  118. package/dist/src/shared/types.d.ts.map +1 -1
  119. package/dist/src/shared/types.js.map +1 -1
  120. package/dist/src/signer/passthrough-signer.d.ts +9 -3
  121. package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
  122. package/dist/src/signer/passthrough-signer.js +13 -0
  123. package/dist/src/signer/passthrough-signer.js.map +1 -1
  124. package/dist/src/signer/types.d.ts +2 -22
  125. package/dist/src/signer/types.d.ts.map +1 -1
  126. package/dist/src/storage/consistency-aware-legacy-storage.d.ts +33 -0
  127. package/dist/src/storage/consistency-aware-legacy-storage.d.ts.map +1 -0
  128. package/dist/src/storage/consistency-aware-legacy-storage.js +65 -0
  129. package/dist/src/storage/consistency-aware-legacy-storage.js.map +1 -0
  130. package/dist/src/storage/interfaces.d.ts +94 -1
  131. package/dist/src/storage/interfaces.d.ts.map +1 -1
  132. package/dist/src/storage/interfaces.js +2 -2
  133. package/dist/src/storage/interfaces.js.map +1 -1
  134. package/dist/src/storage/kysely/store.d.ts +1 -0
  135. package/dist/src/storage/kysely/store.d.ts.map +1 -1
  136. package/dist/src/storage/kysely/store.js +41 -4
  137. package/dist/src/storage/kysely/store.js.map +1 -1
  138. package/dist/src/storage/kysely/sync-cursor-storage.js +2 -2
  139. package/dist/src/storage/kysely/sync-cursor-storage.js.map +1 -1
  140. package/dist/src/storage/kysely/sync-remote-storage.js +8 -8
  141. package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
  142. package/dist/src/storage/kysely/types.d.ts +6 -6
  143. package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -1
  144. package/dist/src/storage/migrations/001_create_operation_table.js +2 -1
  145. package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -1
  146. package/dist/src/storage/migrations/008_create_view_state_table.d.ts +1 -1
  147. package/dist/src/storage/migrations/008_create_view_state_table.d.ts.map +1 -1
  148. package/dist/src/storage/migrations/008_create_view_state_table.js +2 -1
  149. package/dist/src/storage/migrations/008_create_view_state_table.js.map +1 -1
  150. package/dist/src/storage/migrations/009_create_operation_index_tables.js +1 -1
  151. package/dist/src/storage/migrations/009_create_operation_index_tables.js.map +1 -1
  152. package/dist/src/storage/migrations/010_create_sync_tables.js +5 -5
  153. package/dist/src/storage/migrations/010_create_sync_tables.js.map +1 -1
  154. package/dist/src/storage/migrations/migrator.d.ts +3 -2
  155. package/dist/src/storage/migrations/migrator.d.ts.map +1 -1
  156. package/dist/src/storage/migrations/migrator.js +29 -6
  157. package/dist/src/storage/migrations/migrator.js.map +1 -1
  158. package/dist/src/storage/txn.d.ts.map +1 -1
  159. package/dist/src/storage/txn.js +2 -3
  160. package/dist/src/storage/txn.js.map +1 -1
  161. package/dist/src/subs/subscription-notification-read-model.d.ts +17 -0
  162. package/dist/src/subs/subscription-notification-read-model.d.ts.map +1 -0
  163. package/dist/src/subs/subscription-notification-read-model.js +62 -0
  164. package/dist/src/subs/subscription-notification-read-model.js.map +1 -0
  165. package/dist/src/sync/channels/composite-channel-factory.d.ts +30 -0
  166. package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -0
  167. package/dist/src/sync/channels/composite-channel-factory.js +87 -0
  168. package/dist/src/sync/channels/composite-channel-factory.js.map +1 -0
  169. package/dist/src/sync/channels/gql-channel-factory.d.ts +5 -2
  170. package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
  171. package/dist/src/sync/channels/gql-channel-factory.js +8 -2
  172. package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
  173. package/dist/src/sync/channels/gql-channel.d.ts +28 -1
  174. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
  175. package/dist/src/sync/channels/gql-channel.js +150 -22
  176. package/dist/src/sync/channels/gql-channel.js.map +1 -1
  177. package/dist/src/sync/channels/index.d.ts +2 -1
  178. package/dist/src/sync/channels/index.d.ts.map +1 -1
  179. package/dist/src/sync/channels/index.js +2 -1
  180. package/dist/src/sync/channels/index.js.map +1 -1
  181. package/dist/src/sync/channels/polling-channel.d.ts +39 -0
  182. package/dist/src/sync/channels/polling-channel.d.ts.map +1 -0
  183. package/dist/src/sync/channels/polling-channel.js +72 -0
  184. package/dist/src/sync/channels/polling-channel.js.map +1 -0
  185. package/dist/src/sync/channels/utils.d.ts +17 -2
  186. package/dist/src/sync/channels/utils.d.ts.map +1 -1
  187. package/dist/src/sync/channels/utils.js +76 -6
  188. package/dist/src/sync/channels/utils.js.map +1 -1
  189. package/dist/src/sync/errors.d.ts +1 -1
  190. package/dist/src/sync/errors.d.ts.map +1 -1
  191. package/dist/src/sync/errors.js +2 -2
  192. package/dist/src/sync/errors.js.map +1 -1
  193. package/dist/src/sync/index.d.ts +2 -2
  194. package/dist/src/sync/index.d.ts.map +1 -1
  195. package/dist/src/sync/index.js +2 -2
  196. package/dist/src/sync/index.js.map +1 -1
  197. package/dist/src/sync/interfaces.d.ts +16 -1
  198. package/dist/src/sync/interfaces.d.ts.map +1 -1
  199. package/dist/src/sync/sync-builder.d.ts +3 -2
  200. package/dist/src/sync/sync-builder.d.ts.map +1 -1
  201. package/dist/src/sync/sync-builder.js +4 -4
  202. package/dist/src/sync/sync-builder.js.map +1 -1
  203. package/dist/src/sync/sync-manager.d.ts +4 -1
  204. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  205. package/dist/src/sync/sync-manager.js +65 -8
  206. package/dist/src/sync/sync-manager.js.map +1 -1
  207. package/dist/src/sync/utils.d.ts +19 -0
  208. package/dist/src/sync/utils.d.ts.map +1 -1
  209. package/dist/src/sync/utils.js +44 -0
  210. package/dist/src/sync/utils.js.map +1 -1
  211. package/package.json +3 -3
  212. package/dist/src/sync/channels/internal-channel.d.ts +0 -57
  213. package/dist/src/sync/channels/internal-channel.d.ts.map +0 -1
  214. package/dist/src/sync/channels/internal-channel.js +0 -106
  215. 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
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,127 @@ 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
+ 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);
584
643
  return {
644
+ id,
585
645
  index: index,
586
646
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
587
647
  hash: "",
@@ -602,29 +662,81 @@ export class SimpleJobExecutor {
602
662
  catch {
603
663
  latestRevision = 0;
604
664
  }
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) {
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) {
610
710
  return {
611
711
  job,
612
712
  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}.`),
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.`),
615
715
  duration: Date.now() - startTime,
616
716
  };
617
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
+ });
618
730
  const reshuffledOperations = reshuffleByTimestampAndIndex({
619
731
  index: latestRevision,
620
732
  skip: skipCount,
621
- }, [], job.operations.map((operation) => ({
733
+ }, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
622
734
  ...operation,
623
735
  id: operation.id,
624
736
  })));
625
737
  const actions = reshuffledOperations.map((operation) => operation.action);
626
738
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
627
- const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
739
+ const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
628
740
  if (!result.success) {
629
741
  return {
630
742
  job,
@@ -633,18 +745,10 @@ export class SimpleJobExecutor {
633
745
  duration: Date.now() - startTime,
634
746
  };
635
747
  }
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
748
  this.writeCache.invalidate(job.documentId, scope, job.branch);
749
+ if (scope === "document") {
750
+ this.documentMetaCache.invalidate(job.documentId, job.branch);
751
+ }
648
752
  return {
649
753
  job,
650
754
  success: true,
@@ -661,6 +765,8 @@ export class SimpleJobExecutor {
661
765
  return null;
662
766
  }
663
767
  catch (error) {
768
+ this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
769
+ this.writeCache.invalidate(documentId, scope, branch);
664
770
  return {
665
771
  job,
666
772
  success: false,
@@ -692,6 +798,7 @@ export class SimpleJobExecutor {
692
798
  branch: job.branch,
693
799
  documentType: documentType,
694
800
  resultingState,
801
+ ordinal: 0,
695
802
  },
696
803
  },
697
804
  ],
@@ -717,7 +824,7 @@ export class SimpleJobExecutor {
717
824
  continue;
718
825
  }
719
826
  if (signer.signatures.length === 0) {
720
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} has signer but no signatures`);
827
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
721
828
  }
722
829
  const publicKey = signer.app.key;
723
830
  let isValid = false;
@@ -726,10 +833,10 @@ export class SimpleJobExecutor {
726
833
  }
727
834
  catch (error) {
728
835
  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}`);
836
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
730
837
  }
731
838
  if (!isValid) {
732
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} signature verification returned false`);
839
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
733
840
  }
734
841
  }
735
842
  }
@@ -749,6 +856,7 @@ export class SimpleJobExecutor {
749
856
  let isValid = false;
750
857
  try {
751
858
  const tempOperation = {
859
+ id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
752
860
  index: 0,
753
861
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
754
862
  hash: "",