@powerhousedao/reactor 5.2.0-staging.9 → 5.3.0-staging.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 (173) 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.d.ts.map +1 -1
  6. package/dist/src/cache/document-meta-cache.js +2 -1
  7. package/dist/src/cache/document-meta-cache.js.map +1 -1
  8. package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
  9. package/dist/src/cache/kysely-operation-index.js +14 -25
  10. package/dist/src/cache/kysely-operation-index.js.map +1 -1
  11. package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
  12. package/dist/src/cache/kysely-write-cache.js +3 -2
  13. package/dist/src/cache/kysely-write-cache.js.map +1 -1
  14. package/dist/src/cache/operation-index-types.d.ts +1 -1
  15. package/dist/src/client/reactor-client.d.ts +16 -4
  16. package/dist/src/client/reactor-client.d.ts.map +1 -1
  17. package/dist/src/client/reactor-client.js +97 -3
  18. package/dist/src/client/reactor-client.js.map +1 -1
  19. package/dist/src/client/types.d.ts +22 -3
  20. package/dist/src/client/types.d.ts.map +1 -1
  21. package/dist/src/core/reactor-builder.d.ts +10 -9
  22. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  23. package/dist/src/core/reactor-builder.js +52 -14
  24. package/dist/src/core/reactor-builder.js.map +1 -1
  25. package/dist/src/core/reactor-client-builder.d.ts +10 -1
  26. package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
  27. package/dist/src/core/reactor-client-builder.js +16 -1
  28. package/dist/src/core/reactor-client-builder.js.map +1 -1
  29. package/dist/src/core/reactor.d.ts +9 -8
  30. package/dist/src/core/reactor.d.ts.map +1 -1
  31. package/dist/src/core/reactor.js +61 -74
  32. package/dist/src/core/reactor.js.map +1 -1
  33. package/dist/src/core/types.d.ts +22 -7
  34. package/dist/src/core/types.d.ts.map +1 -1
  35. package/dist/src/core/utils.d.ts +1 -2
  36. package/dist/src/core/utils.d.ts.map +1 -1
  37. package/dist/src/core/utils.js +1 -1
  38. package/dist/src/core/utils.js.map +1 -1
  39. package/dist/src/events/types.d.ts +1 -0
  40. package/dist/src/events/types.d.ts.map +1 -1
  41. package/dist/src/executor/simple-job-executor-manager.d.ts +3 -1
  42. package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
  43. package/dist/src/executor/simple-job-executor-manager.js +10 -8
  44. package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
  45. package/dist/src/executor/simple-job-executor.d.ts +15 -3
  46. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  47. package/dist/src/executor/simple-job-executor.js +288 -228
  48. package/dist/src/executor/simple-job-executor.js.map +1 -1
  49. package/dist/src/executor/types.d.ts +2 -0
  50. package/dist/src/executor/types.d.ts.map +1 -1
  51. package/dist/src/executor/types.js.map +1 -1
  52. package/dist/src/executor/util.d.ts +14 -5
  53. package/dist/src/executor/util.d.ts.map +1 -1
  54. package/dist/src/executor/util.js +36 -9
  55. package/dist/src/executor/util.js.map +1 -1
  56. package/dist/src/index.d.ts +6 -2
  57. package/dist/src/index.d.ts.map +1 -1
  58. package/dist/src/index.js +7 -1
  59. package/dist/src/index.js.map +1 -1
  60. package/dist/src/logging/console.d.ts +11 -1
  61. package/dist/src/logging/console.d.ts.map +1 -1
  62. package/dist/src/logging/console.js +45 -14
  63. package/dist/src/logging/console.js.map +1 -1
  64. package/dist/src/logging/types.d.ts +1 -0
  65. package/dist/src/logging/types.d.ts.map +1 -1
  66. package/dist/src/processors/index.d.ts +3 -0
  67. package/dist/src/processors/index.d.ts.map +1 -0
  68. package/dist/src/processors/index.js +2 -0
  69. package/dist/src/processors/index.js.map +1 -0
  70. package/dist/src/processors/processor-manager.d.ts +38 -0
  71. package/dist/src/processors/processor-manager.d.ts.map +1 -0
  72. package/dist/src/processors/processor-manager.js +165 -0
  73. package/dist/src/processors/processor-manager.js.map +1 -0
  74. package/dist/src/processors/types.d.ts +63 -0
  75. package/dist/src/processors/types.d.ts.map +1 -0
  76. package/dist/src/processors/types.js +2 -0
  77. package/dist/src/processors/types.js.map +1 -0
  78. package/dist/src/processors/utils.d.ts +10 -0
  79. package/dist/src/processors/utils.d.ts.map +1 -0
  80. package/dist/src/processors/utils.js +58 -0
  81. package/dist/src/processors/utils.js.map +1 -0
  82. package/dist/src/queue/types.d.ts +2 -0
  83. package/dist/src/queue/types.d.ts.map +1 -1
  84. package/dist/src/queue/types.js.map +1 -1
  85. package/dist/src/read-models/coordinator.d.ts +3 -2
  86. package/dist/src/read-models/coordinator.d.ts.map +1 -1
  87. package/dist/src/read-models/coordinator.js +12 -13
  88. package/dist/src/read-models/coordinator.js.map +1 -1
  89. package/dist/src/read-models/document-view.d.ts.map +1 -1
  90. package/dist/src/read-models/document-view.js +2 -0
  91. package/dist/src/read-models/document-view.js.map +1 -1
  92. package/dist/src/registry/implementation.d.ts +42 -34
  93. package/dist/src/registry/implementation.d.ts.map +1 -1
  94. package/dist/src/registry/implementation.js +168 -48
  95. package/dist/src/registry/implementation.js.map +1 -1
  96. package/dist/src/registry/interfaces.d.ts +69 -8
  97. package/dist/src/registry/interfaces.d.ts.map +1 -1
  98. package/dist/src/shared/errors.d.ts +16 -0
  99. package/dist/src/shared/errors.d.ts.map +1 -1
  100. package/dist/src/shared/errors.js +28 -0
  101. package/dist/src/shared/errors.js.map +1 -1
  102. package/dist/src/shared/types.d.ts +4 -0
  103. package/dist/src/shared/types.d.ts.map +1 -1
  104. package/dist/src/shared/types.js.map +1 -1
  105. package/dist/src/signer/passthrough-signer.d.ts +9 -3
  106. package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
  107. package/dist/src/signer/passthrough-signer.js +13 -0
  108. package/dist/src/signer/passthrough-signer.js.map +1 -1
  109. package/dist/src/signer/types.d.ts +2 -22
  110. package/dist/src/signer/types.d.ts.map +1 -1
  111. package/dist/src/storage/interfaces.d.ts +13 -1
  112. package/dist/src/storage/interfaces.d.ts.map +1 -1
  113. package/dist/src/storage/interfaces.js +2 -2
  114. package/dist/src/storage/interfaces.js.map +1 -1
  115. package/dist/src/storage/kysely/store.d.ts +1 -0
  116. package/dist/src/storage/kysely/store.d.ts.map +1 -1
  117. package/dist/src/storage/kysely/store.js +40 -4
  118. package/dist/src/storage/kysely/store.js.map +1 -1
  119. package/dist/src/storage/kysely/sync-cursor-storage.js +2 -2
  120. package/dist/src/storage/kysely/sync-cursor-storage.js.map +1 -1
  121. package/dist/src/storage/kysely/sync-remote-storage.js +8 -8
  122. package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
  123. package/dist/src/storage/kysely/types.d.ts +6 -6
  124. package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -1
  125. package/dist/src/storage/migrations/001_create_operation_table.js +2 -1
  126. package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -1
  127. package/dist/src/storage/migrations/009_create_operation_index_tables.js +1 -1
  128. package/dist/src/storage/migrations/009_create_operation_index_tables.js.map +1 -1
  129. package/dist/src/storage/migrations/010_create_sync_tables.js +5 -5
  130. package/dist/src/storage/migrations/010_create_sync_tables.js.map +1 -1
  131. package/dist/src/storage/migrations/migrator.d.ts +3 -2
  132. package/dist/src/storage/migrations/migrator.d.ts.map +1 -1
  133. package/dist/src/storage/migrations/migrator.js +29 -6
  134. package/dist/src/storage/migrations/migrator.js.map +1 -1
  135. package/dist/src/storage/txn.d.ts.map +1 -1
  136. package/dist/src/storage/txn.js +2 -3
  137. package/dist/src/storage/txn.js.map +1 -1
  138. package/dist/src/subs/subscription-notification-read-model.d.ts +17 -0
  139. package/dist/src/subs/subscription-notification-read-model.d.ts.map +1 -0
  140. package/dist/src/subs/subscription-notification-read-model.js +62 -0
  141. package/dist/src/subs/subscription-notification-read-model.js.map +1 -0
  142. package/dist/src/sync/channels/composite-channel-factory.d.ts +3 -0
  143. package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -1
  144. package/dist/src/sync/channels/composite-channel-factory.js +5 -1
  145. package/dist/src/sync/channels/composite-channel-factory.js.map +1 -1
  146. package/dist/src/sync/channels/gql-channel-factory.d.ts +3 -0
  147. package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
  148. package/dist/src/sync/channels/gql-channel-factory.js +5 -1
  149. package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
  150. package/dist/src/sync/channels/gql-channel.d.ts +15 -1
  151. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
  152. package/dist/src/sync/channels/gql-channel.js +77 -16
  153. package/dist/src/sync/channels/gql-channel.js.map +1 -1
  154. package/dist/src/sync/channels/polling-channel.d.ts.map +1 -1
  155. package/dist/src/sync/channels/polling-channel.js +7 -5
  156. package/dist/src/sync/channels/polling-channel.js.map +1 -1
  157. package/dist/src/sync/channels/utils.d.ts +17 -2
  158. package/dist/src/sync/channels/utils.d.ts.map +1 -1
  159. package/dist/src/sync/channels/utils.js +76 -6
  160. package/dist/src/sync/channels/utils.js.map +1 -1
  161. package/dist/src/sync/sync-builder.d.ts +3 -2
  162. package/dist/src/sync/sync-builder.d.ts.map +1 -1
  163. package/dist/src/sync/sync-builder.js +4 -4
  164. package/dist/src/sync/sync-builder.js.map +1 -1
  165. package/dist/src/sync/sync-manager.d.ts +3 -2
  166. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  167. package/dist/src/sync/sync-manager.js +17 -13
  168. package/dist/src/sync/sync-manager.js.map +1 -1
  169. package/dist/src/sync/utils.d.ts +19 -0
  170. package/dist/src/sync/utils.d.ts.map +1 -1
  171. package/dist/src/sync/utils.js +44 -0
  172. package/dist/src/sync/utils.js.map +1 -1
  173. package/package.json +3 -3
