@powerhousedao/reactor 5.2.0-staging.9 → 6.0.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 (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 +18 -5
  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 +100 -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 +64 -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 +327 -230
  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 +39 -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
- import { reshuffleByTimestampAndIndex } from "../utils/reshuffle.js";
5
+ import { reshuffleByTimestamp } 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) {
194
- return {
195
- success: false,
196
- generatedOperations,
197
- operationsWithContext,
198
- error: error instanceof Error ? error : new Error(String(error)),
199
- };
200
- }
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) {
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) {
226
140
  return {
227
141
  success: false,
228
142
  generatedOperations,
229
143
  operationsWithContext,
230
- error: error instanceof Error ? error : new Error(String(error)),
144
+ error: error.error,
231
145
  };
232
146
  }
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.
@@ -337,6 +186,15 @@ export class SimpleJobExecutor {
337
186
  };
338
187
  }
339
188
  const document = createDocumentFromAction(action);
189
+ // DEBUG: Log protocolVersions during document creation
190
+ const createInput = action.input;
191
+ this.logger.info("DEBUG: CREATE_DOCUMENT: @Data", {
192
+ Data: {
193
+ documentId: document.header.id,
194
+ inputProtocolVersions: createInput.protocolVersions,
195
+ headerProtocolVersions: document.header.protocolVersions,
196
+ },
197
+ });
340
198
  // Legacy: Store the document in storage
