@powerhousedao/reactor 5.0.1-staging.1 → 5.0.1-staging.11

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 (141) hide show
  1. package/dist/src/cache/kysely-write-cache.d.ts +4 -3
  2. package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
  3. package/dist/src/cache/kysely-write-cache.js +12 -12
  4. package/dist/src/cache/kysely-write-cache.js.map +1 -1
  5. package/dist/src/client/reactor-client.d.ts +2 -2
  6. package/dist/src/client/reactor-client.d.ts.map +1 -1
  7. package/dist/src/client/reactor-client.js +8 -8
  8. package/dist/src/client/reactor-client.js.map +1 -1
  9. package/dist/src/client/types.d.ts +4 -4
  10. package/dist/src/client/types.d.ts.map +1 -1
  11. package/dist/src/core/reactor-builder.d.ts +32 -0
  12. package/dist/src/core/reactor-builder.d.ts.map +1 -0
  13. package/dist/src/core/reactor-builder.js +120 -0
  14. package/dist/src/core/reactor-builder.js.map +1 -0
  15. package/dist/src/core/reactor.d.ts +25 -10
  16. package/dist/src/core/reactor.d.ts.map +1 -1
  17. package/dist/src/core/reactor.js +627 -284
  18. package/dist/src/core/reactor.js.map +1 -1
  19. package/dist/src/core/types.d.ts +64 -7
  20. package/dist/src/core/types.d.ts.map +1 -1
  21. package/dist/src/core/utils.d.ts +46 -2
  22. package/dist/src/core/utils.d.ts.map +1 -1
  23. package/dist/src/core/utils.js +119 -0
  24. package/dist/src/core/utils.js.map +1 -1
  25. package/dist/src/executor/simple-job-executor-manager.d.ts +1 -0
  26. package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
  27. package/dist/src/executor/simple-job-executor-manager.js +41 -17
  28. package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
  29. package/dist/src/executor/simple-job-executor.d.ts +14 -2
  30. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  31. package/dist/src/executor/simple-job-executor.js +383 -249
  32. package/dist/src/executor/simple-job-executor.js.map +1 -1
  33. package/dist/src/executor/types.d.ts +2 -0
  34. package/dist/src/executor/types.d.ts.map +1 -1
  35. package/dist/src/executor/types.js.map +1 -1
  36. package/dist/src/executor/util.d.ts +18 -0
  37. package/dist/src/executor/util.d.ts.map +1 -1
  38. package/dist/src/executor/util.js +42 -1
  39. package/dist/src/executor/util.js.map +1 -1
  40. package/dist/src/index.d.ts +9 -6
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +6 -2
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/job-tracker/in-memory-job-tracker.d.ts +3 -2
  45. package/dist/src/job-tracker/in-memory-job-tracker.d.ts.map +1 -1
  46. package/dist/src/job-tracker/in-memory-job-tracker.js +7 -1
  47. package/dist/src/job-tracker/in-memory-job-tracker.js.map +1 -1
  48. package/dist/src/job-tracker/interfaces.d.ts +5 -4
  49. package/dist/src/job-tracker/interfaces.d.ts.map +1 -1
  50. package/dist/src/queue/interfaces.d.ts +5 -4
  51. package/dist/src/queue/interfaces.d.ts.map +1 -1
  52. package/dist/src/queue/job-execution-handle.d.ts +3 -2
  53. package/dist/src/queue/job-execution-handle.d.ts.map +1 -1
  54. package/dist/src/queue/job-execution-handle.js +2 -2
  55. package/dist/src/queue/job-execution-handle.js.map +1 -1
  56. package/dist/src/queue/queue.d.ts +4 -2
  57. package/dist/src/queue/queue.d.ts.map +1 -1
  58. package/dist/src/queue/queue.js +16 -4
  59. package/dist/src/queue/queue.js.map +1 -1
  60. package/dist/src/queue/types.d.ts +12 -6
  61. package/dist/src/queue/types.d.ts.map +1 -1
  62. package/dist/src/queue/types.js.map +1 -1
  63. package/dist/src/read-models/document-view.d.ts +10 -6
  64. package/dist/src/read-models/document-view.d.ts.map +1 -1
  65. package/dist/src/read-models/document-view.js +116 -113
  66. package/dist/src/read-models/document-view.js.map +1 -1
  67. package/dist/src/read-models/types.d.ts +3 -3
  68. package/dist/src/read-models/types.d.ts.map +1 -1
  69. package/dist/src/shared/consistency-tracker.d.ts +48 -0
  70. package/dist/src/shared/consistency-tracker.d.ts.map +1 -0
  71. package/dist/src/shared/consistency-tracker.js +123 -0
  72. package/dist/src/shared/consistency-tracker.js.map +1 -0
  73. package/dist/src/shared/types.d.ts +35 -1
  74. package/dist/src/shared/types.d.ts.map +1 -1
  75. package/dist/src/shared/types.js.map +1 -1
  76. package/dist/src/storage/interfaces.d.ts +148 -2
  77. package/dist/src/storage/interfaces.d.ts.map +1 -1
  78. package/dist/src/storage/interfaces.js.map +1 -1
  79. package/dist/src/storage/kysely/document-indexer.d.ts +28 -0
  80. package/dist/src/storage/kysely/document-indexer.d.ts.map +1 -0
  81. package/dist/src/storage/kysely/document-indexer.js +350 -0
  82. package/dist/src/storage/kysely/document-indexer.js.map +1 -0
  83. package/dist/src/storage/kysely/keyframe-store.d.ts.map +1 -1
  84. package/dist/src/storage/kysely/keyframe-store.js +6 -13
  85. package/dist/src/storage/kysely/keyframe-store.js.map +1 -1
  86. package/dist/src/storage/kysely/store.js +1 -1
  87. package/dist/src/storage/kysely/store.js.map +1 -1
  88. package/dist/src/storage/kysely/types.d.ts +35 -2
  89. package/dist/src/storage/kysely/types.d.ts.map +1 -1
  90. package/dist/src/storage/migrations/001_create_operation_table.d.ts +3 -0
  91. package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -0
  92. package/dist/src/storage/migrations/001_create_operation_table.js +40 -0
  93. package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -0
  94. package/dist/src/storage/migrations/002_create_keyframe_table.d.ts +3 -0
  95. package/dist/src/storage/migrations/002_create_keyframe_table.d.ts.map +1 -0
  96. package/dist/src/storage/migrations/002_create_keyframe_table.js +27 -0
  97. package/dist/src/storage/migrations/002_create_keyframe_table.js.map +1 -0
  98. package/dist/src/storage/migrations/003_create_document_table.d.ts +3 -0
  99. package/dist/src/storage/migrations/003_create_document_table.d.ts.map +1 -0
  100. package/dist/src/storage/migrations/003_create_document_table.js +10 -0
  101. package/dist/src/storage/migrations/003_create_document_table.js.map +1 -0
  102. package/dist/src/storage/migrations/004_create_document_relationship_table.d.ts +3 -0
  103. package/dist/src/storage/migrations/004_create_document_relationship_table.d.ts.map +1 -0
  104. package/dist/src/storage/migrations/004_create_document_relationship_table.js +35 -0
  105. package/dist/src/storage/migrations/004_create_document_relationship_table.js.map +1 -0
  106. package/dist/src/storage/migrations/005_create_indexer_state_table.d.ts +3 -0
  107. package/dist/src/storage/migrations/005_create_indexer_state_table.d.ts.map +1 -0
  108. package/dist/src/storage/migrations/005_create_indexer_state_table.js +10 -0
  109. package/dist/src/storage/migrations/005_create_indexer_state_table.js.map +1 -0
  110. package/dist/src/storage/migrations/006_create_document_snapshot_table.d.ts +3 -0
  111. package/dist/src/storage/migrations/006_create_document_snapshot_table.d.ts.map +1 -0
  112. package/dist/src/storage/migrations/006_create_document_snapshot_table.js +49 -0
  113. package/dist/src/storage/migrations/006_create_document_snapshot_table.js.map +1 -0
  114. package/dist/src/storage/migrations/007_create_slug_mapping_table.d.ts +3 -0
  115. package/dist/src/storage/migrations/007_create_slug_mapping_table.d.ts.map +1 -0
  116. package/dist/src/storage/migrations/007_create_slug_mapping_table.js +24 -0
  117. package/dist/src/storage/migrations/007_create_slug_mapping_table.js.map +1 -0
  118. package/dist/src/storage/migrations/008_create_view_state_table.d.ts +3 -0
  119. package/dist/src/storage/migrations/008_create_view_state_table.d.ts.map +1 -0
  120. package/dist/src/storage/migrations/008_create_view_state_table.js +9 -0
  121. package/dist/src/storage/migrations/008_create_view_state_table.js.map +1 -0
  122. package/dist/src/storage/migrations/index.d.ts +3 -0
  123. package/dist/src/storage/migrations/index.d.ts.map +1 -0
  124. package/dist/src/storage/migrations/index.js +3 -0
  125. package/dist/src/storage/migrations/index.js.map +1 -0
  126. package/dist/src/storage/migrations/migrator.d.ts +5 -0
  127. package/dist/src/storage/migrations/migrator.d.ts.map +1 -0
  128. package/dist/src/storage/migrations/migrator.js +51 -0
  129. package/dist/src/storage/migrations/migrator.js.map +1 -0
  130. package/dist/src/storage/migrations/run-migrations.d.ts +2 -0
  131. package/dist/src/storage/migrations/run-migrations.d.ts.map +1 -0
  132. package/dist/src/storage/migrations/run-migrations.js +58 -0
  133. package/dist/src/storage/migrations/run-migrations.js.map +1 -0
  134. package/dist/src/storage/migrations/types.d.ts +9 -0
  135. package/dist/src/storage/migrations/types.d.ts.map +1 -0
  136. package/dist/src/storage/migrations/types.js +2 -0
  137. package/dist/src/storage/migrations/types.js.map +1 -0
  138. package/dist/src/storage/txn.d.ts.map +1 -1
  139. package/dist/src/storage/txn.js +2 -0
  140. package/dist/src/storage/txn.js.map +1 -1
  141. package/package.json +8 -5
