@powerhousedao/reactor 5.0.0 → 5.0.1-staging.10

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