@sixb/core 0.1.14 → 0.1.15

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 (206) hide show
  1. package/dist/actions/index.js +21 -21
  2. package/dist/agents/index.js +33 -33
  3. package/dist/agents/index.js.map +1 -1
  4. package/dist/agents/streams/index.js +2 -2
  5. package/dist/agents/streams/internal.js +2 -2
  6. package/dist/auth/index.js +3 -3
  7. package/dist/authorization/index.js +2 -2
  8. package/dist/bootstrap/index.js +36 -36
  9. package/dist/{chunk-dv4ct8mp.js → chunk-1jvq82rp.js} +3 -3
  10. package/dist/{chunk-16jenmzw.js → chunk-2tz5vnvz.js} +28 -28
  11. package/dist/{chunk-16jenmzw.js.map → chunk-2tz5vnvz.js.map} +1 -1
  12. package/dist/{chunk-728fp090.js → chunk-3c90jvfj.js} +2 -2
  13. package/dist/{chunk-vn3yh259.js → chunk-4gmng3vf.js} +1 -1
  14. package/dist/{chunk-hxgj4kzd.js → chunk-4jph1t3v.js} +7 -7
  15. package/dist/{chunk-znw0zvy4.js → chunk-4wn7bxxa.js} +25 -7
  16. package/dist/chunk-4wn7bxxa.js.map +12 -0
  17. package/dist/{chunk-3qpcrcj8.js → chunk-6aawea28.js} +2 -4
  18. package/dist/chunk-6aawea28.js.map +10 -0
  19. package/dist/{chunk-rzkr70gr.js → chunk-6fsr7y93.js} +5 -1
  20. package/dist/{chunk-rzkr70gr.js.map → chunk-6fsr7y93.js.map} +3 -3
  21. package/dist/{chunk-y487ywrv.js → chunk-6q7c68yx.js} +4 -4
  22. package/dist/{chunk-ypj2wqtj.js → chunk-70jmg2zx.js} +2 -2
  23. package/dist/{chunk-wn7mznxc.js → chunk-7jc21943.js} +3 -3
  24. package/dist/{chunk-rzcjp5m2.js → chunk-8fjmqjyx.js} +1 -1
  25. package/dist/{chunk-m8zkvep1.js → chunk-8w27xf9e.js} +1 -1
  26. package/dist/{chunk-z9h698s8.js → chunk-a4g5yjm0.js} +1 -1
  27. package/dist/{chunk-4ypbe95r.js → chunk-adbve9zt.js} +1 -1
  28. package/dist/{chunk-hj0n3c2p.js → chunk-dqkd3kdj.js} +40 -22
  29. package/dist/{chunk-hj0n3c2p.js.map → chunk-dqkd3kdj.js.map} +3 -3
  30. package/dist/{chunk-wnxabtn4.js → chunk-ex8s8wsk.js} +6 -6
  31. package/dist/{chunk-4tnrzqqh.js → chunk-fj1dvp37.js} +1 -1
  32. package/dist/{chunk-3fpgxpsq.js → chunk-fjw417jf.js} +127 -69
  33. package/dist/{chunk-3fpgxpsq.js.map → chunk-fjw417jf.js.map} +4 -4
  34. package/dist/{chunk-ht4a7mjg.js → chunk-g4rbm4a6.js} +42 -8
  35. package/dist/{chunk-ht4a7mjg.js.map → chunk-g4rbm4a6.js.map} +3 -3
  36. package/dist/{chunk-qma80ych.js → chunk-hn2r7z41.js} +3 -2
  37. package/dist/chunk-hn2r7z41.js.map +11 -0
  38. package/dist/{chunk-bdhfn0ww.js → chunk-kkqwzy6h.js} +1 -1
  39. package/dist/{chunk-ww185kt6.js → chunk-mmac1mp6.js} +2 -2
  40. package/dist/{chunk-jfdggg1p.js → chunk-n5t4mq6z.js} +5 -5
  41. package/dist/{chunk-3a338wrp.js → chunk-p0nbkys4.js} +1 -1
  42. package/dist/{chunk-kxea54yg.js → chunk-py2dw8cw.js} +3 -3
  43. package/dist/{chunk-e2c5yxfe.js → chunk-q3grgzp1.js} +2 -2
  44. package/dist/{chunk-6rzb1g6h.js → chunk-qne45xqq.js} +1 -1
  45. package/dist/{chunk-cbn96bve.js → chunk-r5sc1d2b.js} +2 -2
  46. package/dist/{chunk-y6vnvayj.js → chunk-vjcd8d27.js} +1 -1
  47. package/dist/{chunk-ma1mhh7w.js → chunk-wd34wnsy.js} +6 -6
  48. package/dist/{chunk-pvgpckm1.js → chunk-wem5vqjw.js} +7 -7
  49. package/dist/{chunk-gct3m1da.js → chunk-xr7y35yj.js} +3 -3
  50. package/dist/{chunk-q2xghyft.js → chunk-y0zmae99.js} +5 -5
  51. package/dist/{chunk-xgptywp0.js → chunk-y2aq6ag9.js} +1 -1
  52. package/dist/{chunk-yvf2jqj8.js → chunk-yfppyaty.js} +1 -1
  53. package/dist/{chunk-00pfcqh4.js → chunk-z708gemf.js} +129 -70
  54. package/dist/{chunk-00pfcqh4.js.map → chunk-z708gemf.js.map} +5 -5
  55. package/dist/deploy/define.d.ts +4 -0
  56. package/dist/deploy/define.d.ts.map +1 -0
  57. package/dist/deploy/index.d.ts +3 -0
  58. package/dist/deploy/index.d.ts.map +1 -0
  59. package/dist/deploy/index.js +10 -0
  60. package/dist/deploy/index.js.map +10 -0
  61. package/dist/deploy/internal.d.ts +3 -0
  62. package/dist/deploy/internal.d.ts.map +1 -0
  63. package/dist/deploy/internal.js +244 -0
  64. package/dist/deploy/internal.js.map +11 -0
  65. package/dist/deploy/services.d.ts +8 -0
  66. package/dist/deploy/services.d.ts.map +1 -0
  67. package/dist/deploy/types.d.ts +309 -0
  68. package/dist/deploy/types.d.ts.map +1 -0
  69. package/dist/deploy/validation.d.ts +7 -0
  70. package/dist/deploy/validation.d.ts.map +1 -0
  71. package/dist/error-reporting/internal.js +3 -3
  72. package/dist/errors/catalog.d.ts +4 -0
  73. package/dist/errors/catalog.d.ts.map +1 -1
  74. package/dist/errors/internal.js +1 -1
  75. package/dist/events/index.js +3 -3
  76. package/dist/execution/agent.js +32 -32
  77. package/dist/execution/primitive.js +28 -28
  78. package/dist/execution/request.js +30 -30
  79. package/dist/execution/request.js.map +1 -1
  80. package/dist/index.js +48 -48
  81. package/dist/index.js.map +1 -1
  82. package/dist/maintenance/ontology-maintenance.d.ts +2 -0
  83. package/dist/maintenance/ontology-maintenance.d.ts.map +1 -1
  84. package/dist/maintenance/types.d.ts +8 -2
  85. package/dist/maintenance/types.d.ts.map +1 -1
  86. package/dist/materialization/dataset-watermark.d.ts +6 -1
  87. package/dist/materialization/dataset-watermark.d.ts.map +1 -1
  88. package/dist/materialization/index.js +1 -1
  89. package/dist/materialization/model.d.ts +6 -0
  90. package/dist/materialization/model.d.ts.map +1 -1
  91. package/dist/materializer/index.js +17 -17
  92. package/dist/materializer/index.js.map +1 -1
  93. package/dist/materializer/projections/finish-run.d.ts.map +1 -1
  94. package/dist/materializer/projections/replace.d.ts.map +1 -1
  95. package/dist/materializer/projections/source-materialization.d.ts +6 -2
  96. package/dist/materializer/projections/source-materialization.d.ts.map +1 -1
  97. package/dist/models/execution/index.js +13 -13
  98. package/dist/objects/index.js +20 -20
  99. package/dist/objects/query/index.js +14 -14
  100. package/dist/pipelines/index.js +18 -18
  101. package/dist/projections/internal.js +15 -15
  102. package/dist/projections/types.d.ts +1 -1
  103. package/dist/projections/types.d.ts.map +1 -1
  104. package/dist/queues/in-memory.d.ts +1 -1
  105. package/dist/queues/in-memory.d.ts.map +1 -1
  106. package/dist/runtime/internal.js +18 -18
  107. package/dist/shares/internal.js +13 -13
  108. package/dist/shares/internal.js.map +1 -1
  109. package/dist/storage/action-runs/failure.js +2 -2
  110. package/dist/storage/ai-limits/enforcement.js +2 -2
  111. package/dist/storage/connector-connections/provider.js +1 -1
  112. package/dist/storage/file-upload-sessions/provider.js +4 -4
  113. package/dist/storage/index.d.ts +2 -2
  114. package/dist/storage/index.d.ts.map +1 -1
  115. package/dist/storage/index.js +28 -28
  116. package/dist/storage/index.js.map +1 -1
  117. package/dist/storage/internal.js +1 -1
  118. package/dist/storage/ontology/in-memory/sources.d.ts +6 -3
  119. package/dist/storage/ontology/in-memory/sources.d.ts.map +1 -1
  120. package/dist/storage/ontology/index.d.ts +1 -1
  121. package/dist/storage/ontology/index.d.ts.map +1 -1
  122. package/dist/storage/ontology/provider.d.ts +16 -3
  123. package/dist/storage/ontology/provider.d.ts.map +1 -1
  124. package/dist/storage/ontology/provider.js +8 -4
  125. package/dist/storage/ontology/provider.js.map +1 -1
  126. package/dist/storage/ontology/sources.d.ts +36 -8
  127. package/dist/storage/ontology/sources.d.ts.map +1 -1
  128. package/dist/storage/projection-runs/in-memory.d.ts +3 -1
  129. package/dist/storage/projection-runs/in-memory.d.ts.map +1 -1
  130. package/dist/storage/projection-runs/index.d.ts +1 -1
  131. package/dist/storage/projection-runs/index.d.ts.map +1 -1
  132. package/dist/storage/projection-runs/lifecycle.d.ts +12 -0
  133. package/dist/storage/projection-runs/lifecycle.d.ts.map +1 -1
  134. package/dist/storage/projection-runs/provider.js +10 -4
  135. package/dist/storage/projection-runs/provider.js.map +1 -1
  136. package/dist/storage/projection-runs/types.d.ts +32 -1
  137. package/dist/storage/projection-runs/types.d.ts.map +1 -1
  138. package/dist/storage/workflow-runs/provider.js +4 -4
  139. package/dist/syncs/index.js +19 -19
  140. package/dist/testing/index.js +296 -54
  141. package/dist/testing/index.js.map +4 -4
  142. package/dist/testing/ontology-storage-contract.d.ts.map +1 -1
  143. package/dist/testing/projection-run-storage-contract.d.ts.map +1 -1
  144. package/dist/webhooks/internal.js +14 -14
  145. package/dist/webhooks/internal.js.map +1 -1
  146. package/dist/workers/index.js +1 -1
  147. package/dist/workflows/index.js +33 -33
  148. package/package.json +13 -1
  149. package/src/deploy/define.ts +6 -0
  150. package/src/deploy/index.ts +41 -0
  151. package/src/deploy/internal.ts +7 -0
  152. package/src/deploy/services.ts +51 -0
  153. package/src/deploy/types.ts +338 -0
  154. package/src/deploy/validation.ts +261 -0
  155. package/src/errors/catalog.ts +4 -0
  156. package/src/maintenance/ontology-maintenance.ts +32 -2
  157. package/src/maintenance/types.ts +8 -2
  158. package/src/materialization/dataset-watermark.ts +6 -4
  159. package/src/materialization/model.ts +7 -0
  160. package/src/materializer/README.md +26 -6
  161. package/src/materializer/projections/finish-run.ts +129 -26
  162. package/src/materializer/projections/replace.ts +67 -49
  163. package/src/materializer/projections/source-materialization.ts +11 -6
  164. package/src/projections/types.ts +1 -0
  165. package/src/storage/index.ts +6 -1
  166. package/src/storage/ontology/in-memory/sources.ts +119 -55
  167. package/src/storage/ontology/index.ts +4 -1
  168. package/src/storage/ontology/provider.ts +64 -7
  169. package/src/storage/ontology/sources.ts +42 -8
  170. package/src/storage/projection-runs/in-memory.ts +41 -2
  171. package/src/storage/projection-runs/index.ts +2 -0
  172. package/src/storage/projection-runs/lifecycle.ts +61 -4
  173. package/src/storage/projection-runs/types.ts +41 -1
  174. package/src/testing/ontology-storage-contract.ts +141 -15
  175. package/src/testing/projection-run-storage-contract.ts +174 -1
  176. package/dist/chunk-3qpcrcj8.js.map +0 -10
  177. package/dist/chunk-qma80ych.js.map +0 -11
  178. package/dist/chunk-znw0zvy4.js.map +0 -12
  179. /package/dist/{chunk-dv4ct8mp.js.map → chunk-1jvq82rp.js.map} +0 -0
  180. /package/dist/{chunk-728fp090.js.map → chunk-3c90jvfj.js.map} +0 -0
  181. /package/dist/{chunk-vn3yh259.js.map → chunk-4gmng3vf.js.map} +0 -0
  182. /package/dist/{chunk-hxgj4kzd.js.map → chunk-4jph1t3v.js.map} +0 -0
  183. /package/dist/{chunk-y487ywrv.js.map → chunk-6q7c68yx.js.map} +0 -0
  184. /package/dist/{chunk-ypj2wqtj.js.map → chunk-70jmg2zx.js.map} +0 -0
  185. /package/dist/{chunk-wn7mznxc.js.map → chunk-7jc21943.js.map} +0 -0
  186. /package/dist/{chunk-rzcjp5m2.js.map → chunk-8fjmqjyx.js.map} +0 -0
  187. /package/dist/{chunk-m8zkvep1.js.map → chunk-8w27xf9e.js.map} +0 -0
  188. /package/dist/{chunk-z9h698s8.js.map → chunk-a4g5yjm0.js.map} +0 -0
  189. /package/dist/{chunk-4ypbe95r.js.map → chunk-adbve9zt.js.map} +0 -0
  190. /package/dist/{chunk-wnxabtn4.js.map → chunk-ex8s8wsk.js.map} +0 -0
  191. /package/dist/{chunk-4tnrzqqh.js.map → chunk-fj1dvp37.js.map} +0 -0
  192. /package/dist/{chunk-bdhfn0ww.js.map → chunk-kkqwzy6h.js.map} +0 -0
  193. /package/dist/{chunk-ww185kt6.js.map → chunk-mmac1mp6.js.map} +0 -0
  194. /package/dist/{chunk-jfdggg1p.js.map → chunk-n5t4mq6z.js.map} +0 -0
  195. /package/dist/{chunk-3a338wrp.js.map → chunk-p0nbkys4.js.map} +0 -0
  196. /package/dist/{chunk-kxea54yg.js.map → chunk-py2dw8cw.js.map} +0 -0
  197. /package/dist/{chunk-e2c5yxfe.js.map → chunk-q3grgzp1.js.map} +0 -0
  198. /package/dist/{chunk-6rzb1g6h.js.map → chunk-qne45xqq.js.map} +0 -0
  199. /package/dist/{chunk-cbn96bve.js.map → chunk-r5sc1d2b.js.map} +0 -0
  200. /package/dist/{chunk-y6vnvayj.js.map → chunk-vjcd8d27.js.map} +0 -0
  201. /package/dist/{chunk-ma1mhh7w.js.map → chunk-wd34wnsy.js.map} +0 -0
  202. /package/dist/{chunk-pvgpckm1.js.map → chunk-wem5vqjw.js.map} +0 -0
  203. /package/dist/{chunk-gct3m1da.js.map → chunk-xr7y35yj.js.map} +0 -0
  204. /package/dist/{chunk-q2xghyft.js.map → chunk-y0zmae99.js.map} +0 -0
  205. /package/dist/{chunk-xgptywp0.js.map → chunk-y2aq6ag9.js.map} +0 -0
  206. /package/dist/{chunk-yvf2jqj8.js.map → chunk-yfppyaty.js.map} +0 -0