@@ -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;
@@ -22,7 +31,8 @@ export class SimpleJobExecutor {
22
31
  documentMetaCache;
23
32
  signatureVerifier;
24
33
  config;
25
- constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
34
+ constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
35
+ this.logger = logger;
26
36
  this.registry = registry;
27
37
  this.documentStorage = documentStorage;
28
38
  this.operationStorage = operationStorage;
@@ -33,6 +43,7 @@ export class SimpleJobExecutor {
33
43
  this.documentMetaCache = documentMetaCache;
34
44
  this.signatureVerifier = signatureVerifier;
35
45
  this.config = {
46
+ maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
36
47
  maxConcurrency: config.maxConcurrency ?? 1,
37
48
  jobTimeoutMs: config.jobTimeoutMs ?? 30000,
38
49
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
@@ -58,6 +69,7 @@ export class SimpleJobExecutor {
58
69
  const event = {
59
70
  jobId: job.id,
60
71
  operations: result.operationsWithContext,
72
+ jobMeta: job.meta,
61
73
  };
62
74
  this.eventBus
63
75
  .emit(OperationEventTypes.OPERATION_WRITTEN, event)
@@ -85,6 +97,7 @@ export class SimpleJobExecutor {
85
97
  const event = {
86
98
  jobId: job.id,
87
99
  operations: result.operationsWithContext,
100
+ jobMeta: job.meta,
88
101
  };
89
102
  this.eventBus
90
103
  .emit(OperationEventTypes.OPERATION_WRITTEN, event)
@@ -100,7 +113,7 @@ export class SimpleJobExecutor {
100
113
  duration: Date.now() - startTime,
101
114
  };
102
115
  }
103
- async processActions(job, actions, startTime, indexTxn, skipValues) {
116
+ async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
104
117
  const generatedOperations = [];
105
118
  const operationsWithContext = [];
106
119
  try {
@@ -114,207 +127,23 @@ export class SimpleJobExecutor {
114
127
  error: error instanceof Error ? error : new Error(String(error)),
115
128
  };
116
129
  }
117
- let actionIndex = 0;
118
- for (const action of actions) {
119
- if (action.type === "CREATE_DOCUMENT") {
120
- const result = await this.executeCreateDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
121
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
122
- if (error !== null) {
123
- return {
124
- success: false,
125
- generatedOperations,
126
- operationsWithContext,
127
- error: error.error,
128
- };
129
- }
130
- actionIndex++;
131
- continue;
132
- }
133
- if (action.type === "DELETE_DOCUMENT") {
134
- const result = await this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
135
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
136
- if (error !== null) {
137
- return {
138
- success: false,
139
- generatedOperations,
140
- operationsWithContext,
141
- error: error.error,
142
- };
143
- }
144
- actionIndex++;
145
- continue;
146
- }
147
- if (action.type === "UPGRADE_DOCUMENT") {
148
- const result = await this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
149
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
150
- if (error !== null) {
151
- return {
152
- success: false,
153
- generatedOperations,
154
- operationsWithContext,
155
- error: error.error,
156
- };
157
- }
158
- actionIndex++;
159
- continue;
160
- }
161
- if (action.type === "ADD_RELATIONSHIP") {
162
- const result = await this.executeAddRelationshipAction(job, action, startTime, indexTxn);
163
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
164
- if (error !== null) {
165
- return {
166
- success: false,
167
- generatedOperations,
168
- operationsWithContext,
169
- error: error.error,
170
- };
171
- }
172
- actionIndex++;
173
- continue;
174
- }
175
- if (action.type === "REMOVE_RELATIONSHIP") {
176
- const result = await this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
177
- const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
178
- if (error !== null) {
179
- return {
180
- success: false,
181
- generatedOperations,
182
- operationsWithContext,
183
- error: error.error,
184
- };
185
- }
186
- actionIndex++;
187
- continue;
188
- }
189
- let docMeta;
190
- try {
191
- docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
192
- }
193
- 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) {
194
140
  return {
195
141
  success: false,
196
142
  generatedOperations,
197
143
  operationsWithContext,
198
- error: error instanceof Error ? error : new Error(String(error)),
144
+ error: error.error,
199
145
  };
200
146
  }
201
- if (docMeta.state.isDeleted) {
202
- return {
203
- success: false,
204
- generatedOperations,
205
- operationsWithContext,
206
- error: new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso),
207
- };
208
- }
209
- let document;
210
- try {
211
- document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
212
- }
213
- catch (error) {
214
- return {
215
- success: false,
216
- generatedOperations,
217
- operationsWithContext,
218
- error: error instanceof Error ? error : new Error(String(error)),
219
- };
220
- }
221
- let module;
222
- try {
223
- module = this.registry.getModule(document.header.documentType);
224
- }
225
- catch (error) {
226
- return {
227
- success: false,
228
- generatedOperations,
229
- operationsWithContext,
230
- error: error instanceof Error ? error : new Error(String(error)),
231
- };
232
- }
233
- let updatedDocument;
234
- try {
235
- updatedDocument = module.reducer(document, action);
236
- }
237
- catch (error) {
238
- 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)}`;
239
- const enhancedError = new Error(contextMessage);
240
- if (error instanceof Error && error.stack) {
241
- enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
242
- }
243
- return {
244
- success: false,
245
- generatedOperations,
246
- operationsWithContext,
247
- error: enhancedError,
248
- };
249
- }
250
- const scope = job.scope;
251
- const operations = updatedDocument.operations[scope];
252
- if (operations.length === 0) {
253
- return {
254
- success: false,
255
- generatedOperations,
256
- operationsWithContext,
257
- error: new Error("No operation generated from action"),
258
- };
259
- }
260
- const newOperation = operations[operations.length - 1];
261
- if (skipValues && actionIndex < skipValues.length) {
262
- newOperation.skip = skipValues[actionIndex];
263
- }
264
- generatedOperations.push(newOperation);
265
- if (this.config.legacyStorageEnabled) {
266
- try {
267
- await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
268
- }
269
- catch (error) {
270
- return {
271
- success: false,
272
- generatedOperations,
273
- operationsWithContext,
274
- error: error instanceof Error ? error : new Error(String(error)),
275
- };
276
- }
277
- }
278
- const resultingState = JSON.stringify(updatedDocument.state);
279
- try {
280
- await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
281
- txn.addOperations(newOperation);
282
- });
283
- }
284
- catch (error) {
285
- return {
286
- success: false,
287
- generatedOperations,
288
- operationsWithContext,
289
- error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
290
- };
291
- }
292
- updatedDocument.header.revision = {
293
- ...updatedDocument.header.revision,
294
- [scope]: newOperation.index + 1,
295
- };
296
- this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
297
- indexTxn.write([
298
- {
299
- ...newOperation,
300
- documentId: job.documentId,
301
- documentType: document.header.documentType,
302
- branch: job.branch,
303
- scope,
304
- },
305
- ]);
306
- operationsWithContext.push({
307
- operation: newOperation,
308
- context: {
309
- documentId: job.documentId,
310
- scope,
311
- branch: job.branch,
312
- documentType: document.header.documentType,
313
- resultingState,
314
- ordinal: 0,
315
- },
316
- });
317
- actionIndex++;
318
147
  }
319
148
  return {
320
149
  success: true,
@@ -322,6 +151,26 @@ export class SimpleJobExecutor {
322
151
  operationsWithContext,
323
152
  };
324
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
+ }
325
174
  /**
326
175
  * Execute a CREATE_DOCUMENT system action.
327
176
  * This creates a new document in storage along with its initial operation.
@@ -346,7 +195,11 @@ export class SimpleJobExecutor {
346
195
  return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
347
196
  }
348
197
  }
349
- 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
+ });
350
203
  // Legacy: Write the CREATE_DOCUMENT operation to legacy storage
351
204
  if (this.config.legacyStorageEnabled) {
352
205
  try {
@@ -417,7 +270,11 @@ export class SimpleJobExecutor {
417
270
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
418
271
  }
419
272
  const nextIndex = getNextIndexForScope(document, job.scope);
420
- 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
+ });
421
278
  if (this.config.legacyStorageEnabled) {
422
279
  try {
423
280
  await this.documentStorage.delete(documentId);
@@ -457,7 +314,7 @@ export class SimpleJobExecutor {
457
314
  }
458
315
  /**
459
316
  * Execute an UPGRADE_DOCUMENT system action.
460
- * 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.
461
318
  * The operation index is determined from the document's current operation count.
462
319
  */
463
320
  async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
@@ -466,6 +323,8 @@ export class SimpleJobExecutor {
466
323
  return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
467
324
  }
468
325
  const documentId = input.documentId;
326
+ const fromVersion = input.fromVersion;
327
+ const toVersion = input.toVersion;
469
328
  let document;
470
329
  try {
471
330
  document = await this.writeCache.getState(documentId, job.scope, job.branch);
@@ -478,8 +337,35 @@ export class SimpleJobExecutor {
478
337
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
479
338
  }
480
339
  const nextIndex = getNextIndexForScope(document, job.scope);
481
- applyUpgradeDocumentAction(document, action);
482
- 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
+ });
483
369
  // Write the updated document to legacy storage
484
370
  if (this.config.legacyStorageEnabled) {
485
371
  try {
@@ -535,19 +421,12 @@ export class SimpleJobExecutor {
535
421
  catch (error) {
536
422
  return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
537
423
  }
538
- let targetDoc;
539
- try {
540
- targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
541
- }
542
- catch (error) {
543
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
544
- }
545
- const targetDocState = targetDoc.state.document;
546
- if (targetDocState.isDeleted) {
547
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
548
- }
549
424
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
550
- 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
+ });
551
430
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
552
431
  if (writeError !== null) {
553
432
  return writeError;
@@ -581,6 +460,11 @@ export class SimpleJobExecutor {
581
460
  const collectionId = driveCollectionId(job.branch, input.sourceId);
582
461
  indexTxn.addToCollection(collectionId, input.targetId);
583
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
+ });
584
468
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
585
469
  }
586
470
  async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
@@ -599,7 +483,11 @@ export class SimpleJobExecutor {
599
483
  return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
600
484
  }
601
485
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
602
- 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
+ });
603
491
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
604
492
  if (writeError !== null) {
605
493
  return writeError;
@@ -633,10 +521,127 @@ export class SimpleJobExecutor {
633
521
  const collectionId = driveCollectionId(job.branch, input.sourceId);
634
522
  indexTxn.removeFromCollection(collectionId, input.targetId);
635
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
+ });
636
529
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
637
530
  }
638
- 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);
639
643
  return {
644
+ id,
640
645
  index: index,
641
646
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
642
647
  hash: "",
@@ -657,29 +662,81 @@ export class SimpleJobExecutor {
657
662
  catch {
658
663
  latestRevision = 0;
659
664
  }
660
- const minIncomingIndex = job.operations.reduce((min, operation) => Math.min(min, operation.index), Number.POSITIVE_INFINITY);
661
- const skipCount = minIncomingIndex === Number.POSITIVE_INFINITY
662
- ? 0
663
- : Math.max(0, latestRevision - minIncomingIndex);
664
- 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) {
665
710
  return {
666
711
  job,
667
712
  success: false,
668
- error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${MAX_SKIP_THRESHOLD}. ` +
669
- `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.`),
670
715
  duration: Date.now() - startTime,
671
716
  };
672
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
+ });
673
730
  const reshuffledOperations = reshuffleByTimestampAndIndex({
674
731
  index: latestRevision,
675
732
  skip: skipCount,
676
- }, [], job.operations.map((operation) => ({
733
+ }, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
677
734
  ...operation,
678
735
  id: operation.id,
679
736
  })));