@@ -1,5 +1,6 @@
1
1
  import { OperationEventTypes } from "../events/types.js";
2
2
  import { DocumentDeletedError } from "../shared/errors.js";
3
+ import { reshuffleByTimestampAndIndex } from "../utils/reshuffle.js";
3
4
  import { applyDeleteDocumentAction, applyUpgradeDocumentAction, createDocumentFromAction, getNextIndexForScope, } from "./util.js";
4
5
  /**
5
6
  * Simple job executor that processes a job by applying actions through document model reducers.
@@ -15,13 +16,21 @@ export class SimpleJobExecutor {
15
16
  operationStore;
16
17
  eventBus;
17
18
  writeCache;
18
- constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache) {
19
+ config;
20
+ constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, config) {
19
21
  this.registry = registry;
20
22
  this.documentStorage = documentStorage;
21
23
  this.operationStorage = operationStorage;
22
24
  this.operationStore = operationStore;
23
25
  this.eventBus = eventBus;
24
26
  this.writeCache = writeCache;
27
+ this.config = {
28
+ maxConcurrency: config.maxConcurrency ?? 1,
29
+ jobTimeoutMs: config.jobTimeoutMs ?? 30000,
30
+ retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
31
+ retryMaxDelayMs: config.retryMaxDelayMs ?? 5000,
32
+ legacyStorageEnabled: config.legacyStorageEnabled ?? true,
33
+ };
25
34
  }
26
35
  /**
27
36
  * Execute a single job by applying all its actions through the appropriate reducers.
@@ -29,71 +38,129 @@ export class SimpleJobExecutor {
29
38
  */