@@ -16,8 +16,11 @@ import {
16
16
  sourceStageRoots,
17
17
  } from "../provider"
18
18
  import type {
19
+ AbandonRunSourceMaterializationInput,
19
20
  AbandonSourceMaterializationCandidateInput,
20
21
  AbandonSourceMaterializationInput,
22
+ AdoptedSourceMaterialization,
23
+ AdoptSourceMaterializationInput,
21
24
  AssertSourceMaterializationExecution,
22
25
  BeginSourceMaterializationInput,
23
26
  CleanupTerminalSourceMaterializationsInput,
@@ -26,7 +29,7 @@ import type {
26
29
  MarkSourceMaterializationReadyInput,
27
30
  OntologySourceRecord,
28
31
  OntologySourceStorage,
29
- ReclaimSourceMaterializationInput,
32
+ PurgeAbandonedSourceMaterializationsInput,
30
33
  StageSourceAssertion,
31
34
  StageSourceRowsInput,
32
35
  StageSourceRowsResult,
@@ -38,6 +41,7 @@ import {
38
41
  assertNonblank,
39
42
  type InMemoryOntologyState,
40
43
  type InMemorySourceMaterialization,
44
+ type InMemorySourceRoot,
41
45
  insertBounded,
42
46
  sourceMaterializationKey,
43
47
  sourceMaterializationRecord,
@@ -78,7 +82,7 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
78
82
  )
79
83
  if (nonterminal.length > 0) {
80
84
  throw sourceConflict(
81
- `Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`
85
+ `Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; adopt it before beginning another.`
82
86
  )
83
87
  }
84
88
 
@@ -191,8 +195,42 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
191
195
  })
