@smthrs/engine-store 1.0.0-rc.5 → 1.0.0-rc.6

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 (257) hide show
  1. package/README.md +5 -5
  2. package/dist/cjs/ArtifactGc.d.ts +1 -1
  3. package/dist/cjs/ArtifactGc.d.ts.map +1 -1
  4. package/dist/cjs/ArtifactGc.js +1 -1
  5. package/dist/cjs/ArtifactGc.js.map +1 -1
  6. package/dist/cjs/DeferredClockFold.d.ts +4 -4
  7. package/dist/cjs/DeferredClockFold.d.ts.map +1 -1
  8. package/dist/cjs/DeferredClockFold.js +1 -1
  9. package/dist/cjs/DeferredClockFold.js.map +1 -1
  10. package/dist/cjs/DisasterRecovery.d.ts +1 -1
  11. package/dist/cjs/DisasterRecovery.d.ts.map +1 -1
  12. package/dist/cjs/DisasterRecovery.js +1 -1
  13. package/dist/cjs/DisasterRecovery.js.map +1 -1
  14. package/dist/cjs/DurableEngineState.d.ts +1 -1
  15. package/dist/cjs/DurableEngineState.d.ts.map +1 -1
  16. package/dist/cjs/DurableEngineState.js +1 -1
  17. package/dist/cjs/DurableEngineState.js.map +1 -1
  18. package/dist/cjs/ExecutionSnapshot.d.ts +2 -2
  19. package/dist/cjs/ExecutionSnapshot.d.ts.map +1 -1
  20. package/dist/cjs/ExecutionSnapshot.js +1 -1
  21. package/dist/cjs/ExecutionSnapshot.js.map +1 -1
  22. package/dist/cjs/Migrations.d.ts +2 -2
  23. package/dist/cjs/Migrations.d.ts.map +1 -1
  24. package/dist/cjs/OwnerIdentity.js +1 -1
  25. package/dist/cjs/OwnerIdentity.js.map +2 -2
  26. package/dist/cjs/PlanInputStore.d.ts +1 -1
  27. package/dist/cjs/PlanInputStore.d.ts.map +1 -1
  28. package/dist/cjs/PlanInputStore.js +1 -1
  29. package/dist/cjs/PlanInputStore.js.map +1 -1
  30. package/dist/cjs/PlanMergeStore.d.ts +1 -1
  31. package/dist/cjs/PlanMergeStore.d.ts.map +1 -1
  32. package/dist/cjs/PlanMergeStore.js +1 -1
  33. package/dist/cjs/PlanMergeStore.js.map +1 -1
  34. package/dist/cjs/Retention.d.ts +2 -2
  35. package/dist/cjs/Retention.d.ts.map +1 -1
  36. package/dist/cjs/Retention.js +1 -1
  37. package/dist/cjs/Retention.js.map +1 -1
  38. package/dist/cjs/RunChangeFeed.d.ts +1 -1
  39. package/dist/cjs/RunChangeFeed.d.ts.map +1 -1
  40. package/dist/cjs/RunChangeFeed.js +1 -1
  41. package/dist/cjs/RunChangeFeed.js.map +1 -1
  42. package/dist/cjs/SelectionStore.d.ts +1 -1
  43. package/dist/cjs/SelectionStore.d.ts.map +1 -1
  44. package/dist/cjs/SelectionStore.js +1 -1
  45. package/dist/cjs/SelectionStore.js.map +1 -1
  46. package/dist/cjs/StepBoundary.js +3 -3
  47. package/dist/cjs/StepBoundary.js.map +2 -2
  48. package/dist/cjs/StepCacheFold.d.ts +2 -2
  49. package/dist/cjs/StepCacheFold.d.ts.map +1 -1
  50. package/dist/cjs/StepCacheFold.js +1 -1
  51. package/dist/cjs/StepCacheFold.js.map +1 -1
  52. package/dist/cjs/internal/EngineStateSchema.d.ts +1 -1
  53. package/dist/cjs/internal/EngineStateSchema.d.ts.map +1 -1
  54. package/dist/cjs/internal/EngineStateSchema.js.map +1 -1
  55. package/dist/cjs/internal/ExecutionSnapshotRead.d.ts +3 -3
  56. package/dist/cjs/internal/ExecutionSnapshotRead.d.ts.map +1 -1
  57. package/dist/cjs/internal/ExecutionSnapshotRead.js.map +1 -1
  58. package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts +1 -1
  59. package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts.map +1 -1
  60. package/dist/cjs/internal/ExecutionSnapshotRelations.js.map +1 -1
  61. package/dist/cjs/internal/RetentionOps.d.ts +2 -2
  62. package/dist/cjs/internal/RetentionOps.d.ts.map +1 -1
  63. package/dist/cjs/internal/RetentionOps.js +1 -1
  64. package/dist/cjs/internal/RetentionOps.js.map +1 -1
  65. package/dist/cjs/internal/RunCatalogOps.d.ts +1 -1
  66. package/dist/cjs/internal/RunCatalogOps.d.ts.map +1 -1
  67. package/dist/cjs/internal/RunCatalogOps.js +1 -1
  68. package/dist/cjs/internal/RunCatalogOps.js.map +1 -1
  69. package/dist/cjs/internal/RunListing.d.ts +1 -1
  70. package/dist/cjs/internal/RunListing.d.ts.map +1 -1
  71. package/dist/cjs/internal/RunListing.js +1 -1
  72. package/dist/cjs/internal/RunListing.js.map +1 -1
  73. package/dist/cjs/migrations/0001_initial.d.ts +1 -1
  74. package/dist/cjs/migrations/0001_initial.d.ts.map +1 -1
  75. package/dist/cjs/migrations/0001_initial.js +1 -1
  76. package/dist/cjs/migrations/0001_initial.js.map +1 -1
  77. package/dist/cjs/migrations/0002_selection_store.d.ts +1 -1
  78. package/dist/cjs/migrations/0002_selection_store.d.ts.map +1 -1
  79. package/dist/cjs/migrations/0002_selection_store.js +1 -1
  80. package/dist/cjs/migrations/0002_selection_store.js.map +1 -1
  81. package/dist/cjs/migrations/0003_plan_inputs.d.ts +1 -1
  82. package/dist/cjs/migrations/0003_plan_inputs.d.ts.map +1 -1
  83. package/dist/cjs/migrations/0003_plan_inputs.js +1 -1
  84. package/dist/cjs/migrations/0003_plan_inputs.js.map +1 -1
  85. package/dist/cjs/migrations/0004_plan_environment.d.ts +1 -1
  86. package/dist/cjs/migrations/0004_plan_environment.d.ts.map +1 -1
  87. package/dist/cjs/migrations/0004_plan_environment.js +1 -1
  88. package/dist/cjs/migrations/0004_plan_environment.js.map +1 -1
  89. package/dist/cjs/migrations/0005_plan_merges.d.ts +1 -1
  90. package/dist/cjs/migrations/0005_plan_merges.d.ts.map +1 -1
  91. package/dist/cjs/migrations/0005_plan_merges.js +1 -1
  92. package/dist/cjs/migrations/0005_plan_merges.js.map +1 -1
  93. package/dist/cjs/migrations/0006_execution_listing.d.ts +1 -1
  94. package/dist/cjs/migrations/0006_execution_listing.d.ts.map +1 -1
  95. package/dist/cjs/migrations/0006_execution_listing.js +1 -1
  96. package/dist/cjs/migrations/0006_execution_listing.js.map +1 -1
  97. package/dist/cjs/migrations/0007_run_parent_sequence.d.ts +1 -1
  98. package/dist/cjs/migrations/0007_run_parent_sequence.d.ts.map +1 -1
  99. package/dist/cjs/migrations/0007_run_parent_sequence.js +1 -1
  100. package/dist/cjs/migrations/0007_run_parent_sequence.js.map +1 -1
  101. package/dist/cjs/migrations/0008_deferred_consumption.d.ts +1 -1
  102. package/dist/cjs/migrations/0008_deferred_consumption.d.ts.map +1 -1
  103. package/dist/cjs/migrations/0008_deferred_consumption.js +1 -1
  104. package/dist/cjs/migrations/0008_deferred_consumption.js.map +1 -1
  105. package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts +1 -1
  106. package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts.map +1 -1
  107. package/dist/cjs/migrations/0009_deferred_clock_facts.js +1 -1
  108. package/dist/cjs/migrations/0009_deferred_clock_facts.js.map +1 -1
  109. package/dist/cjs/migrations/0010_pending_cancellation.d.ts +1 -1
  110. package/dist/cjs/migrations/0010_pending_cancellation.d.ts.map +1 -1
  111. package/dist/cjs/migrations/0010_pending_cancellation.js +1 -1
  112. package/dist/cjs/migrations/0010_pending_cancellation.js.map +1 -1
  113. package/dist/cjs/test/TestStores.d.ts +4 -4
  114. package/dist/cjs/test/TestStores.d.ts.map +1 -1
  115. package/dist/esm/ArtifactGc.d.ts +1 -1
  116. package/dist/esm/ArtifactGc.d.ts.map +1 -1
  117. package/dist/esm/ArtifactGc.js +1 -1
  118. package/dist/esm/ArtifactGc.js.map +1 -1
  119. package/dist/esm/DeferredClockFold.d.ts +4 -4
  120. package/dist/esm/DeferredClockFold.d.ts.map +1 -1
  121. package/dist/esm/DeferredClockFold.js +1 -1
  122. package/dist/esm/DeferredClockFold.js.map +1 -1
  123. package/dist/esm/DisasterRecovery.d.ts +1 -1
  124. package/dist/esm/DisasterRecovery.d.ts.map +1 -1
  125. package/dist/esm/DisasterRecovery.js +1 -1
  126. package/dist/esm/DisasterRecovery.js.map +1 -1
  127. package/dist/esm/DurableEngineState.d.ts +1 -1
  128. package/dist/esm/DurableEngineState.d.ts.map +1 -1
  129. package/dist/esm/DurableEngineState.js +1 -1
  130. package/dist/esm/DurableEngineState.js.map +1 -1
  131. package/dist/esm/ExecutionSnapshot.d.ts +2 -2
  132. package/dist/esm/ExecutionSnapshot.d.ts.map +1 -1
  133. package/dist/esm/ExecutionSnapshot.js +1 -1
  134. package/dist/esm/ExecutionSnapshot.js.map +1 -1
  135. package/dist/esm/Migrations.d.ts +2 -2
  136. package/dist/esm/Migrations.d.ts.map +1 -1
  137. package/dist/esm/OwnerIdentity.js +1 -1
  138. package/dist/esm/OwnerIdentity.js.map +1 -1
  139. package/dist/esm/PlanInputStore.d.ts +1 -1
  140. package/dist/esm/PlanInputStore.d.ts.map +1 -1
  141. package/dist/esm/PlanInputStore.js +1 -1
  142. package/dist/esm/PlanInputStore.js.map +1 -1
  143. package/dist/esm/PlanMergeStore.d.ts +1 -1
  144. package/dist/esm/PlanMergeStore.d.ts.map +1 -1
  145. package/dist/esm/PlanMergeStore.js +1 -1
  146. package/dist/esm/PlanMergeStore.js.map +1 -1
  147. package/dist/esm/Retention.d.ts +2 -2
  148. package/dist/esm/Retention.d.ts.map +1 -1
  149. package/dist/esm/Retention.js +1 -1
  150. package/dist/esm/Retention.js.map +1 -1
  151. package/dist/esm/RunChangeFeed.d.ts +1 -1
  152. package/dist/esm/RunChangeFeed.d.ts.map +1 -1
  153. package/dist/esm/RunChangeFeed.js +1 -1
  154. package/dist/esm/RunChangeFeed.js.map +1 -1
  155. package/dist/esm/SelectionStore.d.ts +1 -1
  156. package/dist/esm/SelectionStore.d.ts.map +1 -1
  157. package/dist/esm/SelectionStore.js +1 -1
  158. package/dist/esm/SelectionStore.js.map +1 -1
  159. package/dist/esm/StepBoundary.js +3 -3
  160. package/dist/esm/StepBoundary.js.map +1 -1
  161. package/dist/esm/StepCacheFold.d.ts +2 -2
  162. package/dist/esm/StepCacheFold.d.ts.map +1 -1
  163. package/dist/esm/StepCacheFold.js +1 -1
  164. package/dist/esm/StepCacheFold.js.map +1 -1
  165. package/dist/esm/internal/EngineStateSchema.d.ts +1 -1
  166. package/dist/esm/internal/EngineStateSchema.d.ts.map +1 -1
  167. package/dist/esm/internal/EngineStateSchema.js.map +1 -1
  168. package/dist/esm/internal/ExecutionSnapshotRead.d.ts +3 -3
  169. package/dist/esm/internal/ExecutionSnapshotRead.d.ts.map +1 -1
  170. package/dist/esm/internal/ExecutionSnapshotRead.js.map +1 -1
  171. package/dist/esm/internal/ExecutionSnapshotRelations.d.ts +1 -1
  172. package/dist/esm/internal/ExecutionSnapshotRelations.d.ts.map +1 -1
  173. package/dist/esm/internal/ExecutionSnapshotRelations.js.map +1 -1
  174. package/dist/esm/internal/RetentionOps.d.ts +2 -2
  175. package/dist/esm/internal/RetentionOps.d.ts.map +1 -1
  176. package/dist/esm/internal/RetentionOps.js +1 -1
  177. package/dist/esm/internal/RetentionOps.js.map +1 -1
  178. package/dist/esm/internal/RunCatalogOps.d.ts +1 -1
  179. package/dist/esm/internal/RunCatalogOps.d.ts.map +1 -1
  180. package/dist/esm/internal/RunCatalogOps.js +1 -1
  181. package/dist/esm/internal/RunCatalogOps.js.map +1 -1
  182. package/dist/esm/internal/RunListing.d.ts +1 -1
  183. package/dist/esm/internal/RunListing.d.ts.map +1 -1
  184. package/dist/esm/internal/RunListing.js +1 -1
  185. package/dist/esm/internal/RunListing.js.map +1 -1
  186. package/dist/esm/migrations/0001_initial.d.ts +1 -1
  187. package/dist/esm/migrations/0001_initial.d.ts.map +1 -1
  188. package/dist/esm/migrations/0001_initial.js +1 -1
  189. package/dist/esm/migrations/0001_initial.js.map +1 -1
  190. package/dist/esm/migrations/0002_selection_store.d.ts +1 -1
  191. package/dist/esm/migrations/0002_selection_store.d.ts.map +1 -1
  192. package/dist/esm/migrations/0002_selection_store.js +1 -1
  193. package/dist/esm/migrations/0002_selection_store.js.map +1 -1
  194. package/dist/esm/migrations/0003_plan_inputs.d.ts +1 -1
  195. package/dist/esm/migrations/0003_plan_inputs.d.ts.map +1 -1
  196. package/dist/esm/migrations/0003_plan_inputs.js +1 -1
  197. package/dist/esm/migrations/0003_plan_inputs.js.map +1 -1
  198. package/dist/esm/migrations/0004_plan_environment.d.ts +1 -1
  199. package/dist/esm/migrations/0004_plan_environment.d.ts.map +1 -1
  200. package/dist/esm/migrations/0004_plan_environment.js +1 -1
  201. package/dist/esm/migrations/0004_plan_environment.js.map +1 -1
  202. package/dist/esm/migrations/0005_plan_merges.d.ts +1 -1
  203. package/dist/esm/migrations/0005_plan_merges.d.ts.map +1 -1
  204. package/dist/esm/migrations/0005_plan_merges.js +1 -1
  205. package/dist/esm/migrations/0005_plan_merges.js.map +1 -1
  206. package/dist/esm/migrations/0006_execution_listing.d.ts +1 -1
  207. package/dist/esm/migrations/0006_execution_listing.d.ts.map +1 -1
  208. package/dist/esm/migrations/0006_execution_listing.js +1 -1
  209. package/dist/esm/migrations/0006_execution_listing.js.map +1 -1
  210. package/dist/esm/migrations/0007_run_parent_sequence.d.ts +1 -1
  211. package/dist/esm/migrations/0007_run_parent_sequence.d.ts.map +1 -1
  212. package/dist/esm/migrations/0007_run_parent_sequence.js +1 -1
  213. package/dist/esm/migrations/0007_run_parent_sequence.js.map +1 -1
  214. package/dist/esm/migrations/0008_deferred_consumption.d.ts +1 -1
  215. package/dist/esm/migrations/0008_deferred_consumption.d.ts.map +1 -1
  216. package/dist/esm/migrations/0008_deferred_consumption.js +1 -1
  217. package/dist/esm/migrations/0008_deferred_consumption.js.map +1 -1
  218. package/dist/esm/migrations/0009_deferred_clock_facts.d.ts +1 -1
  219. package/dist/esm/migrations/0009_deferred_clock_facts.d.ts.map +1 -1
  220. package/dist/esm/migrations/0009_deferred_clock_facts.js +1 -1
  221. package/dist/esm/migrations/0009_deferred_clock_facts.js.map +1 -1
  222. package/dist/esm/migrations/0010_pending_cancellation.d.ts +1 -1
  223. package/dist/esm/migrations/0010_pending_cancellation.d.ts.map +1 -1
  224. package/dist/esm/migrations/0010_pending_cancellation.js +1 -1
  225. package/dist/esm/migrations/0010_pending_cancellation.js.map +1 -1
  226. package/dist/esm/test/TestStores.d.ts +4 -4
  227. package/dist/esm/test/TestStores.d.ts.map +1 -1
  228. package/package.json +22 -22
  229. package/src/ArtifactGc.ts +1 -1
  230. package/src/DeferredClockFold.ts +1 -1
  231. package/src/DisasterRecovery.ts +1 -1
  232. package/src/DurableEngineState.ts +1 -1
  233. package/src/ExecutionSnapshot.ts +1 -1
  234. package/src/OwnerIdentity.ts +1 -1
  235. package/src/PlanInputStore.ts +1 -1
  236. package/src/PlanMergeStore.ts +1 -1
  237. package/src/Retention.ts +2 -2
  238. package/src/RunChangeFeed.ts +1 -1
  239. package/src/SelectionStore.ts +1 -1
  240. package/src/StepBoundary.ts +3 -3
  241. package/src/StepCacheFold.ts +1 -1
  242. package/src/internal/EngineStateSchema.ts +1 -1
  243. package/src/internal/ExecutionSnapshotRead.ts +1 -1
  244. package/src/internal/ExecutionSnapshotRelations.ts +1 -1
  245. package/src/internal/RetentionOps.ts +2 -2
  246. package/src/internal/RunCatalogOps.ts +1 -1
  247. package/src/internal/RunListing.ts +1 -1
  248. package/src/migrations/0001_initial.ts +1 -1
  249. package/src/migrations/0002_selection_store.ts +1 -1
  250. package/src/migrations/0003_plan_inputs.ts +1 -1
  251. package/src/migrations/0004_plan_environment.ts +1 -1
  252. package/src/migrations/0005_plan_merges.ts +1 -1
  253. package/src/migrations/0006_execution_listing.ts +1 -1
  254. package/src/migrations/0007_run_parent_sequence.ts +1 -1
  255. package/src/migrations/0008_deferred_consumption.ts +1 -1
  256. package/src/migrations/0009_deferred_clock_facts.ts +1 -1
  257. package/src/migrations/0010_pending_cancellation.ts +1 -1
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
4
4
 
5
- This package declares `effect` as an exact
6
- `4.0.0-rc.115` peer dependency. Keep the application on that version so
7
- all Smithers packages share one Effect runtime.
5
+ This package declares `effect` as a `^4.0.4` peer dependency. Install one
6
+ `effect` release in that range so all Smithers packages share one Effect
7
+ runtime.
8
8
 
9
9
  **Documentation:** https://engine-store.smithers.sh
10
10
 
