@powerhousedao/reactor 5.0.3 → 5.0.5

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