192
196
  }
193
197
 
198
+ async adopt(
199
+ input: AdoptSourceMaterializationInput
200
+ ): Promise<AdoptedSourceMaterialization | null> {
201
+ return this.runRootOperation(async () => {
202
+ assertProjectAndSource(input)
203
+ assertSourceExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken)
204
+ assertCanonicalTimestamp(input.adoptedAt, "Source adoptedAt")
205
+ await this.assertCurrentExecution(input)
206
+ const [candidate, ...others] = this.findRunCandidates(
207
+ input.projectId,
208
+ input.source.projectionId,
209
+ input.execution.projectionRunId
210
+ )
211
+ if (!candidate) return null
212
+ if (others.length > 0) throw multipleRunCandidates(input.execution.projectionRunId)
213
+ const [key, materialization] = candidate
214
+ const adopted: InMemorySourceMaterialization = {
215
+ ...materialization,
216
+ executionToken: input.execution.executionToken,
217
+ // Workers' clocks differ; adoption never moves the record back in time.
218
+ updatedAt:
219
+ input.adoptedAt > materialization.updatedAt ? input.adoptedAt : materialization.updatedAt,
220
+ }
221
+ this.state.sourceMaterializations.set(key, adopted)
222
+ return {
223
+ record: sourceMaterializationRecord(adopted),
224
+ resumeStagingOrdinal:
225
+ adopted.status === "ready"
226
+ ? (adopted.rootCount ?? 0)
227
+ : Math.max(0, ...adopted.ordinalRoots.keys()),
228
+ }
229
+ })
230
+ }
231
+
194
232
  async abandon(input: AbandonSourceMaterializationCandidateInput): Promise<OntologySourceRecord>