680
737
  const actions = reshuffledOperations.map((operation) => operation.action);
681
738
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
682
- const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
739
+ const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
683
740
  if (!result.success) {
684
741
  return {
685
742
  job,
@@ -708,6 +765,8 @@ export class SimpleJobExecutor {
708
765
  return null;
709
766
  }
710
767
  catch (error) {
768
+ this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
769
+ this.writeCache.invalidate(documentId, scope, branch);
711
770
  return {
712
771
  job,
713
772
  success: false,
@@ -765,7 +824,7 @@ export class SimpleJobExecutor {
765
824
  continue;
766
825
  }
767
826
  if (signer.signatures.length === 0) {
768
- 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`);
769
828
  }
770
829
  const publicKey = signer.app.key;
771
830
  let isValid = false;
@@ -774,10 +833,10 @@ export class SimpleJobExecutor {
774
833
  }
775
834
  catch (error) {
776
835
  const errorMessage = error instanceof Error ? error.message : String(error);
777
- 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}`);
778
837
  }
779
838
  if (!isValid) {
780
- 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`);
781
840
  }
782
841
  }
783
842
  }
@@ -797,6 +856,7 @@ export class SimpleJobExecutor {
797
856
  let isValid = false;
798
857
  try {
799
858
  const tempOperation = {
859
+ id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
800
860
  index: 0,
801
861
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
802
862
  hash: "",