@@ -140,8 +140,8 @@ The full documentation is at <https://engine-store.smithers.sh>:
140
140
  - [Troubleshooting](https://engine-store.smithers.sh/troubleshooting/): each
141
141
  typed failure, its cause, and its fix.
142
142
 
143
- See also [durable execution](https://smithers.sh/docs/concepts/durable-execution/)
144
- and [content addressing](https://smithers.sh/docs/concepts/content-addressing/)
143
+ See also [durable execution](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/docs/README.md)
144
+ and [content addressing](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/docs/concepts/step-keys.md)
145
145
  on smithers.sh.
146
146
 
147
147
  List reads of durable wait state skip corrupt rows and log storage-integrity warnings; point reads still fail for an invalid row.
@@ -43,7 +43,7 @@ import * as Effect from "effect/Effect";
43
43
  import type * as FileSystem from "effect/FileSystem";
44
44
  import * as Layer from "effect/Layer";
45
45
  import * as Schema from "effect/Schema";
46
- import * as SqlClient from "effect/unstable/sql/SqlClient";
46
+ import * as SqlClient from "effect/sql/SqlClient";
47
47
  /**
48
48
  * Stable error codes returned by artifact garbage collection.
49
49
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ArtifactGc.d.ts","sourceRoot":"","sources":["../../src/ArtifactGc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ArtifactGc.d.ts","sourceRoot":"","sources":["../../src/ArtifactGc.ts"],"names":[],"mappings":"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"}
@@ -46,7 +46,7 @@ var Effect = __toESM(require("effect/Effect"), 1);
46
46
  var Layer = __toESM(require("effect/Layer"), 1);
47
47
  var Option = __toESM(require("effect/Option"), 1);
48
48
  var Schema = __toESM(require("effect/Schema"), 1);
49
- var SqlClient = __toESM(require("effect/unstable/sql/SqlClient"), 1);
49
+ var SqlClient = __toESM(require("effect/sql/SqlClient"), 1);
50
50
  var ArtifactRoots = __toESM(require("./internal/ArtifactRoots.js"), 1);
51
51
  const ArtifactGcErrorCode = Schema.Literals(["invalid_options", "mark_failed", "sweep_failed"]);
52
52
  class ArtifactGcError extends Schema.TaggedError()(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ArtifactGc.ts"],
4
- "sourcesContent": ["/**\n * Mark/sweep garbage collection for the content-addressed artifact store.\n *\n * The artifact tier is the one store in the composition with true *garbage*:\n * every output over the inline bound is spilled to `.flows/objects` by digest\n * (`StepBoundary`), nothing ever deletes a published blob, and a blob whose\n * every referencing row is gone is unreachable forever. The step cache is\n * deliberately NOT collected here \u2014 its rows are the *roots* of this graph,\n * they never become unreachable, and removing one is the policy decision\n * `CacheStore.evict` already serves.\n *\n * The design combines the two prior arts on the shelf:\n *\n * - **Reachability from durable roots** \u2014 git's `git gc` marks from refs;\n * Skyframe deletes what a keyed graph walk cannot reach. Here the roots are\n * every attempt row of every existing (never-deleted) run and every step\n * cache entry; each carries boundary evidence, and\n * `StepBoundary.referencedDigests` names the blobs that evidence needs.\n * Attempt checkpoints are roots too: they are opaque executable state, so\n * the mark keeps digest-shaped strings found in their JSON.\n * - **A grace period over mtime** \u2014 git refuses to prune unreachable objects\n * younger than `gc.pruneExpire` (two weeks), jj keeps operations newer than\n * its keep bound, and Bazel's disk-cache collector\n * (`reference/bazel/.../remote/disk/DiskCacheGarbageCollector.java`) fences\n * on mtime. The cutoff is the collection start time minus the grace period,\n * sampled before resolving policy pins or scanning roots and held fixed\n * through inventory and deletion. Only mtimes strictly before it are\n * eligible: a blob published \u2014 or freshened by a dedupe `put` \u2014 at or after\n * collection start survives, including same-millisecond writes at zero\n * grace, no lock required.\n *\n * Collection NEVER runs automatically. `gc()` is an explicit verb because\n * deletion is irreversible, and a human approving a plan must approve the\n * deletions too. The opt-in {@link ArtifactGcPolicy} layer\n * configures *how* an invocation collects (grace bound, pinned digests); it\n * does not schedule anything.\n *\n * @since 0.1.0\n */\n\nimport * as ArtifactSweep from \"@smthrs/artifacts/ArtifactSweep\"\nimport * as Clock from \"effect/Clock\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport type * as FileSystem from \"effect/FileSystem\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\nimport * as ArtifactRoots from \"./internal/ArtifactRoots.ts\"\n\n/**\n * Stable error codes returned by artifact garbage collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport const ArtifactGcErrorCode = Schema.Literals([\"invalid_options\", \"mark_failed\", \"sweep_failed\"])\n\n/**\n * Stable error codes returned by artifact garbage collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport type ArtifactGcErrorCode = typeof ArtifactGcErrorCode.Type\n\n/**\n * A collection that could not complete. `mark_failed` means the live set\n * could not be computed \u2014 nothing was deleted. `sweep_failed` means the live\n * set held but a deletion refused; everything already swept was garbage, so a\n * re-run converges from wherever the failure stopped it.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class ArtifactGcError extends Schema.TaggedError<ArtifactGcError>()(\n \"@smthrs/engine-store/ArtifactGcError\",\n {\n code: ArtifactGcErrorCode,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown)\n }\n) {}\n\n/**\n * The opt-in collection policy. Explicit {@link GcOptions} on a `gc()` call\n * override the installed policy; the policy overrides the defaults.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Policy {\n /** Grace bound applied when a `gc()` call does not name one. */\n readonly graceMs?: number | undefined\n /**\n * Digests held live regardless of reachability \u2014 the seam for references\n * this composition's tables cannot see (an external index, an export a\n * human wants kept). Resolved fresh on every collection.\n */\n readonly pins?: Effect.Effect<ReadonlyArray<string>> | undefined\n}\n\n/**\n * Service tag for the opt-in collection policy.\n *\n * @category services\n * @since 0.1.0\n */\nexport class ArtifactGcPolicy extends Context.Service<ArtifactGcPolicy, Policy>()(\n \"@smthrs/engine-store/ArtifactGcPolicy\"\n) {}\n\n/**\n * Installs a collection policy.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerPolicy = (policy: Policy): Layer.Layer<ArtifactGcPolicy> => Layer.succeed(ArtifactGcPolicy)(policy)\n\n/**\n * Options for one explicit collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface GcOptions {\n /**\n * How recently a blob must have been written or freshened to survive\n * unreferenced. Defaults to the installed {@link ArtifactGcPolicy}'s bound,\n * then to {@link defaultGraceMs}.\n */\n readonly graceMs?: number | undefined\n /** Digests held live for this collection, unioned with the policy's pins. */\n readonly pins?: ReadonlyArray<string> | undefined\n /** Compute and report without deleting anything. */\n readonly dryRun?: boolean | undefined\n}\n\n/**\n * What one collection did.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface GcReport {\n /** Blobs the inventory enumerated. */\n readonly scannedBlobs: number\n /** Distinct digests the mark phase proved live, pins included. */\n readonly liveDigests: number\n /** Digests deleted \u2014 or, under `dryRun`, the ones a real run would delete. */\n readonly sweptDigests: ReadonlyArray<string>\n /** Bytes the swept blobs held. */\n readonly reclaimedBytes: number\n /** Unreferenced blobs retained by the grace bound or a freshen race. */\n readonly keptByGrace: number\n readonly dryRun: boolean\n}\n\n/**\n * Explicit artifact garbage collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Service {\n /**\n * Marks the live set from the durable roots, then sweeps every blob that\n * is both outside it and older than the grace bound. Explicit only \u2014\n * nothing in the engine composition ever calls this.\n */\n readonly gc: (options?: GcOptions) => Effect.Effect<GcReport, ArtifactGcError>\n}\n\n/**\n * Service tag for artifact garbage collection.\n *\n * @category services\n * @since 0.1.0\n */\nexport class ArtifactGc extends Context.Service<ArtifactGc, Service>()(\"@smthrs/engine-store/ArtifactGc\") {}\n\n/**\n * Two weeks, git's `gc.pruneExpire` default. The bound is deliberately far\n * beyond any live attempt's duration: a blob spilled by a running step is\n * unreferenced until its attempt row finishes, and the grace period is the\n * only thing protecting it in that window.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const defaultGraceMs = 14 * 24 * 60 * 60 * 1000\n\n/**\n * Construction options.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface MakeOptions {\n /** Rows per mark-phase page. Defaults to 500. */\n readonly pageSize?: number | undefined\n}\n\nconst markFailed = (message: string, cause: unknown): ArtifactGcError =>\n new ArtifactGcError({ code: \"mark_failed\", message, cause })\n\nconst sweepFailed = (message: string, cause: unknown): ArtifactGcError =>\n new ArtifactGcError({ code: \"sweep_failed\", message, cause })\n\nconst invalidOptions = (message: string): ArtifactGcError => new ArtifactGcError({ code: \"invalid_options\", message })\n\n/**\n * Extracts the digests one root row keeps live, FAIL-SAFE: a row whose\n * metadata carries a `boundary` this build cannot decode aborts the\n * collection rather than contributing nothing. Silently reading such a row as\n * \"references no artifacts\" is exactly how a live blob gets collected; a\n * metadata shape with no `boundary` key at all is the ordinary\n * foreign-evidence case `referencedDigests` already defines as empty.\n */\nconst rootDigests = (table: string, metaJson: string): Effect.Effect<ReadonlyArray<string>, ArtifactGcError> =>\n ArtifactRoots.rootDigests(table, metaJson).pipe(\n Effect.mapError((cause) => markFailed(cause.message, cause.cause))\n )\n\nconst checkpointDigests = (checkpointJson: string | null): Effect.Effect<ReadonlyArray<string>, ArtifactGcError> =>\n ArtifactRoots.checkpointDigests(checkpointJson).pipe(\n Effect.mapError((cause) => markFailed(cause.message, cause.cause))\n )\n\n/**\n * Builds the collector over the composition's own durable tables and the\n * host-local sweep surface.\n *\n * The mark phase reads `flows_step_cache` and `flows_attempts` directly, the\n * same way `DurableEngineState` range-scans `flows_runs`: this package\n * composes every one of those migrations, so the schema is its own. Every run\n * present in `flows_runs` is a live root \u2014 there is no deleted state, and the\n * attempt table's foreign key guarantees each attempt's run exists \u2014 so the\n * scan is simply *all* attempt rows plus *all* cache entries, paged by\n * primary key so no page holds more than `pageSize` rows in memory.\n *\n * Ordering is the safety argument. The live set is computed BEFORE the\n * inventory, so a root recorded during the sweep can only be missed, never\n * half-seen \u2014 and the blob such a root references is protected anyway: a\n * fresh publication carries a fresh mtime, a re-publication of existing bytes\n * freshens the blob's mtime (`ArtifactStore.put`), and the sweep's deletion\n * is fenced on that mtime (`ArtifactSweep.remove`'s `ifUnmodifiedSinceMs`).\n * A crash mid-sweep deletes some garbage and no live blobs; re-running\n * converges.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (\n options: MakeOptions = {}\n): Effect.Effect<Service, never, SqlClient.SqlClient | ArtifactSweep.ArtifactSweep> =>\n Effect.gen(function*() {\n const sql = yield* Effect.service(SqlClient.SqlClient)\n const sweeper = yield* ArtifactSweep.ArtifactSweep\n const policy = yield* Effect.serviceOption(ArtifactGcPolicy)\n const pageSize = options.pageSize ?? 500\n\n const markCacheEntries = (live: Set<string>) =>\n Effect.gen(function*() {\n let after = \"\"\n for (;;) {\n const rows = yield* sql<{ readonly key_digest: string; readonly meta_json: string }>`\n SELECT key_digest, meta_json FROM flows_step_cache\n WHERE key_digest > ${after}\n ORDER BY key_digest\n LIMIT ${pageSize}\n `.pipe(Effect.mapError((cause) => markFailed(\"the step cache could not be scanned\", cause)))\n for (const row of rows) {\n for (const digest of yield* rootDigests(\"flows_step_cache\", row.meta_json)) {\n live.add(digest)\n }\n }\n if (rows.length < pageSize) return\n after = rows[rows.length - 1]!.key_digest\n }\n })\n\n const markAttempts = (live: Set<string>) =>\n Effect.gen(function*() {\n let after = { runId: \"\", stepKeyDigest: \"\", attempt: -1 }\n for (;;) {\n const rows = yield* sql<{\n readonly run_id: string\n readonly step_key_digest: string\n readonly attempt: number\n readonly checkpoint_json: string | null\n readonly meta_json: string\n }>`\n SELECT run_id, step_key_digest, attempt, checkpoint_json, meta_json FROM flows_attempts\n WHERE (run_id, step_key_digest, attempt) > (${after.runId}, ${after.stepKeyDigest}, ${after.attempt})\n ORDER BY run_id, step_key_digest, attempt\n LIMIT ${pageSize}\n `.pipe(Effect.mapError((cause) => markFailed(\"the attempt table could not be scanned\", cause)))\n for (const row of rows) {\n for (const digest of yield* rootDigests(\"flows_attempts\", row.meta_json)) {\n live.add(digest)\n }\n for (const digest of yield* checkpointDigests(row.checkpoint_json)) {\n live.add(digest)\n }\n }\n if (rows.length < pageSize) return\n const last = rows[rows.length - 1]!\n after = { runId: last.run_id, stepKeyDigest: last.step_key_digest, attempt: last.attempt }\n }\n })\n\n const gc: Service[\"gc\"] = Effect.fn(\"ArtifactGc.gc\")((gcOptions) =>\n Effect.gen(function*() {\n if (!Number.isSafeInteger(pageSize) || pageSize < 1) {\n return yield* Effect.fail(invalidOptions(\"artifact GC pageSize must be a positive safe integer\"))\n }\n const grace = gcOptions?.graceMs ??\n (Option.isSome(policy) ? policy.value.graceMs : undefined) ??\n defaultGraceMs\n if (!Number.isSafeInteger(grace) || grace < 0) {\n return yield* Effect.fail(invalidOptions(\"artifact GC graceMs must be a non-negative safe integer\"))\n }\n const startedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))\n const bound = startedAtMs - grace\n const live = new Set<string>(gcOptions?.pins ?? [])\n if (Option.isSome(policy) && policy.value.pins !== undefined) {\n for (const digest of yield* policy.value.pins) live.add(digest)\n }\n yield* markCacheEntries(live)\n yield* markAttempts(live)\n const blobs = yield* sweeper.inventory.pipe(\n Effect.mapError((cause) => sweepFailed(`the blob inventory refused: ${cause.message}`, cause))\n )\n const swept: Array<string> = []\n let reclaimedBytes = 0\n let keptByGrace = 0\n for (const blob of blobs) {\n if (live.has(blob.digest)) continue\n if (blob.modifiedAtMs >= bound) {\n keptByGrace++\n continue\n }\n if (gcOptions?.dryRun === true) {\n swept.push(blob.digest)\n reclaimedBytes += blob.sizeBytes\n continue\n }\n // The fence re-reads the blob's age inside the deletion: an\n // unreferenced-at-mark blob a concurrent `put` freshened since the\n // inventory fails the fence and is retained, exactly as a fresher\n // cache row survives a laggard's fenced evict (issue #119).\n // remove's fence is inclusive; mtimes have millisecond resolution,\n // so exclude cutoff equality there just as in the inventory check.\n const removed = yield* sweeper.remove(blob.digest, { ifUnmodifiedSinceMs: bound - 1 }).pipe(\n Effect.mapError((cause) => sweepFailed(`sweeping ${blob.digest} refused: ${cause.message}`, cause))\n )\n if (removed) {\n swept.push(blob.digest)\n reclaimedBytes += blob.sizeBytes\n } else {\n keptByGrace++\n }\n }\n return {\n scannedBlobs: blobs.length,\n liveDigests: live.size,\n sweptDigests: swept,\n reclaimedBytes,\n keptByGrace,\n dryRun: gcOptions?.dryRun === true\n }\n })\n )\n\n return { gc }\n })\n\n/**\n * Provides the artifact garbage collector. Install {@link layerPolicy} into\n * this layer's inputs to configure collection; nothing schedules `gc()` \u2014\n * invoking it stays an explicit caller decision.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = (\n options: MakeOptions = {}\n): Layer.Layer<ArtifactGc, never, SqlClient.SqlClient | ArtifactSweep.ArtifactSweep> =>\n Layer.effect(ArtifactGc)(make(options))\n\n/**\n * Provides the artifact garbage collector over a filesystem objects directory:\n * {@link layer} with `ArtifactSweep.layerFileSystem` beneath it. Pass the same\n * `directory` and `coordination` the paired `ArtifactStore.layerFileSystem`\n * publishes with.\n *\n * @category layers\n * @since 1.0.0\n */\nexport const layerFileSystem = (\n options: MakeOptions & ArtifactSweep.SweepOptions = {}\n): Layer.Layer<ArtifactGc, never, SqlClient.SqlClient | FileSystem.FileSystem> =>\n layer(options).pipe(\n Layer.provide(ArtifactSweep.layerFileSystem({ directory: options.directory, coordination: options.coordination }))\n )\n"],
4
+ "sourcesContent": ["/**\n * Mark/sweep garbage collection for the content-addressed artifact store.\n *\n * The artifact tier is the one store in the composition with true *garbage*:\n * every output over the inline bound is spilled to `.flows/objects` by digest\n * (`StepBoundary`), nothing ever deletes a published blob, and a blob whose\n * every referencing row is gone is unreachable forever. The step cache is\n * deliberately NOT collected here \u2014 its rows are the *roots* of this graph,\n * they never become unreachable, and removing one is the policy decision\n * `CacheStore.evict` already serves.\n *\n * The design combines the two prior arts on the shelf:\n *\n * - **Reachability from durable roots** \u2014 git's `git gc` marks from refs;\n * Skyframe deletes what a keyed graph walk cannot reach. Here the roots are\n * every attempt row of every existing (never-deleted) run and every step\n * cache entry; each carries boundary evidence, and\n * `StepBoundary.referencedDigests` names the blobs that evidence needs.\n * Attempt checkpoints are roots too: they are opaque executable state, so\n * the mark keeps digest-shaped strings found in their JSON.\n * - **A grace period over mtime** \u2014 git refuses to prune unreachable objects\n * younger than `gc.pruneExpire` (two weeks), jj keeps operations newer than\n * its keep bound, and Bazel's disk-cache collector\n * (`reference/bazel/.../remote/disk/DiskCacheGarbageCollector.java`) fences\n * on mtime. The cutoff is the collection start time minus the grace period,\n * sampled before resolving policy pins or scanning roots and held fixed\n * through inventory and deletion. Only mtimes strictly before it are\n * eligible: a blob published \u2014 or freshened by a dedupe `put` \u2014 at or after\n * collection start survives, including same-millisecond writes at zero\n * grace, no lock required.\n *\n * Collection NEVER runs automatically. `gc()` is an explicit verb because\n * deletion is irreversible, and a human approving a plan must approve the\n * deletions too. The opt-in {@link ArtifactGcPolicy} layer\n * configures *how* an invocation collects (grace bound, pinned digests); it\n * does not schedule anything.\n *\n * @since 0.1.0\n */\n\nimport * as ArtifactSweep from \"@smthrs/artifacts/ArtifactSweep\"\nimport * as Clock from \"effect/Clock\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport type * as FileSystem from \"effect/FileSystem\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/sql/SqlClient\"\nimport * as ArtifactRoots from \"./internal/ArtifactRoots.ts\"\n\n/**\n * Stable error codes returned by artifact garbage collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport const ArtifactGcErrorCode = Schema.Literals([\"invalid_options\", \"mark_failed\", \"sweep_failed\"])\n\n/**\n * Stable error codes returned by artifact garbage collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport type ArtifactGcErrorCode = typeof ArtifactGcErrorCode.Type\n\n/**\n * A collection that could not complete. `mark_failed` means the live set\n * could not be computed \u2014 nothing was deleted. `sweep_failed` means the live\n * set held but a deletion refused; everything already swept was garbage, so a\n * re-run converges from wherever the failure stopped it.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class ArtifactGcError extends Schema.TaggedError<ArtifactGcError>()(\n \"@smthrs/engine-store/ArtifactGcError\",\n {\n code: ArtifactGcErrorCode,\n message: Schema.String,\n cause: Schema.optional(Schema.Unknown)\n }\n) {}\n\n/**\n * The opt-in collection policy. Explicit {@link GcOptions} on a `gc()` call\n * override the installed policy; the policy overrides the defaults.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Policy {\n /** Grace bound applied when a `gc()` call does not name one. */\n readonly graceMs?: number | undefined\n /**\n * Digests held live regardless of reachability \u2014 the seam for references\n * this composition's tables cannot see (an external index, an export a\n * human wants kept). Resolved fresh on every collection.\n */\n readonly pins?: Effect.Effect<ReadonlyArray<string>> | undefined\n}\n\n/**\n * Service tag for the opt-in collection policy.\n *\n * @category services\n * @since 0.1.0\n */\nexport class ArtifactGcPolicy extends Context.Service<ArtifactGcPolicy, Policy>()(\n \"@smthrs/engine-store/ArtifactGcPolicy\"\n) {}\n\n/**\n * Installs a collection policy.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerPolicy = (policy: Policy): Layer.Layer<ArtifactGcPolicy> => Layer.succeed(ArtifactGcPolicy)(policy)\n\n/**\n * Options for one explicit collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface GcOptions {\n /**\n * How recently a blob must have been written or freshened to survive\n * unreferenced. Defaults to the installed {@link ArtifactGcPolicy}'s bound,\n * then to {@link defaultGraceMs}.\n */\n readonly graceMs?: number | undefined\n /** Digests held live for this collection, unioned with the policy's pins. */\n readonly pins?: ReadonlyArray<string> | undefined\n /** Compute and report without deleting anything. */\n readonly dryRun?: boolean | undefined\n}\n\n/**\n * What one collection did.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface GcReport {\n /** Blobs the inventory enumerated. */\n readonly scannedBlobs: number\n /** Distinct digests the mark phase proved live, pins included. */\n readonly liveDigests: number\n /** Digests deleted \u2014 or, under `dryRun`, the ones a real run would delete. */\n readonly sweptDigests: ReadonlyArray<string>\n /** Bytes the swept blobs held. */\n readonly reclaimedBytes: number\n /** Unreferenced blobs retained by the grace bound or a freshen race. */\n readonly keptByGrace: number\n readonly dryRun: boolean\n}\n\n/**\n * Explicit artifact garbage collection.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Service {\n /**\n * Marks the live set from the durable roots, then sweeps every blob that\n * is both outside it and older than the grace bound. Explicit only \u2014\n * nothing in the engine composition ever calls this.\n */\n readonly gc: (options?: GcOptions) => Effect.Effect<GcReport, ArtifactGcError>\n}\n\n/**\n * Service tag for artifact garbage collection.\n *\n * @category services\n * @since 0.1.0\n */\nexport class ArtifactGc extends Context.Service<ArtifactGc, Service>()(\"@smthrs/engine-store/ArtifactGc\") {}\n\n/**\n * Two weeks, git's `gc.pruneExpire` default. The bound is deliberately far\n * beyond any live attempt's duration: a blob spilled by a running step is\n * unreferenced until its attempt row finishes, and the grace period is the\n * only thing protecting it in that window.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const defaultGraceMs = 14 * 24 * 60 * 60 * 1000\n\n/**\n * Construction options.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface MakeOptions {\n /** Rows per mark-phase page. Defaults to 500. */\n readonly pageSize?: number | undefined\n}\n\nconst markFailed = (message: string, cause: unknown): ArtifactGcError =>\n new ArtifactGcError({ code: \"mark_failed\", message, cause })\n\nconst sweepFailed = (message: string, cause: unknown): ArtifactGcError =>\n new ArtifactGcError({ code: \"sweep_failed\", message, cause })\n\nconst invalidOptions = (message: string): ArtifactGcError => new ArtifactGcError({ code: \"invalid_options\", message })\n\n/**\n * Extracts the digests one root row keeps live, FAIL-SAFE: a row whose\n * metadata carries a `boundary` this build cannot decode aborts the\n * collection rather than contributing nothing. Silently reading such a row as\n * \"references no artifacts\" is exactly how a live blob gets collected; a\n * metadata shape with no `boundary` key at all is the ordinary\n * foreign-evidence case `referencedDigests` already defines as empty.\n */\nconst rootDigests = (table: string, metaJson: string): Effect.Effect<ReadonlyArray<string>, ArtifactGcError> =>\n ArtifactRoots.rootDigests(table, metaJson).pipe(\n Effect.mapError((cause) => markFailed(cause.message, cause.cause))\n )\n\nconst checkpointDigests = (checkpointJson: string | null): Effect.Effect<ReadonlyArray<string>, ArtifactGcError> =>\n ArtifactRoots.checkpointDigests(checkpointJson).pipe(\n Effect.mapError((cause) => markFailed(cause.message, cause.cause))\n )\n\n/**\n * Builds the collector over the composition's own durable tables and the\n * host-local sweep surface.\n *\n * The mark phase reads `flows_step_cache` and `flows_attempts` directly, the\n * same way `DurableEngineState` range-scans `flows_runs`: this package\n * composes every one of those migrations, so the schema is its own. Every run\n * present in `flows_runs` is a live root \u2014 there is no deleted state, and the\n * attempt table's foreign key guarantees each attempt's run exists \u2014 so the\n * scan is simply *all* attempt rows plus *all* cache entries, paged by\n * primary key so no page holds more than `pageSize` rows in memory.\n *\n * Ordering is the safety argument. The live set is computed BEFORE the\n * inventory, so a root recorded during the sweep can only be missed, never\n * half-seen \u2014 and the blob such a root references is protected anyway: a\n * fresh publication carries a fresh mtime, a re-publication of existing bytes\n * freshens the blob's mtime (`ArtifactStore.put`), and the sweep's deletion\n * is fenced on that mtime (`ArtifactSweep.remove`'s `ifUnmodifiedSinceMs`).\n * A crash mid-sweep deletes some garbage and no live blobs; re-running\n * converges.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (\n options: MakeOptions = {}\n): Effect.Effect<Service, never, SqlClient.SqlClient | ArtifactSweep.ArtifactSweep> =>\n Effect.gen(function*() {\n const sql = yield* Effect.service(SqlClient.SqlClient)\n const sweeper = yield* ArtifactSweep.ArtifactSweep\n const policy = yield* Effect.serviceOption(ArtifactGcPolicy)\n const pageSize = options.pageSize ?? 500\n\n const markCacheEntries = (live: Set<string>) =>\n Effect.gen(function*() {\n let after = \"\"\n for (;;) {\n const rows = yield* sql<{ readonly key_digest: string; readonly meta_json: string }>`\n SELECT key_digest, meta_json FROM flows_step_cache\n WHERE key_digest > ${after}\n ORDER BY key_digest\n LIMIT ${pageSize}\n `.pipe(Effect.mapError((cause) => markFailed(\"the step cache could not be scanned\", cause)))\n for (const row of rows) {\n for (const digest of yield* rootDigests(\"flows_step_cache\", row.meta_json)) {\n live.add(digest)\n }\n }\n if (rows.length < pageSize) return\n after = rows[rows.length - 1]!.key_digest\n }\n })\n\n const markAttempts = (live: Set<string>) =>\n Effect.gen(function*() {\n let after = { runId: \"\", stepKeyDigest: \"\", attempt: -1 }\n for (;;) {\n const rows = yield* sql<{\n readonly run_id: string\n readonly step_key_digest: string\n readonly attempt: number\n readonly checkpoint_json: string | null\n readonly meta_json: string\n }>`\n SELECT run_id, step_key_digest, attempt, checkpoint_json, meta_json FROM flows_attempts\n WHERE (run_id, step_key_digest, attempt) > (${after.runId}, ${after.stepKeyDigest}, ${after.attempt})\n ORDER BY run_id, step_key_digest, attempt\n LIMIT ${pageSize}\n `.pipe(Effect.mapError((cause) => markFailed(\"the attempt table could not be scanned\", cause)))\n for (const row of rows) {\n for (const digest of yield* rootDigests(\"flows_attempts\", row.meta_json)) {\n live.add(digest)\n }\n for (const digest of yield* checkpointDigests(row.checkpoint_json)) {\n live.add(digest)\n }\n }\n if (rows.length < pageSize) return\n const last = rows[rows.length - 1]!\n after = { runId: last.run_id, stepKeyDigest: last.step_key_digest, attempt: last.attempt }\n }\n })\n\n const gc: Service[\"gc\"] = Effect.fn(\"ArtifactGc.gc\")((gcOptions) =>\n Effect.gen(function*() {\n if (!Number.isSafeInteger(pageSize) || pageSize < 1) {\n return yield* Effect.fail(invalidOptions(\"artifact GC pageSize must be a positive safe integer\"))\n }\n const grace = gcOptions?.graceMs ??\n (Option.isSome(policy) ? policy.value.graceMs : undefined) ??\n defaultGraceMs\n if (!Number.isSafeInteger(grace) || grace < 0) {\n return yield* Effect.fail(invalidOptions(\"artifact GC graceMs must be a non-negative safe integer\"))\n }\n const startedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))\n const bound = startedAtMs - grace\n const live = new Set<string>(gcOptions?.pins ?? [])\n if (Option.isSome(policy) && policy.value.pins !== undefined) {\n for (const digest of yield* policy.value.pins) live.add(digest)\n }\n yield* markCacheEntries(live)\n yield* markAttempts(live)\n const blobs = yield* sweeper.inventory.pipe(\n Effect.mapError((cause) => sweepFailed(`the blob inventory refused: ${cause.message}`, cause))\n )\n const swept: Array<string> = []\n let reclaimedBytes = 0\n let keptByGrace = 0\n for (const blob of blobs) {\n if (live.has(blob.digest)) continue\n if (blob.modifiedAtMs >= bound) {\n keptByGrace++\n continue\n }\n if (gcOptions?.dryRun === true) {\n swept.push(blob.digest)\n reclaimedBytes += blob.sizeBytes\n continue\n }\n // The fence re-reads the blob's age inside the deletion: an\n // unreferenced-at-mark blob a concurrent `put` freshened since the\n // inventory fails the fence and is retained, exactly as a fresher\n // cache row survives a laggard's fenced evict (issue #119).\n // remove's fence is inclusive; mtimes have millisecond resolution,\n // so exclude cutoff equality there just as in the inventory check.\n const removed = yield* sweeper.remove(blob.digest, { ifUnmodifiedSinceMs: bound - 1 }).pipe(\n Effect.mapError((cause) => sweepFailed(`sweeping ${blob.digest} refused: ${cause.message}`, cause))\n )\n if (removed) {\n swept.push(blob.digest)\n reclaimedBytes += blob.sizeBytes\n } else {\n keptByGrace++\n }\n }\n return {\n scannedBlobs: blobs.length,\n liveDigests: live.size,\n sweptDigests: swept,\n reclaimedBytes,\n keptByGrace,\n dryRun: gcOptions?.dryRun === true\n }\n })\n )\n\n return { gc }\n })\n\n/**\n * Provides the artifact garbage collector. Install {@link layerPolicy} into\n * this layer's inputs to configure collection; nothing schedules `gc()` \u2014\n * invoking it stays an explicit caller decision.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = (\n options: MakeOptions = {}\n): Layer.Layer<ArtifactGc, never, SqlClient.SqlClient | ArtifactSweep.ArtifactSweep> =>\n Layer.effect(ArtifactGc)(make(options))\n\n/**\n * Provides the artifact garbage collector over a filesystem objects directory:\n * {@link layer} with `ArtifactSweep.layerFileSystem` beneath it. Pass the same\n * `directory` and `coordination` the paired `ArtifactStore.layerFileSystem`\n * publishes with.\n *\n * @category layers\n * @since 1.0.0\n */\nexport const layerFileSystem = (\n options: MakeOptions & ArtifactSweep.SweepOptions = {}\n): Layer.Layer<ArtifactGc, never, SqlClient.SqlClient | FileSystem.FileSystem> =>\n layer(options).pipe(\n Layer.provide(ArtifactSweep.layerFileSystem({ directory: options.directory, coordination: options.coordination }))\n )\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -36,7 +36,7 @@ import { Journal, JournalEvent, Projection } from "@smthrs/journal";
36
36
  import * as Effect from "effect/Effect";
37
37
  import * as Option from "effect/Option";
38
38
  import * as Schema from "effect/Schema";
39
- import * as SqlClient from "effect/unstable/sql/SqlClient";
39
+ import * as SqlClient from "effect/sql/SqlClient";
40
40
  /**
41
41
  * The journal event types the fold reads.
42
42
  *
@@ -276,7 +276,7 @@ export declare const capture: (runId: string, upTo: number) => Effect.Effect<{
276
276
  readonly exitDigest?: string;
277
277
  readonly metadataDigest?: string | null;
278
278
  }[];
279
- }, import("effect/unstable/sql/SqlError").SqlError | Journal.JournalError | FoldIncomplete, SqlClient.SqlClient | Journal.Journal>;
279
+ }, import("effect/sql/SqlError").SqlError | Journal.JournalError | FoldIncomplete, SqlClient.SqlClient | Journal.Journal>;
280
280
  /**
281
281
  * A checkpoint state that keeps the fold: `base` (an object, or nothing) with
282
282
  * the run's fold up to `upTo` under {@link checkpointKey}. A composition that
@@ -306,7 +306,7 @@ export declare const checkpointState: (runId: string, upTo: number, base?: Reado
306
306
  readonly metadataDigest?: string | null;
307
307
  }[];
308
308
  };
309
- }, import("effect/unstable/sql/SqlError").SqlError | Journal.JournalError | FoldIncomplete, SqlClient.SqlClient | Journal.Journal>;
309
+ }, import("effect/sql/SqlError").SqlError | Journal.JournalError | FoldIncomplete, SqlClient.SqlClient | Journal.Journal>;
310
310
  /**
311
311
  * What a {@link rebuild} read and wrote.
312
312
  *
@@ -349,6 +349,6 @@ export declare const rebuild: () => Effect.Effect<{
349
349
  clocks: number;
350
350
  deferreds: number;
351
351
  unexplained: number;
352
- }, import("effect/unstable/sql/SqlError").SqlError | import("@smthrs/database/DurableWriter").DatabaseError | Journal.JournalError | FoldIncomplete, SqlClient.SqlClient | DurableWriter | Journal.Journal>;
352
+ }, import("effect/sql/SqlError").SqlError | import("@smthrs/database/DurableWriter").DatabaseError | Journal.JournalError | FoldIncomplete, SqlClient.SqlClient | DurableWriter | Journal.Journal>;
353
353
  export {};
354
354
  //# sourceMappingURL=DeferredClockFold.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DeferredClockFold.d.ts","sourceRoot":"","sources":["../../src/DeferredClockFold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DeferredClockFold.d.ts","sourceRoot":"","sources":["../../src/DeferredClockFold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACnE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA;AAGjD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,aAAa,CAAC,MAAM,CAK5C,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,gCAAgC,CAAA;AAQ1D;;;;;GAKG;AACH,eAAO,MAAM,KAAK;;;;;;;EAOhB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAErC;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ;;;;;;;;EAQnB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;CAClD;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,KAAmD,CAAA;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GACnB,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,WACnB,CAAA;AAE/E;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,WACnB,CAAA;AAWlF;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,OAAO,YAAY,CAAC,KAAK,KAAG,KA4DhE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,CAIlD,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;EAGlB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,KAAG,OAGpC,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,iBAAiB,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAQ5E,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,mGAK/B,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,QAAQ;;;;;EAKnB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;;;;;;;;;;;AAE3C;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,mBAOnC;CAAG;AA8DJ;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;yHAElB,CAAA;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;yHAM1B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAUD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,OAAO;;;;;kMAkGlB,CAAA"}
@@ -53,7 +53,7 @@ var import_journal = require("@smthrs/journal");
53
53
  var Effect = __toESM(require("effect/Effect"), 1);
54
54
  var Option = __toESM(require("effect/Option"), 1);
55
55
  var Schema = __toESM(require("effect/Schema"), 1);
56
- var SqlClient = __toESM(require("effect/unstable/sql/SqlClient"), 1);
56
+ var SqlClient = __toESM(require("effect/sql/SqlClient"), 1);
57
57
  var import_EventTypes = require("./EventTypes.js");
58
58
  const eventTypes = [
59
59
  import_EventTypes.EventTypes.clockScheduled,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/DeferredClockFold.ts"],
4
- "sourcesContent": ["/**\n * The deferred/clock fold: `flows_clock_deadlines` and the mutable half of\n * `flows_deferred_completions` rebuilt from the journal.\n *\n * Every change to either table is journalled on the run's own stream in the\n * transaction that makes it:\n *\n * - `flows.engine.clock-scheduled` when a clock row is inserted;\n * - `flows.engine.clock-completed` when a clock fires or its run settles\n * first (`completeClock`, `completeRunClocks`);\n * - `flows.engine.deferred-completed` when the first completion wins, naming\n * the row and the {@link JournalEvent.contentDigest} of its encoded `exit`\n * and `metadata`;\n * - `flows.engine.deferred-consumed` when a run first observes a result.\n *\n * The journal is redacted and replayed to sync subscribers, so it never\n * carries a completion's value (issue #72). A clock row holds no value at\n * all, so the fold rebuilds it whole. A completion row's value stays in the\n * row: the fold rebuilds its `completed_at_ms` and `consumed_at_ms`, and a\n * rebuild checks the stored text against the recorded digests. A completion\n * the journal names but no row holds, or holds with other bytes, is a typed\n * {@link FoldIncomplete}, never a silent gap.\n *\n * Compaction deletes a run's entries below its floor, so the fold's state at\n * the floor has to survive in the floor's checkpoint: {@link checkpointState}\n * adds it under {@link checkpointKey}, and a rebuild of a compacted run whose\n * floor checkpoint lacks it fails {@link FoldIncomplete} with reason\n * `checkpoint_missing`. A rewind archives the suffix and removes the rows it\n * explains (`@smthrs/time-travel`), so the remaining journal still explains\n * the remaining rows.\n *\n * @since 1.0.0\n */\n\nimport { DurableWriter } from \"@smthrs/database/DurableWriter\"\nimport { Journal, JournalEvent, Projection } from \"@smthrs/journal\"\nimport * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\nimport { EventTypes } from \"./EventTypes.ts\"\n\n/**\n * The journal event types the fold reads.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const eventTypes: ReadonlyArray<string> = [\n EventTypes.clockScheduled,\n EventTypes.clockCompleted,\n EventTypes.deferredCompleted,\n EventTypes.deferredConsumed\n]\n\n/**\n * The key a checkpoint's state keeps the fold under.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const checkpointKey = \"flows.engine.deferred-clock\"\n\nconst NonNegativeSafeInt = Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(0),\n Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER)\n)\nconst Digest = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/))\n\n/**\n * A clock as the journal records it.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Clock = Schema.Struct({\n flowName: Schema.NonEmptyString,\n executionId: Schema.NonEmptyString,\n clockName: Schema.NonEmptyString,\n deferredName: Schema.NonEmptyString,\n dueAtMs: NonNegativeSafeInt,\n completedAtMs: Schema.NullOr(NonNegativeSafeInt)\n})\n\n/**\n * A clock as the journal records it.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Clock = typeof Clock.Type\n\n/**\n * A deferred completion as the journal records it. A completion journalled\n * before the fold existed carried its (redacted) value instead of digests and\n * no completion time: `exitDigest` and `metadataDigest` are then absent and\n * `completedAtMs` is `null`, and a rebuild keeps the row's own stamp.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Deferred = Schema.Struct({\n flowName: Schema.NonEmptyString,\n executionId: Schema.NonEmptyString,\n deferredName: Schema.NonEmptyString,\n completedAtMs: Schema.NullOr(NonNegativeSafeInt),\n exitDigest: Schema.optionalKey(Digest),\n metadataDigest: Schema.optionalKey(Schema.NullOr(Digest)),\n consumedAtMs: Schema.NullOr(NonNegativeSafeInt)\n})\n\n/**\n * A deferred completion as the journal records it.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Deferred = typeof Deferred.Type\n\n/**\n * The folded clocks and completions, keyed by their row address.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface State {\n readonly clocks: ReadonlyMap<string, Clock>\n readonly deferreds: ReadonlyMap<string, Deferred>\n}\n\n/**\n * The empty fold.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const initial: State = { clocks: new Map(), deferreds: new Map() }\n\n/**\n * The key of a clock row.\n *\n * @category models\n * @since 1.0.0\n */\nexport const clockKey = (\n address: { readonly flowName: string; readonly executionId: string; readonly clockName: string }\n) => JSON.stringify([address.flowName, address.executionId, address.clockName])\n\n/**\n * The key of a completion row.\n *\n * @category models\n * @since 1.0.0\n */\nexport const deferredKey = (\n address: { readonly flowName: string; readonly executionId: string; readonly deferredName: string }\n) => JSON.stringify([address.flowName, address.executionId, address.deferredName])\n\nconst record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? value as Readonly<Record<string, unknown>>\n : undefined\n\nconst text = (value: unknown): value is string => typeof value === \"string\" && value.length > 0\nconst time = (value: unknown): value is number => typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0\nconst digest = (value: unknown): value is string => typeof value === \"string\" && /^[0-9a-f]{64}$/.test(value)\n\n/**\n * Folds one committed journal entry. First writes win: a clock keeps its first\n * schedule and first completion, and a completion its first recording and\n * first consumption, exactly as the rows do. An entry of another type, or one\n * whose payload names no row, leaves the state unchanged.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const reduce = (state: State, entry: JournalEvent.Entry): State => {\n const payload = record(entry.payload)\n if (payload === undefined || !text(payload.flowName) || !text(payload.executionId)) return state\n const flowName = payload.flowName\n const executionId = payload.executionId\n switch (entry.eventType) {\n case EventTypes.clockScheduled: {\n if (!text(payload.clockName) || !text(payload.deferredName) || !time(payload.dueAtMs)) return state\n const key = clockKey({ flowName, executionId, clockName: payload.clockName })\n if (state.clocks.has(key)) return state\n const clocks = new Map(state.clocks)\n clocks.set(key, {\n flowName,\n executionId,\n clockName: payload.clockName,\n deferredName: payload.deferredName,\n dueAtMs: payload.dueAtMs,\n completedAtMs: null\n })\n return { ...state, clocks }\n }\n case EventTypes.clockCompleted: {\n if (!text(payload.clockName) || !time(payload.completedAtMs)) return state\n const key = clockKey({ flowName, executionId, clockName: payload.clockName })\n const clock = state.clocks.get(key)\n if (clock === undefined || clock.completedAtMs !== null) return state\n const clocks = new Map(state.clocks)\n clocks.set(key, { ...clock, completedAtMs: payload.completedAtMs })\n return { ...state, clocks }\n }\n case EventTypes.deferredCompleted: {\n if (!text(payload.deferredName)) return state\n const key = deferredKey({ flowName, executionId, deferredName: payload.deferredName })\n if (state.deferreds.has(key)) return state\n const deferreds = new Map(state.deferreds)\n deferreds.set(key, {\n flowName,\n executionId,\n deferredName: payload.deferredName,\n completedAtMs: time(payload.completedAtMs) ? payload.completedAtMs : null,\n ...(digest(payload.exitDigest) ? { exitDigest: payload.exitDigest } : {}),\n ...(digest(payload.exitDigest) && (payload.metadataDigest === null || digest(payload.metadataDigest))\n ? { metadataDigest: payload.metadataDigest }\n : {}),\n consumedAtMs: null\n })\n return { ...state, deferreds }\n }\n case EventTypes.deferredConsumed: {\n if (!text(payload.deferredName) || !time(payload.consumedAtMs)) return state\n const key = deferredKey({ flowName, executionId, deferredName: payload.deferredName })\n const deferred = state.deferreds.get(key)\n if (deferred === undefined || deferred.consumedAtMs !== null) return state\n const deferreds = new Map(state.deferreds)\n deferreds.set(key, { ...deferred, consumedAtMs: payload.consumedAtMs })\n return { ...state, deferreds }\n }\n default:\n return state\n }\n}\n\n/**\n * The fold as a journal projection, for `Journal.project` over one run.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const projection: Projection.Projection<State> = Projection.make({\n name: \"flows.engine.deferred-clock\",\n initial,\n reduce: (state, entry) => Effect.succeed(reduce(state, entry))\n})\n\n/**\n * The checkpoint encoding of the fold.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Encoded = Schema.Struct({\n clocks: Schema.Array(Clock),\n deferreds: Schema.Array(Deferred)\n})\n\n/**\n * The checkpoint encoding of the fold.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Encoded = typeof Encoded.Type\n\n/**\n * Encodes the fold for a checkpoint.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const encode = (state: State): Encoded => ({\n clocks: [...state.clocks.values()],\n deferreds: [...state.deferreds.values()]\n})\n\n/**\n * Decodes the fold a checkpoint kept, or `None` when the checkpoint state\n * holds none under {@link checkpointKey}.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const fromCheckpoint = (checkpointState: unknown): Option.Option<State> => {\n const kept = record(checkpointState)?.[checkpointKey]\n if (kept === undefined) return Option.none()\n const decoded = Schema.decodeUnknownOption(Encoded)(kept)\n return Option.map(decoded, (encoded) => ({\n clocks: new Map(encoded.clocks.map((clock) => [clockKey(clock), clock])),\n deferreds: new Map(encoded.deferreds.map((deferred) => [deferredKey(deferred), deferred]))\n }))\n}\n\n/**\n * Why a rebuild could not reconstruct the tables.\n *\n * - `row_missing`: the journal names a completion no row holds.\n * - `digest_mismatch`: the row holds other bytes than the journal recorded.\n * - `run_missing`: a clock's run has no `flows_runs` row to hang it on.\n * - `checkpoint_missing`: a compacted run's floor checkpoint keeps no fold.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const FoldIncompleteReason = Schema.Literals([\n \"row_missing\",\n \"digest_mismatch\",\n \"run_missing\",\n \"checkpoint_missing\"\n])\n\n/**\n * One reference a rebuild could not join.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const Unjoined = Schema.Struct({\n reason: FoldIncompleteReason,\n executionId: Schema.String,\n flowName: Schema.optionalKey(Schema.String),\n name: Schema.optionalKey(Schema.String)\n})\n\n/**\n * One reference a rebuild could not join.\n *\n * @category errors\n * @since 1.0.0\n */\nexport type Unjoined = typeof Unjoined.Type\n\n/**\n * The journal references state the store no longer holds. The rebuild wrote\n * nothing.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class FoldIncomplete extends Schema.TaggedError<FoldIncomplete>()(\n \"@smthrs/engine-store/DeferredClockFold/FoldIncomplete\",\n {\n code: Schema.Literal(\"fold_incomplete\"),\n message: Schema.String,\n unjoined: Schema.Array(Unjoined)\n }\n) {}\n\nconst pageSize = 512\n\n/** Folds one run's entries after `after` into `start`, in sequence order. */\nconst foldEntries = (\n journal: Journal.Service,\n runId: string,\n start: State,\n after: JournalEvent.Seq | undefined,\n upTo?: number | undefined\n) =>\n Effect.gen(function*() {\n let state = start\n let cursor = after\n while (true) {\n const page = yield* journal.entries({\n runId: runId as JournalEvent.RunId,\n eventTypes,\n limit: pageSize,\n ...(cursor === undefined ? {} : { after: cursor })\n })\n for (const entry of page.entries) {\n if (upTo !== undefined && entry.seq > upTo) return state\n state = reduce(state, entry)\n cursor = entry.seq\n }\n if (!page.hasMore) return state\n }\n })\n\n/** The run's compaction floor checkpoint, if compaction ever ran. */\nconst floorOf = (sql: SqlClient.SqlClient, runId: string) =>\n sql<{ readonly seq: number | string; readonly state_json: string }>`\n SELECT seq, state_json FROM flows_journal_checkpoints\n WHERE run_id = ${runId} AND compacted_at_ms IS NOT NULL\n ORDER BY seq DESC LIMIT 1\n `.pipe(Effect.map((rows) => rows[0]))\n\n/**\n * Folds one run: its floor checkpoint's fold, if it was compacted, then every\n * entry above the floor, up to and including `upTo` when given.\n */\nconst foldRun = (runId: string, upTo?: number | undefined) =>\n Effect.gen(function*() {\n const journal = yield* Journal.Journal\n const sql = yield* SqlClient.SqlClient\n const floor = yield* floorOf(sql, runId)\n if (floor === undefined) return yield* foldEntries(journal, runId, initial, undefined, upTo)\n const kept = fromCheckpoint(JSON.parse(floor.state_json) as unknown)\n if (Option.isNone(kept)) {\n return yield* Effect.fail(\n new FoldIncomplete({\n code: \"fold_incomplete\",\n message: `run ${runId} was compacted below a checkpoint that keeps no deferred/clock fold`,\n unjoined: [{ reason: \"checkpoint_missing\", executionId: runId }]\n })\n )\n }\n return yield* foldEntries(journal, runId, kept.value, Number(floor.seq) as JournalEvent.Seq, upTo)\n })\n\n/**\n * The fold of one run's entries up to and including `upTo`, for the state of\n * a checkpoint at `upTo`. It starts from the run's floor checkpoint when the\n * run was compacted before.\n *\n * @category operations\n * @since 1.0.0\n */\nexport const capture = Effect.fn(\"DeferredClockFold.capture\")(function*(runId: string, upTo: number) {\n return encode(yield* foldRun(runId, upTo))\n})\n\n/**\n * A checkpoint state that keeps the fold: `base` (an object, or nothing) with\n * the run's fold up to `upTo` under {@link checkpointKey}. A composition that\n * compacts runs writes its checkpoints through this, so compaction cannot\n * lose the deferred/clock history.\n *\n * @category operations\n * @since 1.0.0\n */\nexport const checkpointState = Effect.fn(\"DeferredClockFold.checkpointState\")(function*(\n runId: string,\n upTo: number,\n base?: Readonly<Record<string, unknown>> | undefined\n) {\n return { ...base, [checkpointKey]: yield* capture(runId, upTo) }\n})\n\n/**\n * What a {@link rebuild} read and wrote.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Rebuilt {\n /** Runs with deferred or clock entries, or with a compaction floor. */\n readonly runs: number\n /** Clock rows written. */\n readonly clocks: number\n /** Completion rows whose completion and consumption were rewritten. */\n readonly deferreds: number\n /** Completion rows no journal entry explains, left as they are. */\n readonly unexplained: number\n}\n\ninterface StoredDeferred {\n readonly flow_name: string\n readonly execution_id: string\n readonly deferred_name: string\n readonly exit_json: string\n readonly metadata_json: string | null\n}\n\n/**\n * Rebuilds `flows_clock_deadlines` and the completion and consumption stamps\n * of `flows_deferred_completions` from the journal.\n *\n * Every run with deferred or clock entries, and every compacted run, is\n * folded in sequence order, from its floor checkpoint when it was compacted;\n * a compacted run whose floor keeps no fold fails, even one that never had a\n * clock, because the journal can no longer say. Every reference is joined\n * before anything is written: a completion with no row or with other bytes,\n * a clock whose run is gone, or a compacted run whose floor keeps no fold\n * fails {@link FoldIncomplete} listing each one, and nothing is written.\n * Otherwise, in one writer transaction, the clock table is replaced by the\n * fold's clocks and each folded completion row gets the fold's\n * `completed_at_ms` and `consumed_at_ms`. A completion row the journal does\n * not explain is left alone and counted.\n *\n * Run it on a quiescent store, as a restore does.\n *\n * @category operations\n * @since 1.0.0\n */\nexport const rebuild = Effect.fn(\"DeferredClockFold.rebuild\")(function*() {\n const sql = yield* SqlClient.SqlClient\n const writer = yield* DurableWriter\n // A compacted run may have no fold entry left above its floor, so every run\n // with a compaction floor is folded too.\n const runs = yield* sql<{ readonly run_id: string }>`\n SELECT run_id FROM flows_journal_events WHERE ${sql.in(\"event_type\", eventTypes)}\n UNION\n SELECT run_id FROM flows_journal_checkpoints WHERE compacted_at_ms IS NOT NULL\n ORDER BY run_id\n `\n const unjoined: Array<Unjoined> = []\n const clocks: Array<Clock> = []\n const deferreds: Array<Deferred> = []\n for (const { run_id } of runs) {\n const state = yield* foldRun(run_id).pipe(\n Effect.catchTag(\"@smthrs/engine-store/DeferredClockFold/FoldIncomplete\", (error) => {\n unjoined.push(...error.unjoined)\n return Effect.succeed(initial)\n })\n )\n clocks.push(...state.clocks.values())\n deferreds.push(...state.deferreds.values())\n }\n const liveRuns = new Set(\n (yield* sql<{ readonly run_id: string }>`SELECT run_id FROM flows_runs`).map((row) => row.run_id)\n )\n for (const clock of clocks) {\n if (!liveRuns.has(clock.executionId)) {\n unjoined.push({\n reason: \"run_missing\",\n executionId: clock.executionId,\n flowName: clock.flowName,\n name: clock.clockName\n })\n }\n }\n const stored = new Map(\n (yield* sql<StoredDeferred>`\n SELECT flow_name, execution_id, deferred_name, exit_json, metadata_json\n FROM flows_deferred_completions\n `).map((row) => [\n deferredKey({ flowName: row.flow_name, executionId: row.execution_id, deferredName: row.deferred_name }),\n row\n ])\n )\n for (const deferred of deferreds) {\n const row = stored.get(deferredKey(deferred))\n const reference = { executionId: deferred.executionId, flowName: deferred.flowName, name: deferred.deferredName }\n if (row === undefined) {\n unjoined.push({ reason: \"row_missing\", ...reference })\n } else if (\n (deferred.exitDigest !== undefined && JournalEvent.contentDigest(row.exit_json) !== deferred.exitDigest) ||\n (deferred.metadataDigest !== undefined &&\n (row.metadata_json === null ? null : JournalEvent.contentDigest(row.metadata_json)) !==\n deferred.metadataDigest)\n ) {\n unjoined.push({ reason: \"digest_mismatch\", ...reference })\n }\n }\n if (unjoined.length > 0) {\n return yield* Effect.fail(\n new FoldIncomplete({\n code: \"fold_incomplete\",\n message: `the journal references ${unjoined.length} deferred/clock rows the store cannot join`,\n unjoined\n })\n )\n }\n yield* writer.write(Effect.gen(function*() {\n yield* sql`DELETE FROM flows_clock_deadlines`\n for (const clock of clocks) {\n yield* sql`\n INSERT INTO flows_clock_deadlines (\n flow_name, execution_id, clock_name, deferred_name, due_at_ms, completed_at_ms\n ) VALUES (\n ${clock.flowName}, ${clock.executionId}, ${clock.clockName}, ${clock.deferredName},\n ${clock.dueAtMs}, ${clock.completedAtMs}\n )\n `\n }\n for (const deferred of deferreds) {\n yield* sql`\n UPDATE flows_deferred_completions\n SET completed_at_ms = COALESCE(${deferred.completedAtMs}, completed_at_ms),\n consumed_at_ms = ${deferred.consumedAtMs}\n WHERE flow_name = ${deferred.flowName}\n AND execution_id = ${deferred.executionId}\n AND deferred_name = ${deferred.deferredName}\n `\n }\n }))\n return {\n runs: runs.length,\n clocks: clocks.length,\n deferreds: deferreds.length,\n unexplained: stored.size - deferreds.length\n } satisfies Rebuilt\n})\n"],
4
+ "sourcesContent": ["/**\n * The deferred/clock fold: `flows_clock_deadlines` and the mutable half of\n * `flows_deferred_completions` rebuilt from the journal.\n *\n * Every change to either table is journalled on the run's own stream in the\n * transaction that makes it:\n *\n * - `flows.engine.clock-scheduled` when a clock row is inserted;\n * - `flows.engine.clock-completed` when a clock fires or its run settles\n * first (`completeClock`, `completeRunClocks`);\n * - `flows.engine.deferred-completed` when the first completion wins, naming\n * the row and the {@link JournalEvent.contentDigest} of its encoded `exit`\n * and `metadata`;\n * - `flows.engine.deferred-consumed` when a run first observes a result.\n *\n * The journal is redacted and replayed to sync subscribers, so it never\n * carries a completion's value (issue #72). A clock row holds no value at\n * all, so the fold rebuilds it whole. A completion row's value stays in the\n * row: the fold rebuilds its `completed_at_ms` and `consumed_at_ms`, and a\n * rebuild checks the stored text against the recorded digests. A completion\n * the journal names but no row holds, or holds with other bytes, is a typed\n * {@link FoldIncomplete}, never a silent gap.\n *\n * Compaction deletes a run's entries below its floor, so the fold's state at\n * the floor has to survive in the floor's checkpoint: {@link checkpointState}\n * adds it under {@link checkpointKey}, and a rebuild of a compacted run whose\n * floor checkpoint lacks it fails {@link FoldIncomplete} with reason\n * `checkpoint_missing`. A rewind archives the suffix and removes the rows it\n * explains (`@smthrs/time-travel`), so the remaining journal still explains\n * the remaining rows.\n *\n * @since 1.0.0\n */\n\nimport { DurableWriter } from \"@smthrs/database/DurableWriter\"\nimport { Journal, JournalEvent, Projection } from \"@smthrs/journal\"\nimport * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/sql/SqlClient\"\nimport { EventTypes } from \"./EventTypes.ts\"\n\n/**\n * The journal event types the fold reads.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const eventTypes: ReadonlyArray<string> = [\n EventTypes.clockScheduled,\n EventTypes.clockCompleted,\n EventTypes.deferredCompleted,\n EventTypes.deferredConsumed\n]\n\n/**\n * The key a checkpoint's state keeps the fold under.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const checkpointKey = \"flows.engine.deferred-clock\"\n\nconst NonNegativeSafeInt = Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(0),\n Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER)\n)\nconst Digest = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/))\n\n/**\n * A clock as the journal records it.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Clock = Schema.Struct({\n flowName: Schema.NonEmptyString,\n executionId: Schema.NonEmptyString,\n clockName: Schema.NonEmptyString,\n deferredName: Schema.NonEmptyString,\n dueAtMs: NonNegativeSafeInt,\n completedAtMs: Schema.NullOr(NonNegativeSafeInt)\n})\n\n/**\n * A clock as the journal records it.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Clock = typeof Clock.Type\n\n/**\n * A deferred completion as the journal records it. A completion journalled\n * before the fold existed carried its (redacted) value instead of digests and\n * no completion time: `exitDigest` and `metadataDigest` are then absent and\n * `completedAtMs` is `null`, and a rebuild keeps the row's own stamp.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Deferred = Schema.Struct({\n flowName: Schema.NonEmptyString,\n executionId: Schema.NonEmptyString,\n deferredName: Schema.NonEmptyString,\n completedAtMs: Schema.NullOr(NonNegativeSafeInt),\n exitDigest: Schema.optionalKey(Digest),\n metadataDigest: Schema.optionalKey(Schema.NullOr(Digest)),\n consumedAtMs: Schema.NullOr(NonNegativeSafeInt)\n})\n\n/**\n * A deferred completion as the journal records it.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Deferred = typeof Deferred.Type\n\n/**\n * The folded clocks and completions, keyed by their row address.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface State {\n readonly clocks: ReadonlyMap<string, Clock>\n readonly deferreds: ReadonlyMap<string, Deferred>\n}\n\n/**\n * The empty fold.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const initial: State = { clocks: new Map(), deferreds: new Map() }\n\n/**\n * The key of a clock row.\n *\n * @category models\n * @since 1.0.0\n */\nexport const clockKey = (\n address: { readonly flowName: string; readonly executionId: string; readonly clockName: string }\n) => JSON.stringify([address.flowName, address.executionId, address.clockName])\n\n/**\n * The key of a completion row.\n *\n * @category models\n * @since 1.0.0\n */\nexport const deferredKey = (\n address: { readonly flowName: string; readonly executionId: string; readonly deferredName: string }\n) => JSON.stringify([address.flowName, address.executionId, address.deferredName])\n\nconst record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? value as Readonly<Record<string, unknown>>\n : undefined\n\nconst text = (value: unknown): value is string => typeof value === \"string\" && value.length > 0\nconst time = (value: unknown): value is number => typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0\nconst digest = (value: unknown): value is string => typeof value === \"string\" && /^[0-9a-f]{64}$/.test(value)\n\n/**\n * Folds one committed journal entry. First writes win: a clock keeps its first\n * schedule and first completion, and a completion its first recording and\n * first consumption, exactly as the rows do. An entry of another type, or one\n * whose payload names no row, leaves the state unchanged.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const reduce = (state: State, entry: JournalEvent.Entry): State => {\n const payload = record(entry.payload)\n if (payload === undefined || !text(payload.flowName) || !text(payload.executionId)) return state\n const flowName = payload.flowName\n const executionId = payload.executionId\n switch (entry.eventType) {\n case EventTypes.clockScheduled: {\n if (!text(payload.clockName) || !text(payload.deferredName) || !time(payload.dueAtMs)) return state\n const key = clockKey({ flowName, executionId, clockName: payload.clockName })\n if (state.clocks.has(key)) return state\n const clocks = new Map(state.clocks)\n clocks.set(key, {\n flowName,\n executionId,\n clockName: payload.clockName,\n deferredName: payload.deferredName,\n dueAtMs: payload.dueAtMs,\n completedAtMs: null\n })\n return { ...state, clocks }\n }\n case EventTypes.clockCompleted: {\n if (!text(payload.clockName) || !time(payload.completedAtMs)) return state\n const key = clockKey({ flowName, executionId, clockName: payload.clockName })\n const clock = state.clocks.get(key)\n if (clock === undefined || clock.completedAtMs !== null) return state\n const clocks = new Map(state.clocks)\n clocks.set(key, { ...clock, completedAtMs: payload.completedAtMs })\n return { ...state, clocks }\n }\n case EventTypes.deferredCompleted: {\n if (!text(payload.deferredName)) return state\n const key = deferredKey({ flowName, executionId, deferredName: payload.deferredName })\n if (state.deferreds.has(key)) return state\n const deferreds = new Map(state.deferreds)\n deferreds.set(key, {\n flowName,\n executionId,\n deferredName: payload.deferredName,\n completedAtMs: time(payload.completedAtMs) ? payload.completedAtMs : null,\n ...(digest(payload.exitDigest) ? { exitDigest: payload.exitDigest } : {}),\n ...(digest(payload.exitDigest) && (payload.metadataDigest === null || digest(payload.metadataDigest))\n ? { metadataDigest: payload.metadataDigest }\n : {}),\n consumedAtMs: null\n })\n return { ...state, deferreds }\n }\n case EventTypes.deferredConsumed: {\n if (!text(payload.deferredName) || !time(payload.consumedAtMs)) return state\n const key = deferredKey({ flowName, executionId, deferredName: payload.deferredName })\n const deferred = state.deferreds.get(key)\n if (deferred === undefined || deferred.consumedAtMs !== null) return state\n const deferreds = new Map(state.deferreds)\n deferreds.set(key, { ...deferred, consumedAtMs: payload.consumedAtMs })\n return { ...state, deferreds }\n }\n default:\n return state\n }\n}\n\n/**\n * The fold as a journal projection, for `Journal.project` over one run.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const projection: Projection.Projection<State> = Projection.make({\n name: \"flows.engine.deferred-clock\",\n initial,\n reduce: (state, entry) => Effect.succeed(reduce(state, entry))\n})\n\n/**\n * The checkpoint encoding of the fold.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Encoded = Schema.Struct({\n clocks: Schema.Array(Clock),\n deferreds: Schema.Array(Deferred)\n})\n\n/**\n * The checkpoint encoding of the fold.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Encoded = typeof Encoded.Type\n\n/**\n * Encodes the fold for a checkpoint.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const encode = (state: State): Encoded => ({\n clocks: [...state.clocks.values()],\n deferreds: [...state.deferreds.values()]\n})\n\n/**\n * Decodes the fold a checkpoint kept, or `None` when the checkpoint state\n * holds none under {@link checkpointKey}.\n *\n * @category folds\n * @since 1.0.0\n */\nexport const fromCheckpoint = (checkpointState: unknown): Option.Option<State> => {\n const kept = record(checkpointState)?.[checkpointKey]\n if (kept === undefined) return Option.none()\n const decoded = Schema.decodeUnknownOption(Encoded)(kept)\n return Option.map(decoded, (encoded) => ({\n clocks: new Map(encoded.clocks.map((clock) => [clockKey(clock), clock])),\n deferreds: new Map(encoded.deferreds.map((deferred) => [deferredKey(deferred), deferred]))\n }))\n}\n\n/**\n * Why a rebuild could not reconstruct the tables.\n *\n * - `row_missing`: the journal names a completion no row holds.\n * - `digest_mismatch`: the row holds other bytes than the journal recorded.\n * - `run_missing`: a clock's run has no `flows_runs` row to hang it on.\n * - `checkpoint_missing`: a compacted run's floor checkpoint keeps no fold.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const FoldIncompleteReason = Schema.Literals([\n \"row_missing\",\n \"digest_mismatch\",\n \"run_missing\",\n \"checkpoint_missing\"\n])\n\n/**\n * One reference a rebuild could not join.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const Unjoined = Schema.Struct({\n reason: FoldIncompleteReason,\n executionId: Schema.String,\n flowName: Schema.optionalKey(Schema.String),\n name: Schema.optionalKey(Schema.String)\n})\n\n/**\n * One reference a rebuild could not join.\n *\n * @category errors\n * @since 1.0.0\n */\nexport type Unjoined = typeof Unjoined.Type\n\n/**\n * The journal references state the store no longer holds. The rebuild wrote\n * nothing.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class FoldIncomplete extends Schema.TaggedError<FoldIncomplete>()(\n \"@smthrs/engine-store/DeferredClockFold/FoldIncomplete\",\n {\n code: Schema.Literal(\"fold_incomplete\"),\n message: Schema.String,\n unjoined: Schema.Array(Unjoined)\n }\n) {}\n\nconst pageSize = 512\n\n/** Folds one run's entries after `after` into `start`, in sequence order. */\nconst foldEntries = (\n journal: Journal.Service,\n runId: string,\n start: State,\n after: JournalEvent.Seq | undefined,\n upTo?: number | undefined\n) =>\n Effect.gen(function*() {\n let state = start\n let cursor = after\n while (true) {\n const page = yield* journal.entries({\n runId: runId as JournalEvent.RunId,\n eventTypes,\n limit: pageSize,\n ...(cursor === undefined ? {} : { after: cursor })\n })\n for (const entry of page.entries) {\n if (upTo !== undefined && entry.seq > upTo) return state\n state = reduce(state, entry)\n cursor = entry.seq\n }\n if (!page.hasMore) return state\n }\n })\n\n/** The run's compaction floor checkpoint, if compaction ever ran. */\nconst floorOf = (sql: SqlClient.SqlClient, runId: string) =>\n sql<{ readonly seq: number | string; readonly state_json: string }>`\n SELECT seq, state_json FROM flows_journal_checkpoints\n WHERE run_id = ${runId} AND compacted_at_ms IS NOT NULL\n ORDER BY seq DESC LIMIT 1\n `.pipe(Effect.map((rows) => rows[0]))\n\n/**\n * Folds one run: its floor checkpoint's fold, if it was compacted, then every\n * entry above the floor, up to and including `upTo` when given.\n */\nconst foldRun = (runId: string, upTo?: number | undefined) =>\n Effect.gen(function*() {\n const journal = yield* Journal.Journal\n const sql = yield* SqlClient.SqlClient\n const floor = yield* floorOf(sql, runId)\n if (floor === undefined) return yield* foldEntries(journal, runId, initial, undefined, upTo)\n const kept = fromCheckpoint(JSON.parse(floor.state_json) as unknown)\n if (Option.isNone(kept)) {\n return yield* Effect.fail(\n new FoldIncomplete({\n code: \"fold_incomplete\",\n message: `run ${runId} was compacted below a checkpoint that keeps no deferred/clock fold`,\n unjoined: [{ reason: \"checkpoint_missing\", executionId: runId }]\n })\n )\n }\n return yield* foldEntries(journal, runId, kept.value, Number(floor.seq) as JournalEvent.Seq, upTo)\n })\n\n/**\n * The fold of one run's entries up to and including `upTo`, for the state of\n * a checkpoint at `upTo`. It starts from the run's floor checkpoint when the\n * run was compacted before.\n *\n * @category operations\n * @since 1.0.0\n */\nexport const capture = Effect.fn(\"DeferredClockFold.capture\")(function*(runId: string, upTo: number) {\n return encode(yield* foldRun(runId, upTo))\n})\n\n/**\n * A checkpoint state that keeps the fold: `base` (an object, or nothing) with\n * the run's fold up to `upTo` under {@link checkpointKey}. A composition that\n * compacts runs writes its checkpoints through this, so compaction cannot\n * lose the deferred/clock history.\n *\n * @category operations\n * @since 1.0.0\n */\nexport const checkpointState = Effect.fn(\"DeferredClockFold.checkpointState\")(function*(\n runId: string,\n upTo: number,\n base?: Readonly<Record<string, unknown>> | undefined\n) {\n return { ...base, [checkpointKey]: yield* capture(runId, upTo) }\n})\n\n/**\n * What a {@link rebuild} read and wrote.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Rebuilt {\n /** Runs with deferred or clock entries, or with a compaction floor. */\n readonly runs: number\n /** Clock rows written. */\n readonly clocks: number\n /** Completion rows whose completion and consumption were rewritten. */\n readonly deferreds: number\n /** Completion rows no journal entry explains, left as they are. */\n readonly unexplained: number\n}\n\ninterface StoredDeferred {\n readonly flow_name: string\n readonly execution_id: string\n readonly deferred_name: string\n readonly exit_json: string\n readonly metadata_json: string | null\n}\n\n/**\n * Rebuilds `flows_clock_deadlines` and the completion and consumption stamps\n * of `flows_deferred_completions` from the journal.\n *\n * Every run with deferred or clock entries, and every compacted run, is\n * folded in sequence order, from its floor checkpoint when it was compacted;\n * a compacted run whose floor keeps no fold fails, even one that never had a\n * clock, because the journal can no longer say. Every reference is joined\n * before anything is written: a completion with no row or with other bytes,\n * a clock whose run is gone, or a compacted run whose floor keeps no fold\n * fails {@link FoldIncomplete} listing each one, and nothing is written.\n * Otherwise, in one writer transaction, the clock table is replaced by the\n * fold's clocks and each folded completion row gets the fold's\n * `completed_at_ms` and `consumed_at_ms`. A completion row the journal does\n * not explain is left alone and counted.\n *\n * Run it on a quiescent store, as a restore does.\n *\n * @category operations\n * @since 1.0.0\n */\nexport const rebuild = Effect.fn(\"DeferredClockFold.rebuild\")(function*() {\n const sql = yield* SqlClient.SqlClient\n const writer = yield* DurableWriter\n // A compacted run may have no fold entry left above its floor, so every run\n // with a compaction floor is folded too.\n const runs = yield* sql<{ readonly run_id: string }>`\n SELECT run_id FROM flows_journal_events WHERE ${sql.in(\"event_type\", eventTypes)}\n UNION\n SELECT run_id FROM flows_journal_checkpoints WHERE compacted_at_ms IS NOT NULL\n ORDER BY run_id\n `\n const unjoined: Array<Unjoined> = []\n const clocks: Array<Clock> = []\n const deferreds: Array<Deferred> = []\n for (const { run_id } of runs) {\n const state = yield* foldRun(run_id).pipe(\n Effect.catchTag(\"@smthrs/engine-store/DeferredClockFold/FoldIncomplete\", (error) => {\n unjoined.push(...error.unjoined)\n return Effect.succeed(initial)\n })\n )\n clocks.push(...state.clocks.values())\n deferreds.push(...state.deferreds.values())\n }\n const liveRuns = new Set(\n (yield* sql<{ readonly run_id: string }>`SELECT run_id FROM flows_runs`).map((row) => row.run_id)\n )\n for (const clock of clocks) {\n if (!liveRuns.has(clock.executionId)) {\n unjoined.push({\n reason: \"run_missing\",\n executionId: clock.executionId,\n flowName: clock.flowName,\n name: clock.clockName\n })\n }\n }\n const stored = new Map(\n (yield* sql<StoredDeferred>`\n SELECT flow_name, execution_id, deferred_name, exit_json, metadata_json\n FROM flows_deferred_completions\n `).map((row) => [\n deferredKey({ flowName: row.flow_name, executionId: row.execution_id, deferredName: row.deferred_name }),\n row\n ])\n )\n for (const deferred of deferreds) {\n const row = stored.get(deferredKey(deferred))\n const reference = { executionId: deferred.executionId, flowName: deferred.flowName, name: deferred.deferredName }\n if (row === undefined) {\n unjoined.push({ reason: \"row_missing\", ...reference })\n } else if (\n (deferred.exitDigest !== undefined && JournalEvent.contentDigest(row.exit_json) !== deferred.exitDigest) ||\n (deferred.metadataDigest !== undefined &&\n (row.metadata_json === null ? null : JournalEvent.contentDigest(row.metadata_json)) !==\n deferred.metadataDigest)\n ) {\n unjoined.push({ reason: \"digest_mismatch\", ...reference })\n }\n }\n if (unjoined.length > 0) {\n return yield* Effect.fail(\n new FoldIncomplete({\n code: \"fold_incomplete\",\n message: `the journal references ${unjoined.length} deferred/clock rows the store cannot join`,\n unjoined\n })\n )\n }\n yield* writer.write(Effect.gen(function*() {\n yield* sql`DELETE FROM flows_clock_deadlines`\n for (const clock of clocks) {\n yield* sql`\n INSERT INTO flows_clock_deadlines (\n flow_name, execution_id, clock_name, deferred_name, due_at_ms, completed_at_ms\n ) VALUES (\n ${clock.flowName}, ${clock.executionId}, ${clock.clockName}, ${clock.deferredName},\n ${clock.dueAtMs}, ${clock.completedAtMs}\n )\n `\n }\n for (const deferred of deferreds) {\n yield* sql`\n UPDATE flows_deferred_completions\n SET completed_at_ms = COALESCE(${deferred.completedAtMs}, completed_at_ms),\n consumed_at_ms = ${deferred.consumedAtMs}\n WHERE flow_name = ${deferred.flowName}\n AND execution_id = ${deferred.executionId}\n AND deferred_name = ${deferred.deferredName}\n `\n }\n }))\n return {\n runs: runs.length,\n clocks: clocks.length,\n deferreds: deferreds.length,\n unexplained: stored.size - deferreds.length\n } satisfies Rebuilt\n})\n"],
5
5
  "mappings": "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",
6
6
  "names": ["checkpointState"]
7
7
  }
@@ -42,7 +42,7 @@ import * as Effect from "effect/Effect";
42
42
  import * as FileSystem from "effect/FileSystem";
43
43
  import type * as Layer from "effect/Layer";
44
44
  import * as Schema from "effect/Schema";
45
- import * as SqlClient from "effect/unstable/sql/SqlClient";
45
+ import * as SqlClient from "effect/sql/SqlClient";
46
46
  /**
47
47
  * The database file name inside a backup directory and a restored store
48
48
  * directory.
@@ -1 +1 @@
1
- {"version":3,"file":"DisasterRecovery.d.ts","sourceRoot":"","sources":["../../src/DisasterRecovery.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DisasterRecovery.d.ts","sourceRoot":"","sources":["../../src/DisasterRecovery.ts"],"names":[],"mappings":"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"}
@@ -52,7 +52,7 @@ var Clock = __toESM(require("effect/Clock"), 1);
52
52
  var Effect = __toESM(require("effect/Effect"), 1);
53
53
  var FileSystem = __toESM(require("effect/FileSystem"), 1);
54
54
  var Schema = __toESM(require("effect/Schema"), 1);
55
- var SqlClient = __toESM(require("effect/unstable/sql/SqlClient"), 1);
55
+ var SqlClient = __toESM(require("effect/sql/SqlClient"), 1);
56
56
  var ArtifactRoots = __toESM(require("./internal/ArtifactRoots.js"), 1);
57
57
  const databaseFileName = "store.sqlite3";
58
58
  const manifestFileName = "manifest.json";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/DisasterRecovery.ts"],
4
- "sourcesContent": ["/**\n * Hot backup, verified restore, and restore-time fencing for the durable\n * flows store.\n *\n * A backup captures the whole SQLite database \u2014 journal, run store, step\n * cache, engine state, and the out-of-ladder objects created by\n * `internal/EngineStateSchema.ts` \u2014 with `VACUUM INTO`, which reads one\n * consistent snapshot under WAL while writers stay live. The content-addressed\n * artifact blobs are copied beside it, digest-verified, after the database\n * snapshot: results reference artifacts only after publication\n * (`packages/smithers/flows/step-cache/docs/api.md`), so a blob walk taken after the\n * database snapshot is a superset of every digest the snapshot references.\n * A manifest written last records the digests a restore must verify, so a\n * partial backup is detectable by its missing manifest.\n *\n * Restore never lets a pre-backup owner resurrect. The fencing token is the\n * `OwnerId` triple mirrored on `flows_runs` and arbitrated by the journal's\n * `flows_consensus_leases` \u2014 Temporal's shard `rangeID` equality check reduced\n * to one SQL predicate (`reference/temporal`\n * `service/history/shard/context_impl.go`, `renewRangeLocked`). Temporal\n * fences a restored shard by bumping the persisted epoch above anything a\n * surviving owner could hold; the equivalent bump for an identity fence is\n * {@link fence}: clear every persisted claim, owner, and consensus lease in\n * one serialized write transaction, so every pre-backup `OwnerId` fails its\n * equality compare-and-swap (`FenceLost`, journal `fence_lost`) and the\n * restored runs are claimable immediately instead of after the heartbeat\n * staleness cutoff.\n *\n * Host access arrives through Effect's `FileSystem` and `SqlClient` tags and\n * hashing through the injected `Crypto` service, so the module carries no\n * platform binding. The SQL dialect is SQLite \u2014 a non-SQLite backend fails\n * with the `sql` code rather than pretending to snapshot. Every file is\n * stat-checked before it is buffered for hashing or copying. The configurable\n * ceiling defaults to {@link defaultMaxFileSizeBytes}; an oversized file fails\n * with the typed `io` code instead of reaching the host's allocation limit.\n *\n * @since 0.1.0\n */\n\nimport * as ArtifactBackupLease from \"@smthrs/artifacts/ArtifactBackupLease\"\nimport { Sha256 } from \"@smthrs/crypto\"\nimport { DurableWriter } from \"@smthrs/database/DurableWriter\"\nimport * as Clock from \"effect/Clock\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Effect from \"effect/Effect\"\nimport * as FileSystem from \"effect/FileSystem\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\nimport * as ArtifactRoots from \"./internal/ArtifactRoots.ts\"\n\n/**\n * The database file name inside a backup directory and a restored store\n * directory.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const databaseFileName = \"store.sqlite3\"\n\n/**\n * The manifest file name inside a backup directory. It is written last, so\n * its presence marks a complete backup.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const manifestFileName = \"manifest.json\"\n\n/**\n * The artifact blob directory name inside a backup directory and a restored\n * store directory, laid out with the store's own two-hex-prefix fanout.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const objectsDirectoryName = \"objects\"\n\n/**\n * The marker file a restore writes into the target directory, recording when\n * the restore happened and which backup it came from.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const restoredMarkerFileName = \"restored.json\"\n\n/**\n * Default ceiling for one backup file read into memory: 512 MiB.\n *\n * @since 1.0.0\n * @category constants\n */\nexport const defaultMaxFileSizeBytes = 512 * 1024 * 1024\n\n/**\n * One applied row of the shared `flows_migrations` table, recorded at backup\n * time and re-verified at fence time as the schema version of the snapshot.\n *\n * @since 0.1.0\n * @category schemas\n */\nexport const AppliedMigration = Schema.Struct({\n migrationId: Schema.Number,\n name: Schema.String\n})\n\n/**\n * One applied row of the shared `flows_migrations` table.\n *\n * @since 0.1.0\n * @category models\n */\nexport type AppliedMigration = typeof AppliedMigration.Type\n\n/**\n * One captured artifact blob. `sizeBytes` is informational for operators; the\n * digest is the integrity check.\n *\n * @since 0.1.0\n * @category schemas\n */\nexport const ArtifactEntry = Schema.Struct({\n digest: Sha256.Digest,\n sizeBytes: Schema.Number\n})\n\n/**\n * One captured artifact blob.\n *\n * @since 0.1.0\n * @category models\n */\nexport type ArtifactEntry = typeof ArtifactEntry.Type\n\n/**\n * The verification contract of a backup: the database snapshot's digest and\n * schema version, and the digest of every captured artifact blob.\n *\n * @since 0.1.0\n * @category schemas\n */\nexport const BackupManifest = Schema.Struct({\n formatVersion: Schema.Literal(1),\n createdAtMs: Schema.Number,\n database: Schema.Struct({\n file: Schema.Literal(databaseFileName),\n sha256: Sha256.Digest,\n sizeBytes: Schema.Number,\n migrations: Schema.Array(AppliedMigration)\n }),\n artifacts: Schema.Array(ArtifactEntry)\n})\n\n/**\n * The verification contract of a backup.\n *\n * @since 0.1.0\n * @category models\n */\nexport type BackupManifest = typeof BackupManifest.Type\n\n/** JSON text carrying a decoded backup manifest. */\nconst ManifestFromJsonString = Schema.fromJsonString(BackupManifest)\n\n/**\n * Stable failure codes surfaced by backup, verify, restore, and fence.\n * `invalid_options` reports a public option that cannot be admitted safely.\n *\n * @since 0.1.0\n * @category errors\n */\nexport const DisasterRecoveryErrorCode = Schema.Literals([\n \"invalid_options\",\n \"not_empty\",\n \"invalid_manifest\",\n \"missing_file\",\n \"digest_mismatch\",\n \"artifact_corruption\",\n \"snapshot_incomplete\",\n \"schema_mismatch\",\n \"io\",\n \"sql\"\n])\n\n/**\n * A stable disaster-recovery failure code.\n *\n * @since 0.1.0\n * @category errors\n */\nexport type DisasterRecoveryErrorCode = typeof DisasterRecoveryErrorCode.Type\n\n/**\n * A normalized backup or restore failure. The identity string equals this\n * module's package path, per the house rule that a new identity is the\n * defining module path.\n *\n * @since 0.1.0\n * @category errors\n */\nexport class DisasterRecoveryError extends Schema.TaggedError<DisasterRecoveryError>()(\n \"@smthrs/engine-store/DisasterRecoveryError\",\n {\n code: DisasterRecoveryErrorCode,\n method: Schema.String,\n message: Schema.String,\n cause: Schema.Unknown\n }\n) {}\n\n/**\n * Shared buffering limit for backup, verification, and restore operations.\n *\n * @since 1.0.0\n * @category models\n */\nexport interface FileSizeOptions {\n /**\n * Largest file, in bytes, that backup, verification, or restore may buffer.\n * The value must be a finite non-negative safe integer. Defaults to\n * {@link defaultMaxFileSizeBytes}.\n */\n readonly maxFileSizeBytes?: number | undefined\n}\n\n/**\n * Where a backup is written and which artifact store it captures.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface BackupOptions<R = never, E = never> extends FileSizeOptions {\n /** The directory the backup is written into. Created when absent; must be empty when present. */\n readonly directory: string\n /**\n * The live store's content-addressed objects directory (the\n * `ArtifactStore.layerFileSystem` directory). When set, the whole capture\n * holds the cross-process artifact backup lease, so a concurrent\n * `ArtifactGc.gc` sweep cannot delete a blob between the database snapshot\n * and its copy. Absent means the composition has no filesystem artifact\n * tier: the backup carries no blobs and takes no lease, because there is\n * no objects directory to fence, and a frozen snapshot that references any\n * artifact digest fails with `snapshot_incomplete` instead of reporting\n * success. A filesystem-backed composition must therefore pass the same\n * directory it gives `ArtifactStore.layerFileSystem`.\n */\n readonly objectsDirectory?: string | undefined\n /**\n * Opens the frozen `VACUUM INTO` file so backup can enumerate artifact\n * roots and migrations from that exact snapshot, independently of the live\n * connection. The host should open this file without applying migrations.\n */\n readonly snapshotDatabaseLayer: (databaseFile: string) => Layer.Layer<SqlClient.SqlClient, E, R>\n}\n\n/**\n * Which backup to restore and where the restored store lands.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface RestoreOptions extends FileSizeOptions {\n /** The directory a previous {@link backup} wrote. */\n readonly backupDirectory: string\n /** The fresh directory the restored store is written into. Created when absent; must be empty when present. */\n readonly targetDirectory: string\n}\n\n/**\n * Which backup to restore and how to open the freshly restored SQLite file\n * for restore-time fencing.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface RestoreAndFenceOptions<R = never, E = never> extends RestoreOptions {\n /**\n * Builds the database and durable-writer layer for the newly restored file.\n * This keeps database opening as the caller's host composition rather than\n * importing a Node-only driver into this browser-safe module.\n */\n readonly databaseLayer: (databaseFile: string) => Layer.Layer<DurableWriter | SqlClient.SqlClient, E, R>\n}\n\n/**\n * The restored store's on-disk locations, ready to compose database and\n * artifact layers over.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface RestoredStore {\n readonly databaseFile: string\n readonly objectsDirectory: string\n readonly manifest: BackupManifest\n}\n\n/**\n * A restored store after restore-time fencing has invalidated pre-backup\n * owners.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface FencedRestoredStore extends RestoredStore {\n readonly fence: FenceSummary\n}\n\n/**\n * What {@link fence} invalidated on the restored store.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface FenceSummary {\n /** Runs whose pending claim columns were cleared. */\n readonly clearedClaims: number\n /** Runs that were `running` at backup time, now suspended with their owner fence cleared. */\n readonly suspendedRuns: number\n}\n\nconst error = (\n method: string,\n code: DisasterRecoveryErrorCode,\n message: string,\n cause: unknown\n): DisasterRecoveryError =>\n new DisasterRecoveryError({\n code,\n method,\n message: `${code}: DisasterRecovery.${method}: ${message}`,\n cause\n })\n\n/**\n * One mapper per failure family, shared across every call site so the\n * vocabulary stays uniform.\n */\nconst ioFailure = (method: string, subject: string) => (cause: unknown): DisasterRecoveryError =>\n error(method, \"io\", `the host filesystem refused ${subject}`, cause)\n\nconst sqlFailure = (method: string, subject: string) => (cause: unknown): DisasterRecoveryError =>\n error(method, \"sql\", `the database refused ${subject}`, cause)\n\n/** Hashes bytes with the injected crypto service; a broken host is a defect. */\nconst measure = (bytes: Uint8Array): Effect.Effect<typeof Sha256.Digest.Type, never, Crypto.Crypto> =>\n Schema.decodeUnknownEffect(Sha256)(bytes).pipe(Effect.orDie)\n\n/** Resolves and validates the shared whole-file buffering ceiling. */\nconst fileSizeLimit = (\n method: string,\n options: FileSizeOptions\n): number | DisasterRecoveryError => {\n const limit = options.maxFileSizeBytes\n if (limit === undefined) return defaultMaxFileSizeBytes\n if (Number.isSafeInteger(limit) && limit >= 0) return limit\n return error(\n method,\n \"invalid_options\",\n `maxFileSizeBytes must be a finite non-negative safe integer; received ${String(limit)}`,\n limit\n )\n}\n\n/**\n * Refuses an oversized file before a whole-file read allocates its buffer.\n * Every caller admits `limit` through `fileSizeLimit`, so `BigInt` cannot\n * reject it.\n */\nconst checkFileSize = (\n fs: FileSystem.FileSystem,\n method: string,\n path: string,\n limit: number\n): Effect.Effect<void, DisasterRecoveryError> =>\n Effect.gen(function*() {\n const info = yield* fs.stat(path).pipe(Effect.mapError(ioFailure(method, `stating ${path}`)))\n if (info.size <= BigInt(limit)) return\n const message = `${path} is ${info.size} bytes, which exceeds the ${limit}-byte file limit`\n return yield* Effect.fail(error(method, \"io\", message, new RangeError(message)))\n })\n\n/**\n * Admits a target directory that does not exist yet \u2014 created recursively \u2014\n * or exists empty. Anything else is refused rather than merged over.\n */\nconst ensureEmptyDirectory = (\n fs: FileSystem.FileSystem,\n method: string,\n directory: string\n): Effect.Effect<void, DisasterRecoveryError> =>\n Effect.gen(function*() {\n const present = yield* fs.exists(directory).pipe(Effect.mapError(ioFailure(method, `probing ${directory}`)))\n if (!present) {\n return yield* fs.makeDirectory(directory, { recursive: true }).pipe(\n Effect.mapError(ioFailure(method, `creating ${directory}`))\n )\n }\n const entries = yield* fs.readDirectory(directory).pipe(\n Effect.mapError(ioFailure(method, `listing ${directory}`))\n )\n if (entries.length > 0) {\n return yield* Effect.fail(\n error(method, \"not_empty\", `${directory} already holds ${entries.length} entries`, directory)\n )\n }\n })\n\n/**\n * A relative blob path in the store's fanout layout: a two-hex directory\n * holding a 64-hex digest that begins with it. Anything else under the\n * objects directory \u2014 a scratch `.tmp-*` file, the fanout directory itself \u2014\n * is not part of the store's address space.\n */\nconst blobEntry = (entry: string): string | undefined => {\n const match = entry.match(/^([0-9a-f]{2})[/\\\\]([0-9a-f]{64})$/)\n if (match === null || !match[2]!.startsWith(match[1]!)) return undefined\n return match[2]\n}\n\nconst blobPath = (directory: string, digest: string): string => `${directory}/${digest.slice(0, 2)}/${digest}`\n\n/** The applied rows of the shared migrations table, in ladder order. */\nconst appliedMigrations = (\n sql: SqlClient.SqlClient,\n method: string\n): Effect.Effect<Array<AppliedMigration>, DisasterRecoveryError> =>\n sql<{ readonly migration_id: number; readonly name: string }>`\n SELECT migration_id, name FROM flows_migrations ORDER BY migration_id ASC\n `.withoutTransform.pipe(\n Effect.mapError(sqlFailure(method, \"reading flows_migrations\")),\n Effect.map((rows) => rows.map((row) => ({ migrationId: Number(row.migration_id), name: row.name })))\n )\n\ninterface SnapshotState {\n readonly migrations: Array<AppliedMigration>\n readonly artifactRoots: ReadonlySet<string>\n}\n\n/** Reads every artifact root from the frozen database, never the live one. */\nconst inspectSnapshot = (\n sql: SqlClient.SqlClient\n): Effect.Effect<SnapshotState, DisasterRecoveryError> =>\n Effect.gen(function*() {\n const migrations = yield* appliedMigrations(sql, \"backup\")\n const cacheRows = yield* sql<{ readonly meta_json: string }>`\n SELECT meta_json FROM flows_step_cache\n `.withoutTransform.pipe(\n Effect.mapError(sqlFailure(\"backup\", \"reading artifact roots from snapshot step cache\"))\n )\n const attemptRows = yield* sql<{\n readonly checkpoint_json: string | null\n readonly meta_json: string\n }>`\n SELECT checkpoint_json, meta_json FROM flows_attempts\n `.withoutTransform.pipe(\n Effect.mapError(sqlFailure(\"backup\", \"reading artifact roots from snapshot attempts\"))\n )\n const artifactRoots = new Set<string>()\n const decodeFailure = (cause: ArtifactRoots.ArtifactRootDecodeError): DisasterRecoveryError =>\n error(\"backup\", \"snapshot_incomplete\", cause.message, cause.cause)\n for (const row of cacheRows) {\n for (\n const digest of yield* ArtifactRoots.rootDigests(\"flows_step_cache\", row.meta_json).pipe(\n Effect.mapError(decodeFailure)\n )\n ) artifactRoots.add(digest)\n }\n for (const row of attemptRows) {\n for (\n const digest of yield* ArtifactRoots.rootDigests(\"flows_attempts\", row.meta_json).pipe(\n Effect.mapError(decodeFailure)\n )\n ) artifactRoots.add(digest)\n for (\n const digest of yield* ArtifactRoots.checkpointDigests(row.checkpoint_json).pipe(\n Effect.mapError(decodeFailure)\n )\n ) artifactRoots.add(digest)\n }\n return { migrations, artifactRoots }\n })\n\n/** Reads and decodes the manifest a backup directory must carry. */\nconst readManifest = (\n fs: FileSystem.FileSystem,\n method: string,\n backupDirectory: string,\n maxFileSizeBytes: number\n): Effect.Effect<BackupManifest, DisasterRecoveryError, Crypto.Crypto> =>\n Effect.gen(function*() {\n const path = `${backupDirectory}/${manifestFileName}`\n const present = yield* fs.exists(path).pipe(Effect.mapError(ioFailure(method, `probing ${path}`)))\n if (!present) {\n return yield* Effect.fail(\n error(method, \"missing_file\", `${path} does not exist \u2014 the backup is incomplete or not a backup`, path)\n )\n }\n yield* checkFileSize(fs, method, path, maxFileSizeBytes)\n const text = yield* fs.readFileString(path).pipe(Effect.mapError(ioFailure(method, `reading ${path}`)))\n return yield* Schema.decodeUnknownEffect(ManifestFromJsonString)(text).pipe(\n Effect.mapError((cause) =>\n error(method, \"invalid_manifest\", `${path} does not decode as a backup manifest`, cause)\n )\n )\n })\n\n/** Reads a backup file and refuses bytes that no longer hash to the manifest. */\nconst checkedFile = (\n fs: FileSystem.FileSystem,\n method: string,\n path: string,\n expected: string,\n maxFileSizeBytes: number\n): Effect.Effect<Uint8Array, DisasterRecoveryError, Crypto.Crypto> =>\n Effect.gen(function*() {\n const present = yield* fs.exists(path).pipe(Effect.mapError(ioFailure(method, `probing ${path}`)))\n if (!present) {\n return yield* Effect.fail(error(method, \"missing_file\", `${path} does not exist`, path))\n }\n yield* checkFileSize(fs, method, path, maxFileSizeBytes)\n const bytes = yield* fs.readFile(path).pipe(Effect.mapError(ioFailure(method, `reading ${path}`)))\n const measured = yield* measure(bytes)\n if (measured !== expected) {\n return yield* Effect.fail(\n error(method, \"digest_mismatch\", `${path} hashes to ${measured}, the manifest records ${expected}`, path)\n )\n }\n return bytes\n })\n\n/**\n * Captures a hot backup of the live store into an empty directory.\n *\n * The database is snapshotted first with `VACUUM INTO` \u2014 a read transaction\n * under WAL, so live writers are never blocked and the copy is one consistent\n * point in time. The artifact walk runs after it, so every digest the\n * snapshot references is captured (publication precedes reference). Blobs are\n * digest-verified as they are copied; a blob whose bytes no longer hash to\n * its address fails the backup loudly rather than capturing corruption. The\n * manifest is encoded and admitted under the same file-size ceiling before it\n * is written last, so a successful backup remains readable with its own\n * options.\n *\n * With an `objectsDirectory`, the whole capture holds the artifact backup\n * lease, so concurrent sweep deletion cannot remove a referenced blob\n * mid-copy. Without one, no lease is taken; the frozen-root verification\n * still fails the backup loudly when the snapshot references any blob, so an\n * unfenced capture can never report success while missing artifact bytes.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const backup = <R = never, E = never>(\n options: BackupOptions<R, E>\n): Effect.Effect<\n BackupManifest,\n DisasterRecoveryError | E,\n SqlClient.SqlClient | FileSystem.FileSystem | Crypto.Crypto | R\n> =>\n Effect.gen(function*() {\n const limit = fileSizeLimit(\"backup\", options)\n if (typeof limit !== \"number\") return yield* Effect.fail(limit)\n const maxFileSizeBytes = limit\n const sql = yield* Effect.service(SqlClient.SqlClient)\n const fs = yield* FileSystem.FileSystem\n const createdAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))\n yield* ensureEmptyDirectory(fs, \"backup\", options.directory)\n\n const capture = Effect.gen(function*() {\n const databasePath = `${options.directory}/${databaseFileName}`\n yield* sql`VACUUM INTO ${databasePath}`.pipe(\n Effect.mapError(sqlFailure(\"backup\", `snapshotting the database into ${databasePath}`))\n )\n const snapshot = yield* Effect.flatMap(\n Effect.service(SqlClient.SqlClient),\n inspectSnapshot\n ).pipe(Effect.provide(options.snapshotDatabaseLayer(databasePath)))\n // Opening the frozen file may normalize SQLite journal metadata, so its\n // manifest digest is measured only after the inspection connection closes.\n yield* checkFileSize(fs, \"backup\", databasePath, maxFileSizeBytes)\n const databaseBytes = yield* fs.readFile(databasePath).pipe(\n Effect.mapError(ioFailure(\"backup\", `reading ${databasePath} back`))\n )\n const sha256 = yield* measure(databaseBytes)\n\n const artifacts: Array<ArtifactEntry> = []\n const captured = new Set<string>()\n if (options.objectsDirectory !== undefined) {\n const objectsDirectory = options.objectsDirectory\n const entries = yield* fs.readDirectory(objectsDirectory, { recursive: true }).pipe(\n Effect.mapError(ioFailure(\"backup\", `listing ${objectsDirectory}`))\n )\n for (const entry of [...entries].sort()) {\n const digest = blobEntry(entry)\n if (digest === undefined) continue\n const source = blobPath(objectsDirectory, digest)\n yield* checkFileSize(fs, \"backup\", source, maxFileSizeBytes)\n const bytes = yield* fs.readFile(source).pipe(\n Effect.mapError(ioFailure(\"backup\", `reading ${source}`))\n )\n const measured = yield* measure(bytes)\n if (measured !== digest) {\n return yield* Effect.fail(\n error(\n \"backup\",\n \"artifact_corruption\",\n `${source} hashes to ${measured}; repair the store before backing it up`,\n source\n )\n )\n }\n const target = blobPath(`${options.directory}/${objectsDirectoryName}`, digest)\n yield* fs.makeDirectory(`${options.directory}/${objectsDirectoryName}/${digest.slice(0, 2)}`, {\n recursive: true\n }).pipe(Effect.mapError(ioFailure(\"backup\", `creating the objects directory for ${digest}`)))\n yield* fs.writeFile(target, bytes).pipe(Effect.mapError(ioFailure(\"backup\", `writing ${target}`)))\n artifacts.push({ digest: measured, sizeBytes: bytes.length })\n captured.add(measured)\n }\n }\n\n const missing = [...snapshot.artifactRoots].filter((digest) => !captured.has(digest)).sort()\n if (missing.length > 0) {\n return yield* Effect.fail(\n error(\n \"backup\",\n \"snapshot_incomplete\",\n `the frozen database references ${missing.length} artifact blobs absent from the backup`,\n { missing }\n )\n )\n }\n\n const manifest: BackupManifest = {\n formatVersion: 1,\n createdAtMs,\n database: {\n file: databaseFileName,\n sha256,\n sizeBytes: databaseBytes.length,\n migrations: snapshot.migrations\n },\n artifacts\n }\n const manifestPath = `${options.directory}/${manifestFileName}`\n const manifestBytes = new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\\n`)\n if (manifestBytes.byteLength > maxFileSizeBytes) {\n const message =\n `${manifestPath} is ${manifestBytes.byteLength} bytes, which exceeds the ${maxFileSizeBytes}-byte file limit`\n return yield* Effect.fail(error(\"backup\", \"io\", message, new RangeError(message)))\n }\n yield* fs.writeFile(manifestPath, manifestBytes).pipe(\n Effect.mapError(ioFailure(\"backup\", `writing ${manifestFileName}`))\n )\n return manifest\n })\n\n // The directory was empty when the capture began, so everything in it is\n // this capture's: the snapshot, any SQLite sidecar the inspection\n // connection left, and the copied blobs. A failed capture removes them and\n // leaves the directory reusable for a retry. Cleanup is best effort; the\n // original failure is the one reported.\n const discardPartial = fs.readDirectory(options.directory).pipe(\n Effect.flatMap((entries) =>\n Effect.forEach(\n entries,\n (entry) => fs.remove(`${options.directory}/${entry}`, { recursive: true, force: true }),\n { discard: true }\n )\n ),\n Effect.ignore\n )\n\n return yield* (options.objectsDirectory === undefined\n ? capture\n : ArtifactBackupLease.withLease(\n fs,\n options.objectsDirectory,\n capture,\n ioFailure(\"backup\", `coordinating artifact backup in ${options.objectsDirectory}`)\n )).pipe(Effect.onError(() => discardPartial))\n }).pipe(Effect.withSpan(\"DisasterRecovery.backup\"))\n\n/**\n * Verifies a backup directory against its manifest without restoring it:\n * the manifest decodes, the database snapshot hashes to its recorded digest,\n * and every listed artifact blob is present and hashes to its address.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const verify = Effect.fn(\"DisasterRecovery.verify\")(function*(\n backupDirectory: string,\n options: FileSizeOptions = {}\n) {\n const limit = fileSizeLimit(\"verify\", options)\n if (typeof limit !== \"number\") return yield* Effect.fail(limit)\n const maxFileSizeBytes = limit\n const fs = yield* FileSystem.FileSystem\n const manifest = yield* readManifest(fs, \"verify\", backupDirectory, maxFileSizeBytes)\n yield* checkedFile(\n fs,\n \"verify\",\n `${backupDirectory}/${manifest.database.file}`,\n manifest.database.sha256,\n maxFileSizeBytes\n )\n for (const entry of manifest.artifacts) {\n yield* checkedFile(\n fs,\n \"verify\",\n blobPath(`${backupDirectory}/${objectsDirectoryName}`, entry.digest),\n entry.digest,\n maxFileSizeBytes\n )\n }\n return manifest\n})\n\n/**\n * Restores a verified backup into an empty target directory.\n *\n * Every byte is re-verified against the manifest as it is copied, so a backup\n * that rotted in storage is refused rather than restored. The target holds\n * the database file, the objects directory, and a `restored.json` marker; it\n * is a fresh store directory to compose `NodeDatabase.layer` and\n * `ArtifactStore.layerFileSystem` over. The database is restored with the\n * pre-backup owner fences still in place \u2014 run {@link fence} against it\n * before any engine adopts it.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const restore = Effect.fn(\"DisasterRecovery.restore\")(function*(options: RestoreOptions) {\n const limit = fileSizeLimit(\"restore\", options)\n if (typeof limit !== \"number\") return yield* Effect.fail(limit)\n const maxFileSizeBytes = limit\n const fs = yield* FileSystem.FileSystem\n const manifest = yield* readManifest(fs, \"restore\", options.backupDirectory, maxFileSizeBytes)\n yield* ensureEmptyDirectory(fs, \"restore\", options.targetDirectory)\n\n const databaseBytes = yield* checkedFile(\n fs,\n \"restore\",\n `${options.backupDirectory}/${manifest.database.file}`,\n manifest.database.sha256,\n maxFileSizeBytes\n )\n const databaseFile = `${options.targetDirectory}/${databaseFileName}`\n yield* fs.writeFile(databaseFile, databaseBytes).pipe(\n Effect.mapError(ioFailure(\"restore\", `writing ${databaseFile}`))\n )\n\n const objectsDirectory = `${options.targetDirectory}/${objectsDirectoryName}`\n for (const entry of manifest.artifacts) {\n const bytes = yield* checkedFile(\n fs,\n \"restore\",\n blobPath(`${options.backupDirectory}/${objectsDirectoryName}`, entry.digest),\n entry.digest,\n maxFileSizeBytes\n )\n yield* fs.makeDirectory(`${objectsDirectory}/${entry.digest.slice(0, 2)}`, { recursive: true }).pipe(\n Effect.mapError(ioFailure(\"restore\", `creating the objects directory for ${entry.digest}`))\n )\n yield* fs.writeFile(blobPath(objectsDirectory, entry.digest), bytes).pipe(\n Effect.mapError(ioFailure(\"restore\", `writing the blob for ${entry.digest}`))\n )\n }\n\n const restoredAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))\n const marker = {\n formatVersion: 1,\n restoredAtMs,\n backupCreatedAtMs: manifest.createdAtMs,\n databaseSha256: manifest.database.sha256\n }\n yield* fs.writeFileString(\n `${options.targetDirectory}/${restoredMarkerFileName}`,\n `${JSON.stringify(marker, null, 2)}\\n`\n ).pipe(Effect.mapError(ioFailure(\"restore\", `writing ${restoredMarkerFileName}`)))\n return { databaseFile, objectsDirectory, manifest } satisfies RestoredStore\n})\n\n/**\n * Bumps the restored store's fencing epoch so pre-backup owners are fenced\n * out.\n *\n * The store's fence is `OwnerId` equality, so the epoch bump is fence\n * invalidation: in one serialized write transaction every pending claim is\n * cleared, every run that was `running` at backup time is suspended with its\n * owner and heartbeat cleared, and every consensus lease is deleted. A\n * surviving pre-backup owner then fails every fenced operation against the\n * restored store \u2014 `heartbeat` and `transitionOwned` report `FenceLost`,\n * fenced journal appends fail `fence_lost` \u2014 exactly as a stale Temporal owner\n * fails its `rangeID` compare-and-swap. The suspended runs are claimable\n * immediately, without waiting out the heartbeat staleness cutoff and without\n * liveness evidence.\n *\n * Every migration recorded in the manifest must remain applied with the same\n * ID and namespaced name. Forward additions within any installed block are\n * compatible, including engine-store `3006` below an existing plan `4003`;\n * the current database layer applies them on open. A missing or renamed\n * historical entry fails `schema_mismatch` before ownership is cleared.\n *\n * In-flight attempt rows are deliberately untouched: attempt writes are\n * fenced on the run row's ownership, and the resuming engine adopts or\n * retries them under its own fresh owner.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const fence = Effect.fn(\"DisasterRecovery.fence\")(function*(manifest: BackupManifest) {\n const sql = yield* Effect.service(SqlClient.SqlClient)\n const writer = yield* DurableWriter\n const applied = yield* appliedMigrations(sql, \"fence\")\n const expected = manifest.database.migrations\n const key = (migration: AppliedMigration): string => `${migration.migrationId}:${migration.name}`\n // IDs identify migrations within their reserved blocks. Compare historical\n // identities independently so additions in lower blocks cannot shift them.\n const appliedById = new Map(applied.map((migration) => [migration.migrationId, migration.name]))\n if (expected.some((migration) => appliedById.get(migration.migrationId) !== migration.name)) {\n return yield* Effect.fail(\n error(\n \"fence\",\n \"schema_mismatch\",\n `the restored database applied [${applied.map(key).join(\", \")}] but the manifest records [${\n expected.map(key).join(\", \")\n }]`,\n { applied, expected }\n )\n )\n }\n return yield* writer.write(\n Effect.gen(function*() {\n const claims = yield* sql<{ readonly run_id: string }>`\n UPDATE flows_runs\n SET\n claim_host_id = NULL,\n claim_pid = NULL,\n claim_nonce = NULL,\n claimed_at_ms = NULL\n WHERE claim_host_id IS NOT NULL\n RETURNING run_id\n `.withoutTransform\n const running = yield* sql<{ readonly run_id: string }>`\n UPDATE flows_runs\n SET\n status = 'suspended',\n owner_host_id = NULL,\n owner_pid = NULL,\n owner_nonce = NULL,\n heartbeat_at_ms = NULL,\n waiting_reason = 'released',\n waiting_wake_at_ms = NULL,\n waiting_token = NULL\n WHERE status = 'running'\n RETURNING run_id\n `.withoutTransform\n yield* sql`DELETE FROM flows_consensus_leases`.withoutTransform\n return { clearedClaims: claims.length, suspendedRuns: running.length } satisfies FenceSummary\n })\n ).pipe(Effect.mapError(sqlFailure(\"fence\", \"invalidating the persisted fences\")))\n})\n\n/**\n * Restores a backup and immediately fences the restored database.\n *\n * This is the one-shot operator API for disaster recovery. The database\n * driver itself still arrives through a caller-supplied Layer so this module\n * remains host-neutral and browser-bundleable; the bundled Node script passes\n * `NodeDatabase.layer` for the restored file.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const restoreAndFence = <R = never, E = never>(\n options: RestoreAndFenceOptions<R, E>\n): Effect.Effect<FencedRestoredStore, DisasterRecoveryError | E, FileSystem.FileSystem | Crypto.Crypto | R> =>\n Effect.gen(function*() {\n const restored = yield* restore(options)\n const summary = yield* fence(restored.manifest).pipe(\n Effect.provide(options.databaseLayer(restored.databaseFile))\n )\n return { ...restored, fence: summary } satisfies FencedRestoredStore\n })\n"],
4
+ "sourcesContent": ["/**\n * Hot backup, verified restore, and restore-time fencing for the durable\n * flows store.\n *\n * A backup captures the whole SQLite database \u2014 journal, run store, step\n * cache, engine state, and the out-of-ladder objects created by\n * `internal/EngineStateSchema.ts` \u2014 with `VACUUM INTO`, which reads one\n * consistent snapshot under WAL while writers stay live. The content-addressed\n * artifact blobs are copied beside it, digest-verified, after the database\n * snapshot: results reference artifacts only after publication\n * (`packages/smithers/flows/step-cache/docs/api.md`), so a blob walk taken after the\n * database snapshot is a superset of every digest the snapshot references.\n * A manifest written last records the digests a restore must verify, so a\n * partial backup is detectable by its missing manifest.\n *\n * Restore never lets a pre-backup owner resurrect. The fencing token is the\n * `OwnerId` triple mirrored on `flows_runs` and arbitrated by the journal's\n * `flows_consensus_leases` \u2014 Temporal's shard `rangeID` equality check reduced\n * to one SQL predicate (`reference/temporal`\n * `service/history/shard/context_impl.go`, `renewRangeLocked`). Temporal\n * fences a restored shard by bumping the persisted epoch above anything a\n * surviving owner could hold; the equivalent bump for an identity fence is\n * {@link fence}: clear every persisted claim, owner, and consensus lease in\n * one serialized write transaction, so every pre-backup `OwnerId` fails its\n * equality compare-and-swap (`FenceLost`, journal `fence_lost`) and the\n * restored runs are claimable immediately instead of after the heartbeat\n * staleness cutoff.\n *\n * Host access arrives through Effect's `FileSystem` and `SqlClient` tags and\n * hashing through the injected `Crypto` service, so the module carries no\n * platform binding. The SQL dialect is SQLite \u2014 a non-SQLite backend fails\n * with the `sql` code rather than pretending to snapshot. Every file is\n * stat-checked before it is buffered for hashing or copying. The configurable\n * ceiling defaults to {@link defaultMaxFileSizeBytes}; an oversized file fails\n * with the typed `io` code instead of reaching the host's allocation limit.\n *\n * @since 0.1.0\n */\n\nimport * as ArtifactBackupLease from \"@smthrs/artifacts/ArtifactBackupLease\"\nimport { Sha256 } from \"@smthrs/crypto\"\nimport { DurableWriter } from \"@smthrs/database/DurableWriter\"\nimport * as Clock from \"effect/Clock\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Effect from \"effect/Effect\"\nimport * as FileSystem from \"effect/FileSystem\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/sql/SqlClient\"\nimport * as ArtifactRoots from \"./internal/ArtifactRoots.ts\"\n\n/**\n * The database file name inside a backup directory and a restored store\n * directory.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const databaseFileName = \"store.sqlite3\"\n\n/**\n * The manifest file name inside a backup directory. It is written last, so\n * its presence marks a complete backup.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const manifestFileName = \"manifest.json\"\n\n/**\n * The artifact blob directory name inside a backup directory and a restored\n * store directory, laid out with the store's own two-hex-prefix fanout.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const objectsDirectoryName = \"objects\"\n\n/**\n * The marker file a restore writes into the target directory, recording when\n * the restore happened and which backup it came from.\n *\n * @since 0.1.0\n * @category constants\n */\nexport const restoredMarkerFileName = \"restored.json\"\n\n/**\n * Default ceiling for one backup file read into memory: 512 MiB.\n *\n * @since 1.0.0\n * @category constants\n */\nexport const defaultMaxFileSizeBytes = 512 * 1024 * 1024\n\n/**\n * One applied row of the shared `flows_migrations` table, recorded at backup\n * time and re-verified at fence time as the schema version of the snapshot.\n *\n * @since 0.1.0\n * @category schemas\n */\nexport const AppliedMigration = Schema.Struct({\n migrationId: Schema.Number,\n name: Schema.String\n})\n\n/**\n * One applied row of the shared `flows_migrations` table.\n *\n * @since 0.1.0\n * @category models\n */\nexport type AppliedMigration = typeof AppliedMigration.Type\n\n/**\n * One captured artifact blob. `sizeBytes` is informational for operators; the\n * digest is the integrity check.\n *\n * @since 0.1.0\n * @category schemas\n */\nexport const ArtifactEntry = Schema.Struct({\n digest: Sha256.Digest,\n sizeBytes: Schema.Number\n})\n\n/**\n * One captured artifact blob.\n *\n * @since 0.1.0\n * @category models\n */\nexport type ArtifactEntry = typeof ArtifactEntry.Type\n\n/**\n * The verification contract of a backup: the database snapshot's digest and\n * schema version, and the digest of every captured artifact blob.\n *\n * @since 0.1.0\n * @category schemas\n */\nexport const BackupManifest = Schema.Struct({\n formatVersion: Schema.Literal(1),\n createdAtMs: Schema.Number,\n database: Schema.Struct({\n file: Schema.Literal(databaseFileName),\n sha256: Sha256.Digest,\n sizeBytes: Schema.Number,\n migrations: Schema.Array(AppliedMigration)\n }),\n artifacts: Schema.Array(ArtifactEntry)\n})\n\n/**\n * The verification contract of a backup.\n *\n * @since 0.1.0\n * @category models\n */\nexport type BackupManifest = typeof BackupManifest.Type\n\n/** JSON text carrying a decoded backup manifest. */\nconst ManifestFromJsonString = Schema.fromJsonString(BackupManifest)\n\n/**\n * Stable failure codes surfaced by backup, verify, restore, and fence.\n * `invalid_options` reports a public option that cannot be admitted safely.\n *\n * @since 0.1.0\n * @category errors\n */\nexport const DisasterRecoveryErrorCode = Schema.Literals([\n \"invalid_options\",\n \"not_empty\",\n \"invalid_manifest\",\n \"missing_file\",\n \"digest_mismatch\",\n \"artifact_corruption\",\n \"snapshot_incomplete\",\n \"schema_mismatch\",\n \"io\",\n \"sql\"\n])\n\n/**\n * A stable disaster-recovery failure code.\n *\n * @since 0.1.0\n * @category errors\n */\nexport type DisasterRecoveryErrorCode = typeof DisasterRecoveryErrorCode.Type\n\n/**\n * A normalized backup or restore failure. The identity string equals this\n * module's package path, per the house rule that a new identity is the\n * defining module path.\n *\n * @since 0.1.0\n * @category errors\n */\nexport class DisasterRecoveryError extends Schema.TaggedError<DisasterRecoveryError>()(\n \"@smthrs/engine-store/DisasterRecoveryError\",\n {\n code: DisasterRecoveryErrorCode,\n method: Schema.String,\n message: Schema.String,\n cause: Schema.Unknown\n }\n) {}\n\n/**\n * Shared buffering limit for backup, verification, and restore operations.\n *\n * @since 1.0.0\n * @category models\n */\nexport interface FileSizeOptions {\n /**\n * Largest file, in bytes, that backup, verification, or restore may buffer.\n * The value must be a finite non-negative safe integer. Defaults to\n * {@link defaultMaxFileSizeBytes}.\n */\n readonly maxFileSizeBytes?: number | undefined\n}\n\n/**\n * Where a backup is written and which artifact store it captures.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface BackupOptions<R = never, E = never> extends FileSizeOptions {\n /** The directory the backup is written into. Created when absent; must be empty when present. */\n readonly directory: string\n /**\n * The live store's content-addressed objects directory (the\n * `ArtifactStore.layerFileSystem` directory). When set, the whole capture\n * holds the cross-process artifact backup lease, so a concurrent\n * `ArtifactGc.gc` sweep cannot delete a blob between the database snapshot\n * and its copy. Absent means the composition has no filesystem artifact\n * tier: the backup carries no blobs and takes no lease, because there is\n * no objects directory to fence, and a frozen snapshot that references any\n * artifact digest fails with `snapshot_incomplete` instead of reporting\n * success. A filesystem-backed composition must therefore pass the same\n * directory it gives `ArtifactStore.layerFileSystem`.\n */\n readonly objectsDirectory?: string | undefined\n /**\n * Opens the frozen `VACUUM INTO` file so backup can enumerate artifact\n * roots and migrations from that exact snapshot, independently of the live\n * connection. The host should open this file without applying migrations.\n */\n readonly snapshotDatabaseLayer: (databaseFile: string) => Layer.Layer<SqlClient.SqlClient, E, R>\n}\n\n/**\n * Which backup to restore and where the restored store lands.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface RestoreOptions extends FileSizeOptions {\n /** The directory a previous {@link backup} wrote. */\n readonly backupDirectory: string\n /** The fresh directory the restored store is written into. Created when absent; must be empty when present. */\n readonly targetDirectory: string\n}\n\n/**\n * Which backup to restore and how to open the freshly restored SQLite file\n * for restore-time fencing.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface RestoreAndFenceOptions<R = never, E = never> extends RestoreOptions {\n /**\n * Builds the database and durable-writer layer for the newly restored file.\n * This keeps database opening as the caller's host composition rather than\n * importing a Node-only driver into this browser-safe module.\n */\n readonly databaseLayer: (databaseFile: string) => Layer.Layer<DurableWriter | SqlClient.SqlClient, E, R>\n}\n\n/**\n * The restored store's on-disk locations, ready to compose database and\n * artifact layers over.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface RestoredStore {\n readonly databaseFile: string\n readonly objectsDirectory: string\n readonly manifest: BackupManifest\n}\n\n/**\n * A restored store after restore-time fencing has invalidated pre-backup\n * owners.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface FencedRestoredStore extends RestoredStore {\n readonly fence: FenceSummary\n}\n\n/**\n * What {@link fence} invalidated on the restored store.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface FenceSummary {\n /** Runs whose pending claim columns were cleared. */\n readonly clearedClaims: number\n /** Runs that were `running` at backup time, now suspended with their owner fence cleared. */\n readonly suspendedRuns: number\n}\n\nconst error = (\n method: string,\n code: DisasterRecoveryErrorCode,\n message: string,\n cause: unknown\n): DisasterRecoveryError =>\n new DisasterRecoveryError({\n code,\n method,\n message: `${code}: DisasterRecovery.${method}: ${message}`,\n cause\n })\n\n/**\n * One mapper per failure family, shared across every call site so the\n * vocabulary stays uniform.\n */\nconst ioFailure = (method: string, subject: string) => (cause: unknown): DisasterRecoveryError =>\n error(method, \"io\", `the host filesystem refused ${subject}`, cause)\n\nconst sqlFailure = (method: string, subject: string) => (cause: unknown): DisasterRecoveryError =>\n error(method, \"sql\", `the database refused ${subject}`, cause)\n\n/** Hashes bytes with the injected crypto service; a broken host is a defect. */\nconst measure = (bytes: Uint8Array): Effect.Effect<typeof Sha256.Digest.Type, never, Crypto.Crypto> =>\n Schema.decodeUnknownEffect(Sha256)(bytes).pipe(Effect.orDie)\n\n/** Resolves and validates the shared whole-file buffering ceiling. */\nconst fileSizeLimit = (\n method: string,\n options: FileSizeOptions\n): number | DisasterRecoveryError => {\n const limit = options.maxFileSizeBytes\n if (limit === undefined) return defaultMaxFileSizeBytes\n if (Number.isSafeInteger(limit) && limit >= 0) return limit\n return error(\n method,\n \"invalid_options\",\n `maxFileSizeBytes must be a finite non-negative safe integer; received ${String(limit)}`,\n limit\n )\n}\n\n/**\n * Refuses an oversized file before a whole-file read allocates its buffer.\n * Every caller admits `limit` through `fileSizeLimit`, so `BigInt` cannot\n * reject it.\n */\nconst checkFileSize = (\n fs: FileSystem.FileSystem,\n method: string,\n path: string,\n limit: number\n): Effect.Effect<void, DisasterRecoveryError> =>\n Effect.gen(function*() {\n const info = yield* fs.stat(path).pipe(Effect.mapError(ioFailure(method, `stating ${path}`)))\n if (info.size <= BigInt(limit)) return\n const message = `${path} is ${info.size} bytes, which exceeds the ${limit}-byte file limit`\n return yield* Effect.fail(error(method, \"io\", message, new RangeError(message)))\n })\n\n/**\n * Admits a target directory that does not exist yet \u2014 created recursively \u2014\n * or exists empty. Anything else is refused rather than merged over.\n */\nconst ensureEmptyDirectory = (\n fs: FileSystem.FileSystem,\n method: string,\n directory: string\n): Effect.Effect<void, DisasterRecoveryError> =>\n Effect.gen(function*() {\n const present = yield* fs.exists(directory).pipe(Effect.mapError(ioFailure(method, `probing ${directory}`)))\n if (!present) {\n return yield* fs.makeDirectory(directory, { recursive: true }).pipe(\n Effect.mapError(ioFailure(method, `creating ${directory}`))\n )\n }\n const entries = yield* fs.readDirectory(directory).pipe(\n Effect.mapError(ioFailure(method, `listing ${directory}`))\n )\n if (entries.length > 0) {\n return yield* Effect.fail(\n error(method, \"not_empty\", `${directory} already holds ${entries.length} entries`, directory)\n )\n }\n })\n\n/**\n * A relative blob path in the store's fanout layout: a two-hex directory\n * holding a 64-hex digest that begins with it. Anything else under the\n * objects directory \u2014 a scratch `.tmp-*` file, the fanout directory itself \u2014\n * is not part of the store's address space.\n */\nconst blobEntry = (entry: string): string | undefined => {\n const match = entry.match(/^([0-9a-f]{2})[/\\\\]([0-9a-f]{64})$/)\n if (match === null || !match[2]!.startsWith(match[1]!)) return undefined\n return match[2]\n}\n\nconst blobPath = (directory: string, digest: string): string => `${directory}/${digest.slice(0, 2)}/${digest}`\n\n/** The applied rows of the shared migrations table, in ladder order. */\nconst appliedMigrations = (\n sql: SqlClient.SqlClient,\n method: string\n): Effect.Effect<Array<AppliedMigration>, DisasterRecoveryError> =>\n sql<{ readonly migration_id: number; readonly name: string }>`\n SELECT migration_id, name FROM flows_migrations ORDER BY migration_id ASC\n `.withoutTransform.pipe(\n Effect.mapError(sqlFailure(method, \"reading flows_migrations\")),\n Effect.map((rows) => rows.map((row) => ({ migrationId: Number(row.migration_id), name: row.name })))\n )\n\ninterface SnapshotState {\n readonly migrations: Array<AppliedMigration>\n readonly artifactRoots: ReadonlySet<string>\n}\n\n/** Reads every artifact root from the frozen database, never the live one. */\nconst inspectSnapshot = (\n sql: SqlClient.SqlClient\n): Effect.Effect<SnapshotState, DisasterRecoveryError> =>\n Effect.gen(function*() {\n const migrations = yield* appliedMigrations(sql, \"backup\")\n const cacheRows = yield* sql<{ readonly meta_json: string }>`\n SELECT meta_json FROM flows_step_cache\n `.withoutTransform.pipe(\n Effect.mapError(sqlFailure(\"backup\", \"reading artifact roots from snapshot step cache\"))\n )\n const attemptRows = yield* sql<{\n readonly checkpoint_json: string | null\n readonly meta_json: string\n }>`\n SELECT checkpoint_json, meta_json FROM flows_attempts\n `.withoutTransform.pipe(\n Effect.mapError(sqlFailure(\"backup\", \"reading artifact roots from snapshot attempts\"))\n )\n const artifactRoots = new Set<string>()\n const decodeFailure = (cause: ArtifactRoots.ArtifactRootDecodeError): DisasterRecoveryError =>\n error(\"backup\", \"snapshot_incomplete\", cause.message, cause.cause)\n for (const row of cacheRows) {\n for (\n const digest of yield* ArtifactRoots.rootDigests(\"flows_step_cache\", row.meta_json).pipe(\n Effect.mapError(decodeFailure)\n )\n ) artifactRoots.add(digest)\n }\n for (const row of attemptRows) {\n for (\n const digest of yield* ArtifactRoots.rootDigests(\"flows_attempts\", row.meta_json).pipe(\n Effect.mapError(decodeFailure)\n )\n ) artifactRoots.add(digest)\n for (\n const digest of yield* ArtifactRoots.checkpointDigests(row.checkpoint_json).pipe(\n Effect.mapError(decodeFailure)\n )\n ) artifactRoots.add(digest)\n }\n return { migrations, artifactRoots }\n })\n\n/** Reads and decodes the manifest a backup directory must carry. */\nconst readManifest = (\n fs: FileSystem.FileSystem,\n method: string,\n backupDirectory: string,\n maxFileSizeBytes: number\n): Effect.Effect<BackupManifest, DisasterRecoveryError, Crypto.Crypto> =>\n Effect.gen(function*() {\n const path = `${backupDirectory}/${manifestFileName}`\n const present = yield* fs.exists(path).pipe(Effect.mapError(ioFailure(method, `probing ${path}`)))\n if (!present) {\n return yield* Effect.fail(\n error(method, \"missing_file\", `${path} does not exist \u2014 the backup is incomplete or not a backup`, path)\n )\n }\n yield* checkFileSize(fs, method, path, maxFileSizeBytes)\n const text = yield* fs.readFileString(path).pipe(Effect.mapError(ioFailure(method, `reading ${path}`)))\n return yield* Schema.decodeUnknownEffect(ManifestFromJsonString)(text).pipe(\n Effect.mapError((cause) =>\n error(method, \"invalid_manifest\", `${path} does not decode as a backup manifest`, cause)\n )\n )\n })\n\n/** Reads a backup file and refuses bytes that no longer hash to the manifest. */\nconst checkedFile = (\n fs: FileSystem.FileSystem,\n method: string,\n path: string,\n expected: string,\n maxFileSizeBytes: number\n): Effect.Effect<Uint8Array, DisasterRecoveryError, Crypto.Crypto> =>\n Effect.gen(function*() {\n const present = yield* fs.exists(path).pipe(Effect.mapError(ioFailure(method, `probing ${path}`)))\n if (!present) {\n return yield* Effect.fail(error(method, \"missing_file\", `${path} does not exist`, path))\n }\n yield* checkFileSize(fs, method, path, maxFileSizeBytes)\n const bytes = yield* fs.readFile(path).pipe(Effect.mapError(ioFailure(method, `reading ${path}`)))\n const measured = yield* measure(bytes)\n if (measured !== expected) {\n return yield* Effect.fail(\n error(method, \"digest_mismatch\", `${path} hashes to ${measured}, the manifest records ${expected}`, path)\n )\n }\n return bytes\n })\n\n/**\n * Captures a hot backup of the live store into an empty directory.\n *\n * The database is snapshotted first with `VACUUM INTO` \u2014 a read transaction\n * under WAL, so live writers are never blocked and the copy is one consistent\n * point in time. The artifact walk runs after it, so every digest the\n * snapshot references is captured (publication precedes reference). Blobs are\n * digest-verified as they are copied; a blob whose bytes no longer hash to\n * its address fails the backup loudly rather than capturing corruption. The\n * manifest is encoded and admitted under the same file-size ceiling before it\n * is written last, so a successful backup remains readable with its own\n * options.\n *\n * With an `objectsDirectory`, the whole capture holds the artifact backup\n * lease, so concurrent sweep deletion cannot remove a referenced blob\n * mid-copy. Without one, no lease is taken; the frozen-root verification\n * still fails the backup loudly when the snapshot references any blob, so an\n * unfenced capture can never report success while missing artifact bytes.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const backup = <R = never, E = never>(\n options: BackupOptions<R, E>\n): Effect.Effect<\n BackupManifest,\n DisasterRecoveryError | E,\n SqlClient.SqlClient | FileSystem.FileSystem | Crypto.Crypto | R\n> =>\n Effect.gen(function*() {\n const limit = fileSizeLimit(\"backup\", options)\n if (typeof limit !== \"number\") return yield* Effect.fail(limit)\n const maxFileSizeBytes = limit\n const sql = yield* Effect.service(SqlClient.SqlClient)\n const fs = yield* FileSystem.FileSystem\n const createdAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))\n yield* ensureEmptyDirectory(fs, \"backup\", options.directory)\n\n const capture = Effect.gen(function*() {\n const databasePath = `${options.directory}/${databaseFileName}`\n yield* sql`VACUUM INTO ${databasePath}`.pipe(\n Effect.mapError(sqlFailure(\"backup\", `snapshotting the database into ${databasePath}`))\n )\n const snapshot = yield* Effect.flatMap(\n Effect.service(SqlClient.SqlClient),\n inspectSnapshot\n ).pipe(Effect.provide(options.snapshotDatabaseLayer(databasePath)))\n // Opening the frozen file may normalize SQLite journal metadata, so its\n // manifest digest is measured only after the inspection connection closes.\n yield* checkFileSize(fs, \"backup\", databasePath, maxFileSizeBytes)\n const databaseBytes = yield* fs.readFile(databasePath).pipe(\n Effect.mapError(ioFailure(\"backup\", `reading ${databasePath} back`))\n )\n const sha256 = yield* measure(databaseBytes)\n\n const artifacts: Array<ArtifactEntry> = []\n const captured = new Set<string>()\n if (options.objectsDirectory !== undefined) {\n const objectsDirectory = options.objectsDirectory\n const entries = yield* fs.readDirectory(objectsDirectory, { recursive: true }).pipe(\n Effect.mapError(ioFailure(\"backup\", `listing ${objectsDirectory}`))\n )\n for (const entry of [...entries].sort()) {\n const digest = blobEntry(entry)\n if (digest === undefined) continue\n const source = blobPath(objectsDirectory, digest)\n yield* checkFileSize(fs, \"backup\", source, maxFileSizeBytes)\n const bytes = yield* fs.readFile(source).pipe(\n Effect.mapError(ioFailure(\"backup\", `reading ${source}`))\n )\n const measured = yield* measure(bytes)\n if (measured !== digest) {\n return yield* Effect.fail(\n error(\n \"backup\",\n \"artifact_corruption\",\n `${source} hashes to ${measured}; repair the store before backing it up`,\n source\n )\n )\n }\n const target = blobPath(`${options.directory}/${objectsDirectoryName}`, digest)\n yield* fs.makeDirectory(`${options.directory}/${objectsDirectoryName}/${digest.slice(0, 2)}`, {\n recursive: true\n }).pipe(Effect.mapError(ioFailure(\"backup\", `creating the objects directory for ${digest}`)))\n yield* fs.writeFile(target, bytes).pipe(Effect.mapError(ioFailure(\"backup\", `writing ${target}`)))\n artifacts.push({ digest: measured, sizeBytes: bytes.length })\n captured.add(measured)\n }\n }\n\n const missing = [...snapshot.artifactRoots].filter((digest) => !captured.has(digest)).sort()\n if (missing.length > 0) {\n return yield* Effect.fail(\n error(\n \"backup\",\n \"snapshot_incomplete\",\n `the frozen database references ${missing.length} artifact blobs absent from the backup`,\n { missing }\n )\n )\n }\n\n const manifest: BackupManifest = {\n formatVersion: 1,\n createdAtMs,\n database: {\n file: databaseFileName,\n sha256,\n sizeBytes: databaseBytes.length,\n migrations: snapshot.migrations\n },\n artifacts\n }\n const manifestPath = `${options.directory}/${manifestFileName}`\n const manifestBytes = new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\\n`)\n if (manifestBytes.byteLength > maxFileSizeBytes) {\n const message =\n `${manifestPath} is ${manifestBytes.byteLength} bytes, which exceeds the ${maxFileSizeBytes}-byte file limit`\n return yield* Effect.fail(error(\"backup\", \"io\", message, new RangeError(message)))\n }\n yield* fs.writeFile(manifestPath, manifestBytes).pipe(\n Effect.mapError(ioFailure(\"backup\", `writing ${manifestFileName}`))\n )\n return manifest\n })\n\n // The directory was empty when the capture began, so everything in it is\n // this capture's: the snapshot, any SQLite sidecar the inspection\n // connection left, and the copied blobs. A failed capture removes them and\n // leaves the directory reusable for a retry. Cleanup is best effort; the\n // original failure is the one reported.\n const discardPartial = fs.readDirectory(options.directory).pipe(\n Effect.flatMap((entries) =>\n Effect.forEach(\n entries,\n (entry) => fs.remove(`${options.directory}/${entry}`, { recursive: true, force: true }),\n { discard: true }\n )\n ),\n Effect.ignore\n )\n\n return yield* (options.objectsDirectory === undefined\n ? capture\n : ArtifactBackupLease.withLease(\n fs,\n options.objectsDirectory,\n capture,\n ioFailure(\"backup\", `coordinating artifact backup in ${options.objectsDirectory}`)\n )).pipe(Effect.onError(() => discardPartial))\n }).pipe(Effect.withSpan(\"DisasterRecovery.backup\"))\n\n/**\n * Verifies a backup directory against its manifest without restoring it:\n * the manifest decodes, the database snapshot hashes to its recorded digest,\n * and every listed artifact blob is present and hashes to its address.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const verify = Effect.fn(\"DisasterRecovery.verify\")(function*(\n backupDirectory: string,\n options: FileSizeOptions = {}\n) {\n const limit = fileSizeLimit(\"verify\", options)\n if (typeof limit !== \"number\") return yield* Effect.fail(limit)\n const maxFileSizeBytes = limit\n const fs = yield* FileSystem.FileSystem\n const manifest = yield* readManifest(fs, \"verify\", backupDirectory, maxFileSizeBytes)\n yield* checkedFile(\n fs,\n \"verify\",\n `${backupDirectory}/${manifest.database.file}`,\n manifest.database.sha256,\n maxFileSizeBytes\n )\n for (const entry of manifest.artifacts) {\n yield* checkedFile(\n fs,\n \"verify\",\n blobPath(`${backupDirectory}/${objectsDirectoryName}`, entry.digest),\n entry.digest,\n maxFileSizeBytes\n )\n }\n return manifest\n})\n\n/**\n * Restores a verified backup into an empty target directory.\n *\n * Every byte is re-verified against the manifest as it is copied, so a backup\n * that rotted in storage is refused rather than restored. The target holds\n * the database file, the objects directory, and a `restored.json` marker; it\n * is a fresh store directory to compose `NodeDatabase.layer` and\n * `ArtifactStore.layerFileSystem` over. The database is restored with the\n * pre-backup owner fences still in place \u2014 run {@link fence} against it\n * before any engine adopts it.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const restore = Effect.fn(\"DisasterRecovery.restore\")(function*(options: RestoreOptions) {\n const limit = fileSizeLimit(\"restore\", options)\n if (typeof limit !== \"number\") return yield* Effect.fail(limit)\n const maxFileSizeBytes = limit\n const fs = yield* FileSystem.FileSystem\n const manifest = yield* readManifest(fs, \"restore\", options.backupDirectory, maxFileSizeBytes)\n yield* ensureEmptyDirectory(fs, \"restore\", options.targetDirectory)\n\n const databaseBytes = yield* checkedFile(\n fs,\n \"restore\",\n `${options.backupDirectory}/${manifest.database.file}`,\n manifest.database.sha256,\n maxFileSizeBytes\n )\n const databaseFile = `${options.targetDirectory}/${databaseFileName}`\n yield* fs.writeFile(databaseFile, databaseBytes).pipe(\n Effect.mapError(ioFailure(\"restore\", `writing ${databaseFile}`))\n )\n\n const objectsDirectory = `${options.targetDirectory}/${objectsDirectoryName}`\n for (const entry of manifest.artifacts) {\n const bytes = yield* checkedFile(\n fs,\n \"restore\",\n blobPath(`${options.backupDirectory}/${objectsDirectoryName}`, entry.digest),\n entry.digest,\n maxFileSizeBytes\n )\n yield* fs.makeDirectory(`${objectsDirectory}/${entry.digest.slice(0, 2)}`, { recursive: true }).pipe(\n Effect.mapError(ioFailure(\"restore\", `creating the objects directory for ${entry.digest}`))\n )\n yield* fs.writeFile(blobPath(objectsDirectory, entry.digest), bytes).pipe(\n Effect.mapError(ioFailure(\"restore\", `writing the blob for ${entry.digest}`))\n )\n }\n\n const restoredAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))\n const marker = {\n formatVersion: 1,\n restoredAtMs,\n backupCreatedAtMs: manifest.createdAtMs,\n databaseSha256: manifest.database.sha256\n }\n yield* fs.writeFileString(\n `${options.targetDirectory}/${restoredMarkerFileName}`,\n `${JSON.stringify(marker, null, 2)}\\n`\n ).pipe(Effect.mapError(ioFailure(\"restore\", `writing ${restoredMarkerFileName}`)))\n return { databaseFile, objectsDirectory, manifest } satisfies RestoredStore\n})\n\n/**\n * Bumps the restored store's fencing epoch so pre-backup owners are fenced\n * out.\n *\n * The store's fence is `OwnerId` equality, so the epoch bump is fence\n * invalidation: in one serialized write transaction every pending claim is\n * cleared, every run that was `running` at backup time is suspended with its\n * owner and heartbeat cleared, and every consensus lease is deleted. A\n * surviving pre-backup owner then fails every fenced operation against the\n * restored store \u2014 `heartbeat` and `transitionOwned` report `FenceLost`,\n * fenced journal appends fail `fence_lost` \u2014 exactly as a stale Temporal owner\n * fails its `rangeID` compare-and-swap. The suspended runs are claimable\n * immediately, without waiting out the heartbeat staleness cutoff and without\n * liveness evidence.\n *\n * Every migration recorded in the manifest must remain applied with the same\n * ID and namespaced name. Forward additions within any installed block are\n * compatible, including engine-store `3006` below an existing plan `4003`;\n * the current database layer applies them on open. A missing or renamed\n * historical entry fails `schema_mismatch` before ownership is cleared.\n *\n * In-flight attempt rows are deliberately untouched: attempt writes are\n * fenced on the run row's ownership, and the resuming engine adopts or\n * retries them under its own fresh owner.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const fence = Effect.fn(\"DisasterRecovery.fence\")(function*(manifest: BackupManifest) {\n const sql = yield* Effect.service(SqlClient.SqlClient)\n const writer = yield* DurableWriter\n const applied = yield* appliedMigrations(sql, \"fence\")\n const expected = manifest.database.migrations\n const key = (migration: AppliedMigration): string => `${migration.migrationId}:${migration.name}`\n // IDs identify migrations within their reserved blocks. Compare historical\n // identities independently so additions in lower blocks cannot shift them.\n const appliedById = new Map(applied.map((migration) => [migration.migrationId, migration.name]))\n if (expected.some((migration) => appliedById.get(migration.migrationId) !== migration.name)) {\n return yield* Effect.fail(\n error(\n \"fence\",\n \"schema_mismatch\",\n `the restored database applied [${applied.map(key).join(\", \")}] but the manifest records [${\n expected.map(key).join(\", \")\n }]`,\n { applied, expected }\n )\n )\n }\n return yield* writer.write(\n Effect.gen(function*() {\n const claims = yield* sql<{ readonly run_id: string }>`\n UPDATE flows_runs\n SET\n claim_host_id = NULL,\n claim_pid = NULL,\n claim_nonce = NULL,\n claimed_at_ms = NULL\n WHERE claim_host_id IS NOT NULL\n RETURNING run_id\n `.withoutTransform\n const running = yield* sql<{ readonly run_id: string }>`\n UPDATE flows_runs\n SET\n status = 'suspended',\n owner_host_id = NULL,\n owner_pid = NULL,\n owner_nonce = NULL,\n heartbeat_at_ms = NULL,\n waiting_reason = 'released',\n waiting_wake_at_ms = NULL,\n waiting_token = NULL\n WHERE status = 'running'\n RETURNING run_id\n `.withoutTransform\n yield* sql`DELETE FROM flows_consensus_leases`.withoutTransform\n return { clearedClaims: claims.length, suspendedRuns: running.length } satisfies FenceSummary\n })\n ).pipe(Effect.mapError(sqlFailure(\"fence\", \"invalidating the persisted fences\")))\n})\n\n/**\n * Restores a backup and immediately fences the restored database.\n *\n * This is the one-shot operator API for disaster recovery. The database\n * driver itself still arrives through a caller-supplied Layer so this module\n * remains host-neutral and browser-bundleable; the bundled Node script passes\n * `NodeDatabase.layer` for the restored file.\n *\n * @since 0.1.0\n * @category operations\n */\nexport const restoreAndFence = <R = never, E = never>(\n options: RestoreAndFenceOptions<R, E>\n): Effect.Effect<FencedRestoredStore, DisasterRecoveryError | E, FileSystem.FileSystem | Crypto.Crypto | R> =>\n Effect.gen(function*() {\n const restored = yield* restore(options)\n const summary = yield* fence(restored.manifest).pipe(\n Effect.provide(options.databaseLayer(restored.databaseFile))\n )\n return { ...restored, fence: summary } satisfies FencedRestoredStore\n })\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,7 +18,7 @@ import * as Effect from "effect/Effect";
18
18
  import * as Layer from "effect/Layer";
19
19
  import * as Option from "effect/Option";
20
20
  import * as Schema from "effect/Schema";
21
- import * as SqlClient from "effect/unstable/sql/SqlClient";
21
+ import * as SqlClient from "effect/sql/SqlClient";
22
22
  import type { OnParentExit } from "./RunState.ts";
23
23
  /**
24
24
  * A position in pending admission creation order.