195
- async abandon(input: ReclaimSourceMaterializationInput): Promise<OntologySourceRecord | null>
233
+ async abandon(input: AbandonRunSourceMaterializationInput): Promise<OntologySourceRecord | null>
196
234
  async abandon(input: AbandonSourceMaterializationInput): Promise<OntologySourceRecord | null> {
197
235
  return this.runRootOperation(async () => {
198
236
  assertProjectAndSource(input)
@@ -201,7 +239,7 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
201
239
  await this.assertCurrentExecution(input)
202
240
  return input.kind === "candidate"
203
241
  ? this.abandonCandidate(input)
204
- : this.abandonForReclaim(input)
242
+ : this.abandonRunCandidate(input)
205
243
  })
206
244
  }
207
245
 
@@ -211,18 +249,14 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
211
249
  return this.runRootOperation(() => {
212
250
  assertNonblank(input.projectId, "Terminal source cleanup project id")
213
251
  assertCanonicalTimestamp(input.terminalBefore, "Terminal source cleanup cutoff")
214
- if (!Number.isSafeInteger(input.limit) || input.limit <= 0) {
215
- throw new MaterializationValidationError(
216
- "Terminal source cleanup limit must be a positive safe integer."
217
- )
218
- }
252
+ assertCleanupLimit(input.limit)
219
253
 
220
254
  const candidates: [string, InMemorySourceMaterialization][] = []
221
255
  for (const entry of this.state.sourceMaterializations.entries()) {
222
256
  const materialization = entry[1]
223
257
  if (
224
258
  materialization.projectId !== input.projectId ||
225
- (materialization.status !== "superseded" && materialization.status !== "abandoned") ||
259
+ materialization.status !== "superseded" ||
226
260
  materialization.terminalAt === null ||
227
261
  materialization.terminalAt >= input.terminalBefore
228
262
  ) {
@@ -239,42 +273,69 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
239
273
  insertBounded(candidates, entry, input.limit, compareTerminalMaterializations)
240
274
  }
241
275
 
242
- let remaining = input.limit
243
- let rowsDeleted = 0
244
- let materializationsDeleted = 0
245
- for (const [key, candidate] of candidates) {
276
+ return this.deleteRetiredRoots(
277
+ candidates,
278
+ input.limit,
279
+ (root) => !root.active && root.retiredAt !== null && root.retiredAt < input.terminalBefore
280
+ )
281
+ })
282
+ }
283
+
284
+ async purgeAbandoned(
285
+ input: PurgeAbandonedSourceMaterializationsInput
286
+ ): Promise<CleanupTerminalSourceMaterializationsResult> {
287
+ return this.runRootOperation(() => {
288
+ assertNonblank(input.projectId, "Abandoned source purge project id")
289
+ assertCleanupLimit(input.limit)
290
+ const abandoned = [...this.state.sourceMaterializations.entries()]
291
+ .filter(
292
+ ([, materialization]) =>
293
+ materialization.projectId === input.projectId && materialization.status === "abandoned"
294
+ )
295
+ .sort(compareTerminalMaterializations)
296
+ return this.deleteRetiredRoots(abandoned, input.limit, () => true)
297
+ })
298
+ }
299
+
300
+ private deleteRetiredRoots(
301
+ candidates: readonly [string, InMemorySourceMaterialization][],
302
+ limit: number,
303
+ retired: (root: InMemorySourceRoot) => boolean
304
+ ): CleanupTerminalSourceMaterializationsResult {
305
+ let remaining = limit
306
+ let rowsDeleted = 0
307
+ let materializationsDeleted = 0
308
+ for (const [key, candidate] of candidates) {
309
+ if (remaining === 0) break
310
+ const current = this.state.sourceMaterializations.get(key)
311
+ if (current !== candidate) continue
312
+ current.rootOrdinals.clear()
313
+ current.ordinalRoots.clear()
314
+ for (const [rootKey, root] of current.roots) {
246
315
  if (remaining === 0) break
247
- const current = this.state.sourceMaterializations.get(key)
248
- if (current !== candidate) continue
249
- current.rootOrdinals.clear()
250
- current.ordinalRoots.clear()
251
- for (const [rootKey, root] of current.roots) {
316
+ if (!retired(root)) continue
317
+ for (const entityKey of root.entityKeys) {
252
318
  if (remaining === 0) break
253
- if (root.active || root.retiredAt === null || root.retiredAt >= input.terminalBefore)
254
- continue
255
- for (const entityKey of root.entityKeys) {
256
- if (remaining === 0) break
257
- current.rowsByEntity.delete(entityKey)
258
- root.entityKeys.delete(entityKey)
259
- rowsDeleted += 1
260
- remaining -= 1
261
- }
262
- if (root.entityKeys.size === 0 && remaining > 0) {
263
- current.roots.delete(rootKey)
264
- current.rootOrdinals.delete(rootKey)
265
- current.ordinalRoots.delete(root.stagingOrdinal)
266
- rowsDeleted += 1
267
- remaining -= 1
268
- }
319
+ current.rowsByEntity.delete(entityKey)
320
+ root.entityKeys.delete(entityKey)
321
+ rowsDeleted += 1
322
+ remaining -= 1
269
323
  }
270
- if (current.roots.size === 0 && current.rowsByEntity.size === 0 && remaining > 0) {
271
- this.state.sourceMaterializations.delete(key)
272
- materializationsDeleted += 1
324
+ if (root.entityKeys.size === 0 && remaining > 0) {
325
+ current.roots.delete(rootKey)
326
+ current.rootOrdinals.delete(rootKey)
327
+ current.ordinalRoots.delete(root.stagingOrdinal)
328
+ rowsDeleted += 1
273
329
  remaining -= 1
274
330
  }
275
331
  }
276
- return { rowsDeleted, materializationsDeleted }
277
- })
332
+ if (current.roots.size === 0 && current.rowsByEntity.size === 0 && remaining > 0) {
333
+ this.state.sourceMaterializations.delete(key)
334
+ materializationsDeleted += 1
335
+ remaining -= 1
336
+ }
337
+ }
338
+ return { rowsDeleted, materializationsDeleted }
278
339
  }
279
340
 
280
341
  async summarizeTerminal(
@@ -356,26 +417,15 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
356
417
  return this.transitionToAbandoned(materialization, input.abandonedAt)
357
418
  }
358
419
 
359
- private abandonForReclaim(input: ReclaimSourceMaterializationInput): OntologySourceRecord | null {
420
+ private abandonRunCandidate(
421
+ input: AbandonRunSourceMaterializationInput
422
+ ): OntologySourceRecord | null {
360
423
  const candidates = this.findRunCandidates(
361
424
  input.projectId,
362
425
  input.source.projectionId,
363
426
  input.execution.projectionRunId
364
427
  )
365
- if (
366
- candidates.some(
367
- ([, materialization]) => materialization.executionToken === input.execution.executionToken
368
- )
369
- ) {
370
- throw sourceConflict(
371
- `Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`
372
- )
373
- }
374
- if (candidates.length > 1) {
375
- throw sourceConflict(
376
- `Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`
377
- )
378
- }
428
+ if (candidates.length > 1) throw multipleRunCandidates(input.execution.projectionRunId)
379
429
  const candidate = candidates[0]?.[1]
380
430
  return candidate ? this.transitionToAbandoned(candidate, input.abandonedAt) : null
381
431
  }
@@ -609,6 +659,20 @@ function assertNotBefore(
609
659
  }
610
660
  }
611
661
 
662
+ function assertCleanupLimit(limit: number): void {
663
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
664
+ throw new MaterializationValidationError(
665
+ "Terminal source cleanup limit must be a positive safe integer."
666
+ )
667
+ }
668
+ }
669
+
670
+ function multipleRunCandidates(projectionRunId: string) {
671
+ return sourceConflict(
672
+ `Projection run '${projectionRunId}' has multiple nonterminal source materializations.`
673
+ )
674
+ }
675
+
612
676
  function compareTerminalMaterializations(
613
677
  [, left]: readonly [string, InMemorySourceMaterialization],
614
678
  [, right]: readonly [string, InMemorySourceMaterialization]
@@ -97,8 +97,11 @@ export type {
97
97
  } from "./outbox"
98
98
  export { ONTOLOGY_OUTBOX_FAILURE_CODES } from "./outbox"
99
99
  export type {
100
+ AbandonRunSourceMaterializationInput,
100
101
  AbandonSourceMaterializationCandidateInput,
101
102
  AbandonSourceMaterializationInput,
103
+ AdoptedSourceMaterialization,
104
+ AdoptSourceMaterializationInput,
102
105
  AssertSourceMaterializationExecution,
103
106
  AssertSourceMaterializationExecutionInput,
104
107
  BeginSourceMaterializationInput,
@@ -109,7 +112,7 @@ export type {
109
112
  OntologySourceMaterializationStatus,
110
113
  OntologySourceRecord,
111
114
  OntologySourceStorage,
112
- ReclaimSourceMaterializationInput,
115
+ PurgeAbandonedSourceMaterializationsInput,
113
116
  StageSourceAssertion,
114
117
  StageSourceRoot,
115
118
  StageSourceRowsInput,
@@ -11,6 +11,8 @@ import type {
11
11
  ExpectedObjectRevision,
12
12
  OntologyLinkRef,
13
13
  OntologyObjectRef,
14
+ ProjectionEntityRef,
15
+ ProjectionSourceAssertion,
14
16
  } from "../../materialization/model"
15
17
  import { linkRefKey, objectRefKey, projectionEntityKey } from "../../materialization/refs"
16
18
  import type { OntologyCommitOriginSelector, OntologyCommitWrite } from "./commits"
@@ -716,7 +718,6 @@ export function sourceStageRow(row: StageSourceAssertion): SourceStageRow {
716
718
  export interface SourceStageRoot {
717
719
  readonly root: StageSourceAssertion["root"]
718
720
  readonly rootKey: string
719
- readonly sortKey: string
720
721
  readonly stagingOrdinal: number
721
722
  readonly deleted: boolean
722
723
  }
@@ -763,7 +764,6 @@ export function sourceStageRoots(
763
764
  roots.set(key, {
764
765
  root: row.root,
765
766
  rootKey: key,
766
- sortKey: utf8SortKey(key),
767
767
  stagingOrdinal: row.stagingOrdinal,
768
768
  deleted,
769
769
  })
@@ -965,12 +965,69 @@ export function sourceEntityColumns(entity: StageSourceAssertion["root"]): {
965
965
  }
966
966
  }
967
967
 
968
- export function utf8SortKey(canonicalKey: string): string {
969
- let key = ""
970
- for (const byte of new TextEncoder().encode(canonicalKey)) {
971
- key += byte.toString(16).padStart(2, "0")
968
+ /** The inverse of `projectionEntityKey`, for providers that store only the canonical key. */
969
+ export function sourceEntityFromKey(key: string): ProjectionEntityRef {
970
+ const parts: unknown = JSON.parse(key)
971
+ if (Array.isArray(parts) && parts.every((part) => typeof part === "string")) {
972
+ if (parts[0] === "object" && parts.length === 3) {
973
+ return { kind: "object", ref: { objectTypeId: parts[1]!, primaryId: parts[2]! } }
974
+ }
975
+ if (parts[0] === "link" && parts.length === 6) {
976
+ return {
977
+ kind: "link",
978
+ ref: {
979
+ source: { objectTypeId: parts[1]!, primaryId: parts[2]! },
980
+ linkId: parts[3]!,
981
+ target: { objectTypeId: parts[4]!, primaryId: parts[5]! },
982
+ },
983
+ }
984
+ }
972
985
  }
973
- return key
986
+ throw new MaterializationConflictError(
987
+ "source-materialization",
988
+ `Stored source key ${key} is not a canonical entity key.`
989
+ )
990
+ }
991
+
992
+ /** What an assertion carries beyond its identity, or null when that is nothing. */
993
+ export function sourceAssertionPayload(
994
+ assertion: ProjectionSourceAssertion
995
+ ): Readonly<Record<string, unknown>> | null {
996
+ const { kind: _kind, ref: _ref, ...payload } = assertion
997
+ return Object.keys(payload).length === 0 ? null : payload
998
+ }
999
+
1000
+ /** Rebuilds an assertion from its typed identity columns and its payload. */
1001
+ export function sourceAssertionFromColumns(
1002
+ columns: {
1003
+ readonly entity_kind: "object" | "link"
1004
+ readonly object_type_id: string | null
1005
+ readonly primary_id: string | null
1006
+ readonly source_type_id: string | null
1007
+ readonly source_primary_id: string | null
1008
+ readonly link_id: string | null
1009
+ readonly target_type_id: string | null
1010
+ readonly target_primary_id: string | null
1011
+ },
1012
+ payload: unknown
1013
+ ): ProjectionSourceAssertion {
1014
+ const rest = (payload ?? {}) as Record<string, unknown>
1015
+ if (columns.entity_kind === "object") {
1016
+ return {
1017
+ kind: "object",
1018
+ ref: { objectTypeId: columns.object_type_id!, primaryId: columns.primary_id! },
1019
+ ...rest,
1020
+ } as ProjectionSourceAssertion
1021
+ }
1022
+ return {
1023
+ kind: "link",
1024
+ ref: {
1025
+ source: { objectTypeId: columns.source_type_id!, primaryId: columns.source_primary_id! },
1026
+ linkId: columns.link_id!,
1027
+ target: { objectTypeId: columns.target_type_id!, primaryId: columns.target_primary_id! },
1028
+ },
1029
+ ...rest,
1030
+ } as ProjectionSourceAssertion
974
1031
  }
975
1032
 
976
1033
  export function isExactStagingManifest(
@@ -128,7 +128,7 @@ interface BaseAbandonSourceMaterializationInput {
128
128
  readonly abandonedAt: string
129
129
  }
130
130
 
131
- /** Abandon the current execution's exact candidate after ingress or semantic failure. */
131
+ /** Abandon the current execution's exact candidate, e.g. an adopted one whose delta base moved. */
132
132
  export interface AbandonSourceMaterializationCandidateInput
133
133
  extends BaseAbandonSourceMaterializationInput {
134
134
  readonly kind: "candidate"
@@ -136,20 +136,38 @@ export interface AbandonSourceMaterializationCandidateInput
136
136
  }
137
137
 
138
138
  /**
139
- * Reclaim a logical run after redelivery. The current execution may abandon the prior execution's
140
- * staging/ready candidate, but never one already owned by its own token.
139
+ * Release whatever candidate a run still holds, whichever execution staged it. Called in the same
140
+ * transaction as the run's terminal transition, so no candidate outlives its run.
141
141
  */
142
- export interface ReclaimSourceMaterializationInput extends BaseAbandonSourceMaterializationInput {
143
- readonly kind: "reclaim"
142
+ export interface AbandonRunSourceMaterializationInput
143
+ extends BaseAbandonSourceMaterializationInput {
144
+ readonly kind: "run"
144
145
  }
145
146
 
146
147
  export type AbandonSourceMaterializationInput =
147
148
  | AbandonSourceMaterializationCandidateInput
148
- | ReclaimSourceMaterializationInput
149
+ | AbandonRunSourceMaterializationInput
150
+
151
+ export interface AdoptSourceMaterializationInput {
152
+ readonly projectId: string
153
+ readonly source: ProjectionSourceRef
154
+ readonly execution: ProjectionExecution
155
+ readonly adoptedAt: string
156
+ }
157
+
158
+ export interface AdoptedSourceMaterialization {
159
+ readonly record: OntologySourceRecord
160
+ /**
161
+ * Where staging resumes. Every root before this ordinal is complete; the root at it may be
162
+ * partial and is staged again, which staging accepts for identical rows. Equals the root count
163
+ * once the candidate is ready.
164
+ */
165
+ readonly resumeStagingOrdinal: number
166
+ }
149
167
 
150
168
  export interface CleanupTerminalSourceMaterializationsInput {
151
169
  readonly projectId: string
152
- /** Exclusive cutoff for retired roots in terminal manifests, and for empty terminal manifests. */
170
+ /** Exclusive cutoff for retired roots in superseded manifests, and for empty ones. */
153
171
  readonly terminalBefore: string
154
172
  /** Maximum total assertion, root-reference, and manifest deletions performed by one call. */
155
173
  readonly limit: number
@@ -161,6 +179,12 @@ export interface CleanupTerminalSourceMaterializationsResult {
161
179
  readonly materializationsDeleted: number
162
180
  }
163
181
 
182
+ export interface PurgeAbandonedSourceMaterializationsInput {
183
+ readonly projectId: string
184
+ /** Maximum total assertion, root-reference, and manifest deletions performed by one call. */
185
+ readonly limit: number
186
+ }
187
+
164
188
  export interface SummarizeTerminalSourceMaterializationsInput {
165
189
  readonly projectId: string
166
190
  }
@@ -195,11 +219,21 @@ export interface OntologySourceStorage {
195
219
  stageRows(input: StageSourceRowsInput): Promise<StageSourceRowsResult>
196
220
  markReady(input: MarkSourceMaterializationReadyInput): Promise<OntologySourceRecord>
197
221
  getActive(input: GetActiveOntologySourceInput): Promise<OntologySourceRecord | null>
222
+ /** Hands a redelivered run's candidate to its current execution; null when it has none. */
223
+ adopt(input: AdoptSourceMaterializationInput): Promise<AdoptedSourceMaterialization | null>
198
224
  abandon(input: AbandonSourceMaterializationCandidateInput): Promise<OntologySourceRecord>
199
- abandon(input: ReclaimSourceMaterializationInput): Promise<OntologySourceRecord | null>
225
+ abandon(input: AbandonRunSourceMaterializationInput): Promise<OntologySourceRecord | null>
226
+ /** Retired roots of superseded manifests, after their retention. */
200
227
  cleanupTerminal(
201
228
  input: CleanupTerminalSourceMaterializationsInput
202
229
  ): Promise<CleanupTerminalSourceMaterializationsResult>
230
+ /**
231
+ * Abandoned candidates, whole and oldest first. Nothing reads an abandoned candidate, so it has
232
+ * no retention; deleting one manifest at a time keeps each delete on the source-version index.
233
+ */
234
+ purgeAbandoned(
235
+ input: PurgeAbandonedSourceMaterializationsInput
236
+ ): Promise<CleanupTerminalSourceMaterializationsResult>
203
237
  summarizeTerminal(
204
238
  input: SummarizeTerminalSourceMaterializationsInput
205
239
  ): Promise<TerminalSourceMaterializationSummary>
@@ -26,8 +26,10 @@ import {
26
26
  finishProjectionRunRecord,
27
27
  immutableDatasetVersionConflict,
28
28
  mergeProjectionRunProgress,
29
+ parseProjectionAttemptFailure,
29
30
  planProjectionRunReclaim,
30
31
  projectionRunNotFound,
32
+ projectionRunSupersedes,
31
33
  publicProjectionRunRecord,
32
34
  requireTelemetryProjectionRun,
33
35
  type StoredProjectionRunRecord,
@@ -36,6 +38,7 @@ import {
36
38
  import {
37
39
  type AdvanceProjectionTelemetryCheckpointInput,
38
40
  type FailProjectionRunEnqueueInput,
41
+ type FindSupersedingProjectionRunInput,
39
42
  type FinishProjectionRunInput,
40
43
  type ListLatestProjectionRunsInput,
41
44
  type ListLatestProjectionRunsResult,
@@ -47,6 +50,7 @@ import {
47
50
  type ProjectionRunStorage,
48
51
  projectionRunObjectTypesVisible,
49
52
  type QueueProjectionRunInput,
53
+ type RecordProjectionAttemptFailureInput,
50
54
  type RecordProjectionMissingTargetInput,
51
55
  type StartOrReclaimProjectionRunInput,
52
56
  type TelemetryProjectionRunRecord,
@@ -60,6 +64,10 @@ function projectionRunKey(projectId: string, id: string): string {
60
64
  return JSON.stringify([projectId, id])
61
65
  }
62
66
 
67
+ function compareText(left: string, right: string): number {
68
+ return left < right ? -1 : left > right ? 1 : 0
69
+ }
70
+
63
71
  function compareRuns(a: ProjectionRunRecord, b: ProjectionRunRecord, order: "asc" | "desc") {
64
72
  return compareStartedAt(a, b, order)
65
73
  }
@@ -152,7 +160,6 @@ export class InMemoryProjectionRunStorage implements ProjectionRunStorage {
152
160
  executionToken,
153
161
  startedAt: existing.startedAt ?? new Date(input.startedAt ?? new Date()),
154
162
  finishedAt: undefined,
155
- error: undefined,
156
163
  }
157
164
 
158
165
  this.rows.set(key, structuredClone(record))
@@ -246,6 +253,17 @@ export class InMemoryProjectionRunStorage implements ProjectionRunStorage {
246
253
  })
247
254
  }
248
255
 
256
+ async recordAttemptFailure(
257
+ input: RecordProjectionAttemptFailureInput
258
+ ): Promise<ProjectionRunRecord> {
259
+ return this.runRootOperation(() => {
260
+ const existing = this.requireExecution(input)
261
+ const next = { ...existing, error: parseProjectionAttemptFailure(existing, input.error) }
262
+ this.rows.set(projectionRunKey(input.projectId, input.id), structuredClone(next))
263
+ return publicProjectionRunRecord(next)
264
+ })
265
+ }
266
+
249
267
  async finish(input: FinishProjectionRunInput): Promise<ProjectionRunRecord> {
250
268
  return this.runRootOperation(() => {
251
269
  const existing = this.requireExecution(input)
@@ -311,12 +329,33 @@ export class InMemoryProjectionRunStorage implements ProjectionRunStorage {
311
329
  })
312
330
  }
313
331
 
332
+ async findSupersedingRun(
333
+ input: FindSupersedingProjectionRunInput
334
+ ): Promise<ProjectionRunRecord | null> {
335
+ return this.runRootOperation(() => {
336
+ const run = this.rows.get(projectionRunKey(input.projectId, input.id))
337
+ if (!run) throw projectionRunNotFound(input.projectId, input.id)
338
+ const newest = [...this.rows.values()]
339
+ .filter((candidate) => projectionRunSupersedes(candidate, run))
340
+ .sort(
341
+ (left, right) =>
342
+ compareText(
343
+ right.identity.datasetVersion.createdAt,
344
+ left.identity.datasetVersion.createdAt
345
+ ) || compareText(right.id, left.id)
346
+ )[0]
347
+ return newest ? publicProjectionRunRecord(newest) : null
348
+ })
349
+ }
350
+
314
351
  async listLatestByProjectionIds(
315
352
  input: ListLatestProjectionRunsInput
316
353
  ): Promise<ListLatestProjectionRunsResult> {
317
354
  return this.runRootOperation(() => {
318
355
  const runs = latestStartedAtByOwnerId(
319
- [...this.rows.values()].filter((record) => record.projectId === input.projectId),
356
+ [...this.rows.values()].filter(
357
+ (record) => record.projectId === input.projectId && record.status !== "superseded"
358
+ ),
320
359
  input.projectionIds,
321
360
  (record) => record.identity.projectionId
322
361
  )
@@ -3,6 +3,7 @@ export { InMemoryProjectionRunStorage } from "./in-memory"
3
3
  export type {
4
4
  AdvanceProjectionTelemetryCheckpointInput,
5
5
  FailProjectionRunEnqueueInput,
6
+ FindSupersedingProjectionRunInput,
6
7
  FinishProjectionRunInput,
7
8
  LinkProjectionRunRecord,
8
9
  LinkProjectionTarget,
@@ -27,6 +28,7 @@ export type {
27
28
  ProjectionTarget,
28
29
  ProjectionTelemetryCheckpoint,
29
30
  QueueProjectionRunInput,
31
+ RecordProjectionAttemptFailureInput,
30
32
  RecordProjectionMissingTargetInput,
31
33
  StartOrReclaimProjectionRunInput,
32
34
  TelemetryProjectionRunRecord,