341
199
  if (this.config.legacyStorageEnabled) {
342
200
  try {
@@ -346,7 +204,11 @@ export class SimpleJobExecutor {
346
204
  return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
347
205
  }
348
206
  }
349
- const operation = this.createOperation(action, 0, skip);
207
+ const operation = this.createOperation(action, 0, skip, {
208
+ documentId: document.header.id,
209
+ scope: job.scope,
210
+ branch: job.branch,
211
+ });
350
212
  // Legacy: Write the CREATE_DOCUMENT operation to legacy storage
351
213
  if (this.config.legacyStorageEnabled) {
352
214
  try {
@@ -417,7 +279,11 @@ export class SimpleJobExecutor {
417
279
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
418
280
  }
419
281
  const nextIndex = getNextIndexForScope(document, job.scope);
420
- const operation = this.createOperation(action, nextIndex);
282
+ const operation = this.createOperation(action, nextIndex, 0, {
283
+ documentId,
284
+ scope: job.scope,
285
+ branch: job.branch,
286
+ });
421
287
  if (this.config.legacyStorageEnabled) {
422
288
  try {
423
289
  await this.documentStorage.delete(documentId);
@@ -457,7 +323,7 @@ export class SimpleJobExecutor {
457
323
  }
458
324
  /**
459
325
  * Execute an UPGRADE_DOCUMENT system action.
460
- * This sets the document's initial state from the upgrade action.
326
+ * Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
461
327
  * The operation index is determined from the document's current operation count.
462
328
  */
463
329
  async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
@@ -466,6 +332,8 @@ export class SimpleJobExecutor {
466
332
  return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
467
333
  }
468
334
  const documentId = input.documentId;
335
+ const fromVersion = input.fromVersion;
336
+ const toVersion = input.toVersion;
469
337
  let document;
470
338
  try {
471
339
  document = await this.writeCache.getState(documentId, job.scope, job.branch);
@@ -478,8 +346,35 @@ export class SimpleJobExecutor {
478
346
  return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
479
347
  }
480
348
  const nextIndex = getNextIndexForScope(document, job.scope);
481
- applyUpgradeDocumentAction(document, action);
482
- const operation = this.createOperation(action, nextIndex, skip);
349
+ let upgradePath;
350
+ if (fromVersion > 0 && fromVersion < toVersion) {
351
+ try {
352
+ upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
353
+ }
354
+ catch (error) {
355
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
356
+ }
357
+ }
358
+ if (fromVersion === toVersion && fromVersion > 0) {
359
+ return {
360
+ job,
361
+ success: true,
362
+ operations: [],
363
+ operationsWithContext: [],
364
+ duration: Date.now() - startTime,
365
+ };
366
+ }
367
+ try {
368
+ document = applyUpgradeDocumentAction(document, action, upgradePath);
369
+ }
370
+ catch (error) {
371
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
372
+ }
373
+ const operation = this.createOperation(action, nextIndex, skip, {
374
+ documentId,
375
+ scope: job.scope,
376
+ branch: job.branch,
377
+ });
483
378
  // Write the updated document to legacy storage
484
379
  if (this.config.legacyStorageEnabled) {
485
380
  try {
@@ -535,19 +430,12 @@ export class SimpleJobExecutor {
535
430
  catch (error) {
536
431
  return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
537
432
  }
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
433
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
550
- const operation = this.createOperation(action, nextIndex);
434
+ const operation = this.createOperation(action, nextIndex, 0, {
435
+ documentId: input.sourceId,
436
+ scope: job.scope,
437
+ branch: job.branch,
438
+ });
551
439
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
552
440
  if (writeError !== null) {
553
441
  return writeError;
@@ -581,6 +469,11 @@ export class SimpleJobExecutor {
581
469
  const collectionId = driveCollectionId(job.branch, input.sourceId);
582
470
  indexTxn.addToCollection(collectionId, input.targetId);
583
471
  }
472
+ this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
473
+ state: sourceDoc.state.document,
474
+ documentType: sourceDoc.header.documentType,
475
+ documentScopeRevision: operation.index + 1,
476
+ });
584
477
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
585
478
  }
586
479
  async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
@@ -599,7 +492,11 @@ export class SimpleJobExecutor {
599
492
  return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
600
493
  }
601
494
  const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
602
- const operation = this.createOperation(action, nextIndex);
495
+ const operation = this.createOperation(action, nextIndex, 0, {
496
+ documentId: input.sourceId,
497
+ scope: job.scope,
498
+ branch: job.branch,
499
+ });
603
500
  const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
604
501
  if (writeError !== null) {
605
502
  return writeError;
@@ -633,10 +530,149 @@ export class SimpleJobExecutor {
633
530
  const collectionId = driveCollectionId(job.branch, input.sourceId);
634
531
  indexTxn.removeFromCollection(collectionId, input.targetId);
635
532
  }
533
+ this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
534
+ state: sourceDoc.state.document,
535
+ documentType: sourceDoc.header.documentType,
536
+ documentScopeRevision: operation.index + 1,
537
+ });
636
538
  return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
637
539
  }
638
- createOperation(action, index, skip = 0) {
540
+ /**
541
+ * Execute a regular document action by applying it through the document model reducer.
542
+ * If sourceOperation is provided (for load jobs), its id and timestamp are preserved.
543
+ */
544
+ async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
545
+ let docMeta;
546
+ try {
547
+ docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
548
+ }
549
+ catch (error) {
550
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
551
+ }
552
+ if (docMeta.state.isDeleted) {
553
+ return this.buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
554
+ }
555
+ let document;
556
+ try {
557
+ document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
558
+ }
559
+ catch (error) {
560
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
561
+ }
562
+ // DEBUG: Log document state from cache before reducer
563
+ const docOpsFromCache = document.operations[job.scope] || [];
564
+ const protocolVersionFromHeader = document.header.protocolVersions?.["base-reducer"];
565
+ this.logger.info("DEBUG: Document from cache: @Document", {
566
+ documentId: job.documentId,
567
+ scope: job.scope,
568
+ actionType: action.type,
569
+ protocolVersionFromHeader,
570
+ operationCount: docOpsFromCache.length,
571
+ operationIndices: docOpsFromCache.map((op) => op.index),
572
+ operationTypes: docOpsFromCache.map((op) => op.action.type),
573
+ });
574
+ let module;
575
+ try {
576
+ // Use document version to get the correct module
577
+ // Version 0 means not yet upgraded - use latest version
578
+ const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
579
+ module = this.registry.getModule(document.header.documentType, moduleVersion);
580
+ }
581
+ catch (error) {
582
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
583
+ }
584
+ let updatedDocument;
585
+ try {
586
+ const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
587
+ const reducerOptions = sourceOperation
588
+ ? {
589
+ skip,
590
+ branch: job.branch,
591
+ replayOptions: { operation: sourceOperation },
592
+ protocolVersion,
593
+ }
594
+ : { skip, branch: job.branch, protocolVersion };
595
+ updatedDocument = module.reducer(document, action, undefined, reducerOptions);
596
+ }
597
+ catch (error) {
598
+ 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)}`;
599
+ const enhancedError = new Error(contextMessage);
600
+ if (error instanceof Error && error.stack) {
601
+ enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
602
+ }
603
+ return this.buildErrorResult(job, enhancedError, startTime);
604
+ }
605
+ const scope = job.scope;
606
+ const operations = updatedDocument.operations[scope];
607
+ // DEBUG: Log document state after reducer
608
+ this.logger.info("DEBUG: Document after reducer: @Document", {
609
+ documentId: job.documentId,
610
+ scope,
611
+ operationCount: operations.length,
612
+ operationIndices: operations.map((op) => op.index),
613
+ operationTypes: operations.map((op) => op.action.type),
614
+ });
615
+ if (operations.length === 0) {
616
+ return this.buildErrorResult(job, new Error("No operation generated from action"), startTime);
617
+ }
618
+ const newOperation = operations[operations.length - 1];
619
+ if (!isUndoRedo(action)) {
620
+ newOperation.skip = skip;
621
+ }
622
+ if (this.config.legacyStorageEnabled) {
623
+ try {
624
+ await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
625
+ }
626
+ catch (error) {
627
+ return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
628
+ }
629
+ }
630
+ const resultingState = JSON.stringify({
631
+ ...updatedDocument.state,
632
+ header: updatedDocument.header,
633
+ });
634
+ const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
635
+ if (writeFailResult !== null) {
636
+ return writeFailResult;
637
+ }
638
+ updatedDocument.header.revision = {
639
+ ...updatedDocument.header.revision,
640
+ [scope]: newOperation.index + 1,
641
+ };
642
+ this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
643
+ indexTxn.write([
644
+ {
645
+ ...newOperation,
646
+ documentId: job.documentId,
647
+ documentType: document.header.documentType,
648
+ branch: job.branch,
649
+ scope,
650
+ },
651
+ ]);
652
+ return {
653
+ job,
654
+ success: true,
655
+ operations: [newOperation],
656
+ operationsWithContext: [
657
+ {
658
+ operation: newOperation,
659
+ context: {
660
+ documentId: job.documentId,
661
+ scope,
662
+ branch: job.branch,
663
+ documentType: document.header.documentType,
664
+ resultingState,
665
+ ordinal: 0,
666
+ },
667
+ },
668
+ ],
669
+ duration: Date.now() - startTime,
670
+ };
671
+ }
672
+ createOperation(action, index, skip = 0, context) {
673
+ const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
639
674
  return {
675
+ id,
640
676
  index: index,
641
677
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
642
678
  hash: "",
@@ -657,29 +693,87 @@ export class SimpleJobExecutor {
657
693
  catch {
658
694
  latestRevision = 0;
659
695
  }
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) {
696
+ let minIncomingIndex = Number.POSITIVE_INFINITY;
697
+ let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
698
+ for (const operation of job.operations) {
699
+ minIncomingIndex = Math.min(minIncomingIndex, operation.index);
700
+ const ts = operation.timestampUtcMs || "";
701
+ if (ts < minIncomingTimestamp) {
702
+ minIncomingTimestamp = ts;
703
+ }
704
+ }
705
+ let conflictingOps = [];
706
+ try {
707
+ const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
708
+ if (conflictingResult.hasMore) {
709
+ return {
710
+ job,
711
+ success: false,
712
+ error: new Error(`Excessive reshuffle detected: more than ${this.config.maxSkipThreshold} conflicting operations found. ` +
713
+ `This indicates a significant divergence between local and incoming operations.`),
714
+ duration: Date.now() - startTime,
715
+ };
716
+ }
717
+ conflictingOps = conflictingResult.items;
718
+ }
719
+ catch {
720
+ conflictingOps = [];
721
+ }
722
+ // Filter out operations that have been superseded by later operations with skip values.
723
+ // An operation at index N is superseded if there exists an operation at index M > N
724
+ // where (M - skip_M) <= N, meaning the later operation's logical index covers N.
725
+ const nonSupersededOps = conflictingOps.filter((op) => {
726
+ for (const laterOp of conflictingOps) {
727
+ if (laterOp.index > op.index && laterOp.skip > 0) {
728
+ const logicalIndex = laterOp.index - laterOp.skip;
729
+ if (logicalIndex <= op.index) {
730
+ return false;
731
+ }
732
+ }
733
+ }
734
+ return true;
735
+ });
736
+ // All non-superseded conflicting operations need to be reshuffled
737
+ const existingOpsToReshuffle = nonSupersededOps;
738
+ // Skip count is the number of existing operations that need to be rewound
739
+ const skipCount = existingOpsToReshuffle.length;
740
+ if (skipCount > this.config.maxSkipThreshold) {
665
741
  return {
666
742
  job,
667
743
  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}.`),
744
+ error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${this.config.maxSkipThreshold}. ` +
745
+ `This indicates a significant divergence between local and incoming operations.`),
670
746
  duration: Date.now() - startTime,
671
747
  };
672
748
  }
673
- const reshuffledOperations = reshuffleByTimestampAndIndex({
749
+ // Filter out incoming operations that are duplicates (action already exists locally
750
+ // or appears multiple times in incoming)
751
+ const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
752
+ const seenIncomingActionIds = new Set();
753
+ const incomingOpsToApply = job.operations.filter((op) => {
754
+ if (existingActionIds.has(op.action.id))
755
+ return false;
756
+ if (seenIncomingActionIds.has(op.action.id))
757
+ return false;
758
+ seenIncomingActionIds.add(op.action.id);
759
+ return true;
760
+ });
761
+ const reshuffledOperations = reshuffleByTimestamp({
674
762
  index: latestRevision,
675
763
  skip: skipCount,
676
- }, [], job.operations.map((operation) => ({
764
+ }, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
677
765
  ...operation,
678
766
  id: operation.id,
679
767
  })));
768
+ // For v2, all NOOPs have skip=1 - consecutive NOOPs are handled during state rebuild
769
+ for (const operation of reshuffledOperations) {
770
+ if (operation.action.type === "NOOP") {
771
+ operation.skip = 1;
772
+ }
773
+ }
680
774
  const actions = reshuffledOperations.map((operation) => operation.action);
681
775
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
682
- const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
776
+ const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
683
777
  if (!result.success) {
684
778
  return {
685
779
  job,
@@ -708,6 +802,8 @@ export class SimpleJobExecutor {
708
802
  return null;
709
803
  }
710
804
  catch (error) {
805
+ this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
806
+ this.writeCache.invalidate(documentId, scope, branch);
711
807
  return {
712
808
  job,
713
809
  success: false,
@@ -765,7 +861,7 @@ export class SimpleJobExecutor {
765
861
  continue;
766
862
  }
767
863
  if (signer.signatures.length === 0) {
768
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} has signer but no signatures`);
864
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
769
865
  }
770
866
  const publicKey = signer.app.key;
771
867
  let isValid = false;
@@ -774,10 +870,10 @@ export class SimpleJobExecutor {
774
870
  }
775
871
  catch (error) {
776
872
  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}`);
873
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
778
874
  }
779
875
  if (!isValid) {
780
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id ?? "unknown"} at index ${operation.index} signature verification returned false`);
876
+ throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
781
877
  }
782
878
  }
783
879
  }
@@ -797,6 +893,7 @@ export class SimpleJobExecutor {
797
893
  let isValid = false;
798
894
  try {
799
895
  const tempOperation = {
896
+ id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
800
897
  index: 0,
801
898
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
802
899
  hash: "",