30
39
  async executeJob(job) {
31
40
  const startTime = Date.now();
41
+ if (job.kind === "load") {
42
+ return await this.executeLoadJob(job, startTime);
43
+ }
44
+ const result = await this.processActions(job, job.actions, startTime);
45
+ if (!result.success) {
46
+ return {
47
+ job,
48
+ success: false,
49
+ error: result.error,
50
+ duration: Date.now() - startTime,
51
+ };
52
+ }
53
+ if (result.operationsWithContext.length > 0) {
54
+ this.eventBus
55
+ .emit(OperationEventTypes.OPERATION_WRITTEN, {
56
+ operations: result.operationsWithContext,
57
+ })
58
+ .catch(() => {
59
+ // TODO: Log error
60
+ });
61
+ }
62
+ return {
63
+ job,
64
+ success: true,
65
+ operations: result.generatedOperations,
66
+ operationsWithContext: result.operationsWithContext,
67
+ duration: Date.now() - startTime,
68
+ };
69
+ }
70
+ async processActions(job, actions, startTime, skipValues) {
32
71
  const generatedOperations = [];
33
72
  const operationsWithContext = [];
34
- // Process each action in the job sequentially
35
- for (const action of job.actions) {
36
- // Handle system actions specially (CREATE_DOCUMENT, DELETE_DOCUMENT, etc.)
73
+ let actionIndex = 0;
74
+ for (const action of actions) {
37
75
  if (action.type === "CREATE_DOCUMENT") {
38
- const result = await this.executeCreateDocumentAction(job, action, startTime);
39
- if (!result.success) {
40
- return result;
41
- }
42
- if (result.operations && result.operations.length > 0) {
43
- generatedOperations.push(...result.operations);
44
- }
45
- if (result.operationsWithContext) {
46
- operationsWithContext.push(...result.operationsWithContext);
76
+ const result = await this.executeCreateDocumentAction(job, action, startTime, skipValues?.[actionIndex]);
77
+ const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
78
+ if (error !== null) {
79
+ return {
80
+ success: false,
81
+ generatedOperations,
82
+ operationsWithContext,
83
+ error: error.error,
84
+ };
47
85
  }
86
+ actionIndex++;
48
87
  continue;
49
88
  }
50
89
  if (action.type === "DELETE_DOCUMENT") {
51
90
  const result = await this.executeDeleteDocumentAction(job, action, startTime);
52
- if (!result.success) {
53
- return result;
54
- }
55
- if (result.operations && result.operations.length > 0) {
56
- generatedOperations.push(...result.operations);
57
- }
58
- if (result.operationsWithContext) {
59
- operationsWithContext.push(...result.operationsWithContext);
91
+ const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
92
+ if (error !== null) {
93
+ return {
94
+ success: false,
95
+ generatedOperations,
96
+ operationsWithContext,
97
+ error: error.error,
98
+ };
60
99
  }
100
+ actionIndex++;
61
101
  continue;
62
102
  }
63
103
  if (action.type === "UPGRADE_DOCUMENT") {
64
- const result = await this.executeUpgradeDocumentAction(job, action, startTime);
65
- if (!result.success) {
66
- return result;
104
+ const result = await this.executeUpgradeDocumentAction(job, action, startTime, skipValues?.[actionIndex]);
105
+ const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
106
+ if (error !== null) {
107
+ return {
108
+ success: false,
109
+ generatedOperations,
110
+ operationsWithContext,
111
+ error: error.error,
112
+ };
67
113
  }
68
- if (result.operations && result.operations.length > 0) {
69
- generatedOperations.push(...result.operations);
114
+ actionIndex++;
115
+ continue;
116
+ }
117
+ if (action.type === "ADD_RELATIONSHIP") {
118
+ const result = await this.executeAddRelationshipAction(job, action, startTime);
119
+ const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
120
+ if (error !== null) {
121
+ return {
122
+ success: false,
123
+ generatedOperations,
124
+ operationsWithContext,
125
+ error: error.error,
126
+ };
70
127
  }
71
- if (result.operationsWithContext) {
72
- operationsWithContext.push(...result.operationsWithContext);
128
+ actionIndex++;
129
+ continue;
130
+ }
131
+ if (action.type === "REMOVE_RELATIONSHIP") {
132
+ const result = await this.executeRemoveRelationshipAction(job, action, startTime);
133
+ const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
134
+ if (error !== null) {
135
+ return {
136
+ success: false,
137
+ generatedOperations,
138
+ operationsWithContext,
139
+ error: error.error,
140
+ };
73
141
  }
142
+ actionIndex++;
74
143
  continue;
75
144
  }
76
- // For regular actions, load the document and apply through reducer
77
145
  let document;
78
146
  try {
79
147
  document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
80
148
  }
81
149
  catch (error) {
82
150
  return {
83
- job,
84
151
  success: false,
152
+ generatedOperations,
153
+ operationsWithContext,
85
154
  error: error instanceof Error ? error : new Error(String(error)),
86
- duration: Date.now() - startTime,
87
155
  };
88
156
  }
89
- // Check if document is deleted
90
157
  const documentState = document.state.document;
91
158
  if (documentState.isDeleted) {
92
159
  return {
93
- job,
94
160
  success: false,
161
+ generatedOperations,
162
+ operationsWithContext,
95
163
  error: new DocumentDeletedError(job.documentId, documentState.deletedAtUtcIso),
96
- duration: Date.now() - startTime,
97
164
  };
98
165
  }
99
166
  let module;
@@ -102,36 +169,58 @@ export class SimpleJobExecutor {
102
169
  }
103
170
  catch (error) {
104
171
  return {
105
- job,
106
172
  success: false,
173
+ generatedOperations,
174
+ operationsWithContext,
107
175
  error: error instanceof Error ? error : new Error(String(error)),
108
- duration: Date.now() - startTime,
109
176
  };
110
177
  }
111
- // Reducer assigns index based on document's current operation count
112
- const updatedDocument = module.reducer(document, action);
113
- const scope = job.scope;
114
- const operations = updatedDocument.operations[scope];
115
- if (operations.length === 0) {
116
- throw new Error("No operation generated from action");
117
- }
118
- const newOperation = operations[operations.length - 1];
119
- generatedOperations.push(newOperation);
120
- // Write the operation to legacy storage
178
+ let updatedDocument;
121
179
  try {
122
- await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
180
+ updatedDocument = module.reducer(document, action);
123
181
  }
124
182
  catch (error) {
183
+ 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)}`;
184
+ const enhancedError = new Error(contextMessage);
185
+ if (error instanceof Error && error.stack) {
186
+ enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
187
+ }
125
188
  return {
126
- job,
127
189
  success: false,
128
- error: error instanceof Error ? error : new Error(String(error)),
129
- duration: Date.now() - startTime,
190
+ generatedOperations,
191
+ operationsWithContext,
192
+ error: enhancedError,
130
193
  };
131
194
  }
132
- // Compute resultingState for passing via context (not persisted)
195
+ const scope = job.scope;
196
+ const operations = updatedDocument.operations[scope];
197
+ if (operations.length === 0) {
198
+ return {
199
+ success: false,
200
+ generatedOperations,
201
+ operationsWithContext,
202
+ error: new Error("No operation generated from action"),
203
+ };
204
+ }
205
+ const newOperation = operations[operations.length - 1];
206
+ if (skipValues && actionIndex < skipValues.length) {
207
+ newOperation.skip = skipValues[actionIndex];
208
+ }
209
+ generatedOperations.push(newOperation);
210
+ if (this.config.legacyStorageEnabled) {
211
+ try {
212
+ await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
213
+ }
214
+ catch (error) {
215
+ return {
216
+ success: false,
217
+ generatedOperations,
218
+ operationsWithContext,
219
+ error: error instanceof Error ? error : new Error(String(error)),
220
+ };
221
+ }
222
+ }
133
223
  const resultingState = JSON.stringify(updatedDocument.state);
134
- // Write the operation to new IOperationStore (dual-writing)
135
224
  try {
136
225
  await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
137
226
  txn.addOperations(newOperation);
@@ -139,10 +228,10 @@ export class SimpleJobExecutor {
139
228
  }
140
229
  catch (error) {
141
230
  return {
142
- job,
143
231
  success: false,
232
+ generatedOperations,
233
+ operationsWithContext,
144
234
  error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
145
- duration: Date.now() - startTime,
146
235
  };
147
236
  }
148
237
  updatedDocument.header.revision = {
@@ -157,25 +246,15 @@ export class SimpleJobExecutor {
157
246
  scope,
158
247
  branch: job.branch,
159
248
  documentType: document.header.documentType,
160
- resultingState, // Ephemeral, passed via events only
249
+ resultingState,
161
250
  },
162
251
  });
163
- }
164
- // Emit event for read models with all operations - non-blocking
165
- if (operationsWithContext.length > 0) {
166
- this.eventBus
167
- .emit(OperationEventTypes.OPERATION_WRITTEN, {
168
- operations: operationsWithContext,
169
- })
170
- .catch(() => {
171
- // TODO: Log error
172
- });
252
+ actionIndex++;
173
253
  }
174
254
  return {
175
- job,
176
255
  success: true,
177
- operations: generatedOperations,
178
- duration: Date.now() - startTime,
256
+ generatedOperations,
257
+ operationsWithContext,
179
258
  };
180
259
  }
181
260
  /**
@@ -183,7 +262,7 @@ export class SimpleJobExecutor {
183
262
  * This creates a new document in storage along with its initial operation.
184
263
  * For a new document, the operation index is always 0.
185
264
  */
186
- async executeCreateDocumentAction(job, action, startTime) {
265
+ async executeCreateDocumentAction(job, action, startTime, skip = 0) {
187
266
  if (job.scope !== "document") {
188
267
  return {
189
268
  job,
@@ -194,36 +273,23 @@ export class SimpleJobExecutor {
194
273
  }
195
274
  const document = createDocumentFromAction(action);
196
275
  // Legacy: Store the document in storage
197
- try {
198
- await this.documentStorage.create(document);
199
- }
200
- catch (error) {
201
- return {
202
- job,
203
- success: false,
204
- error: new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`),
205
- duration: Date.now() - startTime,
206
- };
276
+ if (this.config.legacyStorageEnabled) {
277
+ try {
278
+ await this.documentStorage.create(document);
279
+ }
280
+ catch (error) {
281
+ return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
282
+ }
207
283
  }
208
- // Create the operation with index 0 (first operation for a new document)
209
- const operation = {
210
- index: 0,
211
- timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
212
- hash: "", // Will be computed later
213
- skip: 0, // Always 0 for new operations; skip > 0 only during reshuffle
214
- action: action,
215
- };
284
+ const operation = this.createOperation(action, 0, skip);
216
285
  // Legacy: Write the CREATE_DOCUMENT operation to legacy storage
217
- try {
218
- await this.operationStorage.addDocumentOperations(document.header.id, [operation], document);
219
- }
220
- catch (error) {
221
- return {
222
- job,
223
- success: false,
224
- error: new Error(`Failed to write CREATE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`),
225
- duration: Date.now() - startTime,
226
- };
286
+ if (this.config.legacyStorageEnabled) {
287
+ try {
288
+ await this.operationStorage.addDocumentOperations(document.header.id, [operation], document);
289
+ }
290
+ catch (error) {
291
+ return this.buildErrorResult(job, new Error(`Failed to write CREATE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
292
+ }
227
293
  }
228
294
  // Compute resultingState for passing via context (not persisted)
229
295
  // Include header and all scopes present in the document state (auth, document, etc.)
@@ -233,44 +299,13 @@ export class SimpleJobExecutor {
233
299
  ...document.state,
234
300
  };
235
301
  const resultingState = JSON.stringify(resultingStateObj);
236
- // Write the operation to new IOperationStore (dual-writing)
237
- // Note: resultingState is NOT persisted in IOperationStore
238
- try {
239
- await this.operationStore.apply(document.header.id, document.header.documentType, job.scope, job.branch, operation.index, (txn) => {
240
- txn.addOperations(operation);
241
- });
302
+ const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime);
303
+ if (writeError !== null) {
304
+ return writeError;
242
305
  }
243
- catch (error) {
244
- return {
245
- job,
246
- success: false,
247
- error: new Error(`Failed to write CREATE_DOCUMENT operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
248
- duration: Date.now() - startTime,
249
- };
250
- }
251
- document.header.revision = {
252
- ...document.header.revision,
253
- [job.scope]: operation.index + 1,
254
- };
255
- this.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
256
- return {
257
- job,
258
- success: true,
259
- operations: [operation],
260
- operationsWithContext: [
261
- {
262
- operation,
263
- context: {
264
- documentId: document.header.id,
265
- scope: job.scope,
266
- branch: job.branch,
267
- documentType: document.header.documentType,
268
- resultingState,
269
- },
270
- },
271
- ],
272
- duration: Date.now() - startTime,
273
- };
306
+ this.updateDocumentRevision(document, job.scope, operation.index);
307
+ this.writeCacheState(document.header.id, job.scope, job.branch, operation.index, document);
308
+ return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
274
309
  }
275
310
  /**
276
311
  * Execute a DELETE_DOCUMENT system action.
@@ -280,12 +315,7 @@ export class SimpleJobExecutor {
280
315
  async executeDeleteDocumentAction(job, action, startTime) {
281
316
  const input = action.input;
282
317
  if (!input.documentId) {
283
- return {
284
- job,
285
- success: false,
286
- error: new Error("DELETE_DOCUMENT action requires a documentId in input"),
287
- duration: Date.now() - startTime,
288
- };
318
+ return this.buildErrorResult(job, new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
289
319
  }
290
320
  const documentId = input.documentId;
291
321
  let document;
@@ -293,43 +323,22 @@ export class SimpleJobExecutor {
293
323
  document = await this.writeCache.getState(documentId, job.scope, job.branch);
294
324
  }
295
325
  catch (error) {
296
- return {
297
- job,
298
- success: false,
299
- error: new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`),
300
- duration: Date.now() - startTime,
301
- };
326
+ return this.buildErrorResult(job, new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
302
327
  }
303
328
  // Check if document is already deleted
304
329
  const documentState = document.state.document;
305
330
  if (documentState.isDeleted) {
306
- return {
307
- job,
308
- success: false,
309
- error: new DocumentDeletedError(documentId, documentState.deletedAtUtcIso),
310
- duration: Date.now() - startTime,
311
- };
331
+ return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
312
332
  }
313
- // Determine the next operation index for this scope only (per-scope indexing)
314
333
  const nextIndex = getNextIndexForScope(document, job.scope);
315
- // Create the DELETE_DOCUMENT operation
316
- const operation = {
317
- index: nextIndex,
318
- timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
319
- hash: "", // Will be computed later
320
- skip: 0, // Always 0 for new operations; skip > 0 only during reshuffle
321
- action: action,
322
- };
323
- try {
324
- await this.documentStorage.delete(documentId);
325
- }
326
- catch (error) {
327
- return {
328
- job,
329
- success: false,
330
- error: new Error(`Failed to delete document from legacy storage: ${error instanceof Error ? error.message : String(error)}`),
331
- duration: Date.now() - startTime,
332
- };
334
+ const operation = this.createOperation(action, nextIndex);
335
+ if (this.config.legacyStorageEnabled) {
336
+ try {
337
+ await this.documentStorage.delete(documentId);
338
+ }
339
+ catch (error) {
340
+ return this.buildErrorResult(job, new Error(`Failed to delete document from legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
341
+ }
333
342
  }
334
343
  // Mark the document as deleted in the state for read model indexing
335
344
  applyDeleteDocumentAction(document, action);
@@ -340,131 +349,236 @@ export class SimpleJobExecutor {
340
349
  document: document.state.document,
341
350
  };
342
351
  const resultingState = JSON.stringify(resultingStateObj);
343
- // Write the DELETE_DOCUMENT operation to IOperationStore
344
- // Note: resultingState is NOT persisted in IOperationStore
345
- try {
346
- await this.operationStore.apply(documentId, document.header.documentType, job.scope, job.branch, operation.index, (txn) => {
347
- txn.addOperations(operation);
348
- });
349
- }
350
- catch (error) {
351
- return {
352
- job,
353
- success: false,
354
- error: new Error(`Failed to write DELETE_DOCUMENT operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
355
- duration: Date.now() - startTime,
356
- };
352
+ const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
353
+ if (writeError !== null) {
354
+ return writeError;
357
355
  }
358
- return {
359
- job,
360
- success: true,
361
- operations: [operation],
362
- operationsWithContext: [
363
- {
364
- operation,
365
- context: {
366
- documentId,
367
- scope: job.scope,
368
- branch: job.branch,
369
- documentType: document.header.documentType,
370
- resultingState,
371
- },
372
- },
373
- ],
374
- duration: Date.now() - startTime,
375
- };
356
+ return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
376
357
  }
377
358
  /**
378
359
  * Execute an UPGRADE_DOCUMENT system action.
379
360
  * This sets the document's initial state from the upgrade action.
380
361
  * The operation index is determined from the document's current operation count.
381
362
  */
382
- async executeUpgradeDocumentAction(job, action, startTime) {
363
+ async executeUpgradeDocumentAction(job, action, startTime, skip = 0) {
383
364
  const input = action.input;
384
365
  if (!input.documentId) {
385
- return {
386
- job,
387
- success: false,
388
- error: new Error("UPGRADE_DOCUMENT action requires a documentId in input"),
389
- duration: Date.now() - startTime,
390
- };
366
+ return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
391
367
  }
392
368
  const documentId = input.documentId;
393
- // Load the document from write cache
394
369
  let document;
395
370
  try {
396
371
  document = await this.writeCache.getState(documentId, job.scope, job.branch);
397
372
  }
398
373
  catch (error) {
399
- return {
400
- job,
401
- success: false,
402
- error: new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`),
403
- duration: Date.now() - startTime,
404
- };
374
+ return this.buildErrorResult(job, new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
405
375
  }
406
- // Check if document is deleted
407
376
  const documentState = document.state.document;
408
377
  if (documentState.isDeleted) {
409
- return {
410
- job,
411
- success: false,
412
- error: new DocumentDeletedError(documentId, documentState.deletedAtUtcIso),
413
- duration: Date.now() - startTime,
414
- };
378
+ return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
415
379
  }
416
- // Determine the next operation index for this scope only (per-scope indexing)
417
380
  const nextIndex = getNextIndexForScope(document, job.scope);
418
- // Apply the initialState from the upgrade action
419
- // The initialState from UPGRADE_DOCUMENT should be merged with the existing base state
420
- // to preserve auth and document scopes while adding model-specific scopes (global, local, etc.)
421
381
  applyUpgradeDocumentAction(document, action);
422
- // Create the UPGRADE_DOCUMENT operation with calculated index
423
- const operation = {
424
- index: nextIndex,
382
+ const operation = this.createOperation(action, nextIndex, skip);
383
+ // Write the updated document to legacy storage
384
+ if (this.config.legacyStorageEnabled) {
385
+ try {
386
+ await this.operationStorage.addDocumentOperations(documentId, [operation], document);
387
+ }
388
+ catch (error) {
389
+ return this.buildErrorResult(job, new Error(`Failed to write UPGRADE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
390
+ }
391
+ }
392
+ // Compute resultingState for passing via context (not persisted)
393
+ const resultingStateObj = {
394
+ header: document.header,
395
+ ...document.state,
396
+ };
397
+ const resultingState = JSON.stringify(resultingStateObj);
398
+ const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
399
+ if (writeError !== null) {
400
+ return writeError;
401
+ }
402
+ this.updateDocumentRevision(document, job.scope, operation.index);
403
+ this.writeCacheState(documentId, job.scope, job.branch, operation.index, document);
404
+ return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
405
+ }
406
+ async executeAddRelationshipAction(job, action, startTime) {
407
+ if (job.scope !== "document") {
408
+ return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
409
+ }
410
+ const input = action.input;
411
+ if (!input.sourceId || !input.targetId || !input.relationshipType) {
412
+ return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
413
+ }
414
+ if (input.sourceId === input.targetId) {
415
+ return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)"), startTime);
416
+ }
417
+ let sourceDoc;
418
+ try {
419
+ sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
420
+ }
421
+ catch (error) {
422
+ return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
423
+ }
424
+ let targetDoc;
425
+ try {
426
+ targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
427
+ }
428
+ catch (error) {
429
+ return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
430
+ }
431
+ const targetDocState = targetDoc.state.document;
432
+ if (targetDocState.isDeleted) {
433
+ return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
434
+ }
435
+ const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
436
+ const operation = this.createOperation(action, nextIndex);
437
+ const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
438
+ if (writeError !== null) {
439
+ return writeError;
440
+ }
441
+ sourceDoc.header.lastModifiedAtUtcIso =
442
+ operation.timestampUtcMs || new Date().toISOString();
443
+ this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
444
+ sourceDoc.operations = {
445
+ ...sourceDoc.operations,
446
+ [job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
447
+ };
448
+ const scopeState = sourceDoc.state[job.scope];
449
+ const resultingStateObj = {
450
+ header: structuredClone(sourceDoc.header),
451
+ [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
452
+ };
453
+ const resultingState = JSON.stringify(resultingStateObj);
454
+ this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
455
+ return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
456
+ }
457
+ async executeRemoveRelationshipAction(job, action, startTime) {
458
+ if (job.scope !== "document") {
459
+ return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
460
+ }
461
+ const input = action.input;
462
+ if (!input.sourceId || !input.targetId || !input.relationshipType) {
463
+ return this.buildErrorResult(job, new Error("REMOVE_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
464
+ }
465
+ let sourceDoc;
466
+ try {
467
+ sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
468
+ }
469
+ catch (error) {
470
+ return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
471
+ }
472
+ const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
473
+ const operation = this.createOperation(action, nextIndex);
474
+ const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
475
+ if (writeError !== null) {
476
+ return writeError;
477
+ }
478
+ sourceDoc.header.lastModifiedAtUtcIso =
479
+ operation.timestampUtcMs || new Date().toISOString();
480
+ this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
481
+ sourceDoc.operations = {
482
+ ...sourceDoc.operations,
483
+ [job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
484
+ };
485
+ const scopeState = sourceDoc.state[job.scope];
486
+ const resultingStateObj = {
487
+ header: structuredClone(sourceDoc.header),
488
+ [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
489
+ };
490
+ const resultingState = JSON.stringify(resultingStateObj);
491
+ this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
492
+ return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
493
+ }
494
+ createOperation(action, index, skip = 0) {
495
+ return {
496
+ index: index,
425
497
  timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
426
- hash: "", // Will be computed later
427
- skip: 0, // Always 0 for new operations; skip > 0 only during reshuffle
498
+ hash: "",
499
+ skip: skip,
428
500
  action: action,
429
501
  };
430
- // Write the updated document to legacy storage
502
+ }
503
+ async executeLoadJob(job, startTime) {
504
+ if (job.operations.length === 0) {
505
+ return this.buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
506
+ }
507
+ const scope = job.scope;
508
+ let latestRevision = 0;
431
509
  try {
432
- await this.operationStorage.addDocumentOperations(documentId, [operation], document);
510
+ const revisions = await this.operationStore.getRevisions(job.documentId, job.branch);
511
+ latestRevision = revisions.revision[scope] ?? 0;
433
512
  }
434
513
  catch (error) {
514
+ latestRevision = 0;
515
+ }
516
+ const minIncomingIndex = job.operations.reduce((min, operation) => Math.min(min, operation.index), Number.POSITIVE_INFINITY);
517
+ const skipCount = minIncomingIndex === Number.POSITIVE_INFINITY
518
+ ? 0
519
+ : Math.max(0, latestRevision - minIncomingIndex);
520
+ const reshuffledOperations = reshuffleByTimestampAndIndex({
521
+ index: latestRevision,
522
+ skip: skipCount,
523
+ }, [], job.operations.map((operation) => ({
524
+ ...operation,
525
+ id: operation.id,
526
+ })));
527
+ const actions = reshuffledOperations.map((operation) => operation.action);
528
+ const skipValues = reshuffledOperations.map((operation) => operation.skip);
529
+ const result = await this.processActions(job, actions, startTime, skipValues);
530
+ if (!result.success) {
435
531
  return {
436
532
  job,
437
533
  success: false,
438
- error: new Error(`Failed to write UPGRADE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`),
534
+ error: result.error,
439
535
  duration: Date.now() - startTime,
440
536
  };
441
537
  }
442
- // Compute resultingState for passing via context (not persisted)
443
- const resultingStateObj = {
444
- header: document.header,
445
- ...document.state,
538
+ if (result.operationsWithContext.length > 0) {
539
+ this.eventBus
540
+ .emit(OperationEventTypes.OPERATION_WRITTEN, {
541
+ operations: result.operationsWithContext,
542
+ })
543
+ .catch(() => {
544
+ // TODO: log error channel once logging is wired
545
+ });
546
+ }
547
+ this.writeCache.invalidate(job.documentId, scope, job.branch);
548
+ return {
549
+ job,
550
+ success: true,
551
+ operations: result.generatedOperations,
552
+ operationsWithContext: result.operationsWithContext,
553
+ duration: Date.now() - startTime,
446
554
  };
447
- const resultingState = JSON.stringify(resultingStateObj);
448
- // Write the operation to new IOperationStore (dual-writing)
449
- // Note: resultingState is NOT persisted in IOperationStore
555
+ }
556
+ async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime) {
450
557
  try {
451
- await this.operationStore.apply(documentId, document.header.documentType, job.scope, job.branch, operation.index, (txn) => {
558
+ await this.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
452
559
  txn.addOperations(operation);
453
560
  });
561
+ return null;
454
562
  }
455
563
  catch (error) {
456
564
  return {
457
565
  job,
458
566
  success: false,
459
- error: new Error(`Failed to write UPGRADE_DOCUMENT operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
567
+ error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
460
568
  duration: Date.now() - startTime,
461
569
  };
462
570
  }
571
+ }
572
+ updateDocumentRevision(document, scope, operationIndex) {
463
573
  document.header.revision = {
464
574
  ...document.header.revision,
465
- [job.scope]: operation.index + 1,
575
+ [scope]: operationIndex + 1,
466
576
  };
467
- this.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
577
+ }
578
+ writeCacheState(documentId, scope, branch, operationIndex, document) {
579
+ this.writeCache.putState(documentId, scope, branch, operationIndex, document);
580
+ }
581
+ buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
468
582
  return {
469
583
  job,
470
584
  success: true,
@@ -473,10 +587,10 @@ export class SimpleJobExecutor {
473
587
  {
474
588
  operation,
475
589
  context: {
476
- documentId,
590
+ documentId: documentId,
477
591
  scope: job.scope,
478
592
  branch: job.branch,
479
- documentType: document.header.documentType,
593
+ documentType: documentType,
480
594
  resultingState,
481
595
  },
482
596
  },
@@ -484,5 +598,25 @@ export class SimpleJobExecutor {
484
598
  duration: Date.now() - startTime,
485
599
  };
486
600
  }
601
+ buildErrorResult(job, error, startTime) {
602
+ return {
603
+ job,
604
+ success: false,
605
+ error: error,
606
+ duration: Date.now() - startTime,
607
+ };
608
+ }
609
+ accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
610
+ if (!result.success) {
611
+ return result;
612
+ }
613
+ if (result.operations && result.operations.length > 0) {
614
+ generatedOperations.push(...result.operations);
615
+ }
616
+ if (result.operationsWithContext) {
617
+ operationsWithContext.push(...result.operationsWithContext);
618
+ }
619
+ return null;
620
+ }
487
621
  }
488
622
  //# sourceMappingURL=simple-job-executor.js.map