@smthrs/run-store 0.0.0-stage → 1.0.0-rc.3

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 (157) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +21 -0
  3. package/README.md +195 -2
  4. package/dist/cjs/AttemptStore.d.ts +359 -0
  5. package/dist/cjs/AttemptStore.d.ts.map +1 -0
  6. package/dist/cjs/AttemptStore.js +731 -0
  7. package/dist/cjs/AttemptStore.js.map +7 -0
  8. package/dist/cjs/Heartbeat.d.ts +46 -0
  9. package/dist/cjs/Heartbeat.d.ts.map +1 -0
  10. package/dist/cjs/Heartbeat.js +28 -0
  11. package/dist/cjs/Heartbeat.js.map +7 -0
  12. package/dist/cjs/Migrations.d.ts +42 -0
  13. package/dist/cjs/Migrations.d.ts.map +1 -0
  14. package/dist/cjs/Migrations.js +55 -0
  15. package/dist/cjs/Migrations.js.map +7 -0
  16. package/dist/cjs/Ownership.d.ts +275 -0
  17. package/dist/cjs/Ownership.d.ts.map +1 -0
  18. package/dist/cjs/Ownership.js +129 -0
  19. package/dist/cjs/Ownership.js.map +7 -0
  20. package/dist/cjs/RunStore.d.ts +571 -0
  21. package/dist/cjs/RunStore.d.ts.map +1 -0
  22. package/dist/cjs/RunStore.js +1079 -0
  23. package/dist/cjs/RunStore.js.map +7 -0
  24. package/dist/cjs/RunStoreMetrics.d.ts +151 -0
  25. package/dist/cjs/RunStoreMetrics.d.ts.map +1 -0
  26. package/dist/cjs/RunStoreMetrics.js +91 -0
  27. package/dist/cjs/RunStoreMetrics.js.map +7 -0
  28. package/dist/cjs/index.d.ts +47 -0
  29. package/dist/cjs/index.d.ts.map +1 -0
  30. package/dist/cjs/index.js +43 -0
  31. package/dist/cjs/index.js.map +7 -0
  32. package/dist/cjs/internal/Boundary.d.ts +80 -0
  33. package/dist/cjs/internal/Boundary.d.ts.map +1 -0
  34. package/dist/cjs/internal/Boundary.js +92 -0
  35. package/dist/cjs/internal/Boundary.js.map +7 -0
  36. package/dist/cjs/internal/Companion.d.ts +44 -0
  37. package/dist/cjs/internal/Companion.d.ts.map +1 -0
  38. package/dist/cjs/internal/Companion.js +50 -0
  39. package/dist/cjs/internal/Companion.js.map +7 -0
  40. package/dist/cjs/internal/SpanOutcome.d.ts +37 -0
  41. package/dist/cjs/internal/SpanOutcome.d.ts.map +1 -0
  42. package/dist/cjs/internal/SpanOutcome.js +62 -0
  43. package/dist/cjs/internal/SpanOutcome.js.map +7 -0
  44. package/dist/cjs/migrations/0001_initial.d.ts +15 -0
  45. package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
  46. package/dist/cjs/migrations/0001_initial.js +110 -0
  47. package/dist/cjs/migrations/0001_initial.js.map +7 -0
  48. package/dist/cjs/migrations/0002_lineage.d.ts +27 -0
  49. package/dist/cjs/migrations/0002_lineage.d.ts.map +1 -0
  50. package/dist/cjs/migrations/0002_lineage.js +43 -0
  51. package/dist/cjs/migrations/0002_lineage.js.map +7 -0
  52. package/dist/cjs/migrations/0003_execution_revisions.d.ts +15 -0
  53. package/dist/cjs/migrations/0003_execution_revisions.d.ts.map +1 -0
  54. package/dist/cjs/migrations/0003_execution_revisions.js +77 -0
  55. package/dist/cjs/migrations/0003_execution_revisions.js.map +7 -0
  56. package/dist/cjs/migrations/0004_waiting_request.d.ts +32 -0
  57. package/dist/cjs/migrations/0004_waiting_request.d.ts.map +1 -0
  58. package/dist/cjs/migrations/0004_waiting_request.js +42 -0
  59. package/dist/cjs/migrations/0004_waiting_request.js.map +7 -0
  60. package/dist/cjs/package.json +1 -0
  61. package/dist/cjs/test/TestRunStore.d.ts +17 -0
  62. package/dist/cjs/test/TestRunStore.d.ts.map +1 -0
  63. package/dist/cjs/test/TestRunStore.js +42 -0
  64. package/dist/cjs/test/TestRunStore.js.map +7 -0
  65. package/dist/esm/AttemptStore.d.ts +359 -0
  66. package/dist/esm/AttemptStore.d.ts.map +1 -0
  67. package/dist/esm/AttemptStore.js +835 -0
  68. package/dist/esm/AttemptStore.js.map +1 -0
  69. package/dist/esm/Heartbeat.d.ts +46 -0
  70. package/dist/esm/Heartbeat.d.ts.map +1 -0
  71. package/dist/esm/Heartbeat.js +46 -0
  72. package/dist/esm/Heartbeat.js.map +1 -0
  73. package/dist/esm/Migrations.d.ts +42 -0
  74. package/dist/esm/Migrations.d.ts.map +1 -0
  75. package/dist/esm/Migrations.js +56 -0
  76. package/dist/esm/Migrations.js.map +1 -0
  77. package/dist/esm/Ownership.d.ts +275 -0
  78. package/dist/esm/Ownership.d.ts.map +1 -0
  79. package/dist/esm/Ownership.js +299 -0
  80. package/dist/esm/Ownership.js.map +1 -0
  81. package/dist/esm/RunStore.d.ts +571 -0
  82. package/dist/esm/RunStore.d.ts.map +1 -0
  83. package/dist/esm/RunStore.js +1287 -0
  84. package/dist/esm/RunStore.js.map +1 -0
  85. package/dist/esm/RunStoreMetrics.d.ts +151 -0
  86. package/dist/esm/RunStoreMetrics.d.ts.map +1 -0
  87. package/dist/esm/RunStoreMetrics.js +120 -0
  88. package/dist/esm/RunStoreMetrics.js.map +1 -0
  89. package/dist/esm/index.d.ts +47 -0
  90. package/dist/esm/index.d.ts.map +1 -0
  91. package/dist/esm/index.js +47 -0
  92. package/dist/esm/index.js.map +1 -0
  93. package/dist/esm/internal/Boundary.d.ts +80 -0
  94. package/dist/esm/internal/Boundary.d.ts.map +1 -0
  95. package/dist/esm/internal/Boundary.js +109 -0
  96. package/dist/esm/internal/Boundary.js.map +1 -0
  97. package/dist/esm/internal/Companion.d.ts +44 -0
  98. package/dist/esm/internal/Companion.d.ts.map +1 -0
  99. package/dist/esm/internal/Companion.js +53 -0
  100. package/dist/esm/internal/Companion.js.map +1 -0
  101. package/dist/esm/internal/SpanOutcome.d.ts +37 -0
  102. package/dist/esm/internal/SpanOutcome.d.ts.map +1 -0
  103. package/dist/esm/internal/SpanOutcome.js +48 -0
  104. package/dist/esm/internal/SpanOutcome.js.map +1 -0
  105. package/dist/esm/migrations/0001_initial.d.ts +15 -0
  106. package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
  107. package/dist/esm/migrations/0001_initial.js +88 -0
  108. package/dist/esm/migrations/0001_initial.js.map +1 -0
  109. package/dist/esm/migrations/0002_lineage.d.ts +27 -0
  110. package/dist/esm/migrations/0002_lineage.d.ts.map +1 -0
  111. package/dist/esm/migrations/0002_lineage.js +33 -0
  112. package/dist/esm/migrations/0002_lineage.js.map +1 -0
  113. package/dist/esm/migrations/0003_execution_revisions.d.ts +15 -0
  114. package/dist/esm/migrations/0003_execution_revisions.d.ts.map +1 -0
  115. package/dist/esm/migrations/0003_execution_revisions.js +58 -0
  116. package/dist/esm/migrations/0003_execution_revisions.js.map +1 -0
  117. package/dist/esm/migrations/0004_waiting_request.d.ts +32 -0
  118. package/dist/esm/migrations/0004_waiting_request.d.ts.map +1 -0
  119. package/dist/esm/migrations/0004_waiting_request.js +37 -0
  120. package/dist/esm/migrations/0004_waiting_request.js.map +1 -0
  121. package/dist/esm/test/TestRunStore.d.ts +17 -0
  122. package/dist/esm/test/TestRunStore.d.ts.map +1 -0
  123. package/dist/esm/test/TestRunStore.js +19 -0
  124. package/dist/esm/test/TestRunStore.js.map +1 -0
  125. package/docs/README.md +142 -0
  126. package/docs/api.md +901 -0
  127. package/docs/concepts/durable-values.md +140 -0
  128. package/docs/concepts/execution-revisions.md +53 -0
  129. package/docs/concepts/fencing.md +146 -0
  130. package/docs/concepts/leases.md +154 -0
  131. package/docs/concepts/liveness-evidence.md +142 -0
  132. package/docs/guides/cancel-a-run.md +156 -0
  133. package/docs/guides/claim-and-finish-a-run.md +175 -0
  134. package/docs/guides/compose-the-stores.md +142 -0
  135. package/docs/guides/observe-outcomes.md +182 -0
  136. package/docs/guides/record-step-attempts.md +170 -0
  137. package/docs/guides/recover-a-stalled-run.md +173 -0
  138. package/docs/guides/testing.md +141 -0
  139. package/docs/installation.md +109 -0
  140. package/docs/quickstart.md +201 -0
  141. package/docs/troubleshooting.md +255 -0
  142. package/package.json +183 -3
  143. package/src/AttemptStore.ts +1195 -0
  144. package/src/Heartbeat.ts +47 -0
  145. package/src/Migrations.ts +59 -0
  146. package/src/Ownership.ts +407 -0
  147. package/src/RunStore.ts +2063 -0
  148. package/src/RunStoreMetrics.ts +165 -0
  149. package/src/index.ts +51 -0
  150. package/src/internal/Boundary.ts +130 -0
  151. package/src/internal/Companion.ts +64 -0
  152. package/src/internal/SpanOutcome.ts +71 -0
  153. package/src/migrations/0001_initial.ts +114 -0
  154. package/src/migrations/0002_lineage.ts +38 -0
  155. package/src/migrations/0003_execution_revisions.ts +69 -0
  156. package/src/migrations/0004_waiting_request.ts +39 -0
  157. package/src/test/TestRunStore.ts +22 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0001_initial.js","sourceRoot":"","sources":["../../../src/migrations/0001_initial.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Initial durable run and attempt schema.\n *\n * @since 0.1.0\n */\n\nimport * as Dialect from \"@smthrs/database/Dialect\"\nimport * as Effect from \"effect/Effect\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\n\n/**\n * Creates the `flows_runs` and `flows_attempts` tables and their indexes.\n *\n * @category migrations\n * @since 0.1.0\n */\nexport const initial: Effect.Effect<void, unknown, SqlClient.SqlClient> = Effect.gen(function*() {\n const sql = yield* SqlClient.SqlClient\n\n yield* sql`CREATE TABLE flows_runs (\n ${Dialect.rowId(sql)}\n run_id TEXT PRIMARY KEY CHECK (length(run_id) > 0),\n status TEXT NOT NULL,\n created_at_ms ${Dialect.integer(sql)} NOT NULL CHECK (${\n Dialect.isInteger(sql, sql`created_at_ms`)\n } AND created_at_ms >= 0 AND created_at_ms <= 9007199254740991),\n started_at_ms ${Dialect.integer(sql)} CHECK (started_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`started_at_ms`)\n } AND started_at_ms >= 0 AND started_at_ms <= 9007199254740991)),\n finished_at_ms ${Dialect.integer(sql)} CHECK (finished_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`finished_at_ms`)\n } AND finished_at_ms >= 0 AND finished_at_ms <= 9007199254740991)),\n owner_host_id TEXT,\n owner_pid ${Dialect.integer(sql)},\n owner_nonce TEXT,\n heartbeat_at_ms ${Dialect.integer(sql)} CHECK (heartbeat_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`heartbeat_at_ms`)\n } AND heartbeat_at_ms >= 0 AND heartbeat_at_ms <= 9007199254740991)),\n claim_host_id TEXT,\n claim_pid ${Dialect.integer(sql)},\n claim_nonce TEXT,\n claimed_at_ms ${Dialect.integer(sql)} CHECK (claimed_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`claimed_at_ms`)\n } AND claimed_at_ms >= 0 AND claimed_at_ms <= 9007199254740991)),\n parent_run_id TEXT CHECK (parent_run_id IS NULL OR length(parent_run_id) > 0),\n cancel_requested_at_ms ${Dialect.integer(sql)} CHECK (cancel_requested_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`cancel_requested_at_ms`)\n } AND cancel_requested_at_ms >= 0 AND cancel_requested_at_ms <= 9007199254740991)),\n waiting_reason TEXT CHECK (waiting_reason IS NULL OR length(waiting_reason) > 0),\n waiting_wake_at_ms ${Dialect.integer(sql)} CHECK (waiting_wake_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`waiting_wake_at_ms`)\n } AND waiting_wake_at_ms >= 0 AND waiting_wake_at_ms <= 9007199254740991)),\n waiting_token TEXT CHECK (waiting_token IS NULL OR length(waiting_token) > 0),\n state_json TEXT NOT NULL CHECK (${Dialect.jsonValid(sql, sql`state_json`)}),\n CHECK (status IN ('pending', 'running', 'suspended', 'completed', 'failed', 'cancelled')),\n CHECK (\n (\n status = 'running' AND\n owner_host_id IS NOT NULL AND\n length(owner_host_id) > 0 AND\n owner_pid IS NOT NULL AND\n ${Dialect.isInteger(sql, sql`owner_pid`)} AND owner_pid >= 0 AND\n owner_nonce IS NOT NULL AND\n length(owner_nonce) > 0 AND\n heartbeat_at_ms IS NOT NULL\n ) OR (\n status <> 'running' AND\n owner_host_id IS NULL AND\n owner_pid IS NULL AND\n owner_nonce IS NULL AND\n heartbeat_at_ms IS NULL\n )\n ),\n CHECK (\n (claim_host_id IS NULL AND claim_pid IS NULL AND claim_nonce IS NULL AND claimed_at_ms IS NULL) OR\n (claim_host_id IS NOT NULL AND length(claim_host_id) > 0 AND\n claim_pid IS NOT NULL AND ${Dialect.isInteger(sql, sql`claim_pid`)} AND claim_pid >= 0 AND\n claim_nonce IS NOT NULL AND length(claim_nonce) > 0 AND claimed_at_ms IS NOT NULL)\n ),\n CHECK (\n (waiting_reason IS NULL AND waiting_wake_at_ms IS NULL AND waiting_token IS NULL) OR\n waiting_reason IS NOT NULL\n ),\n FOREIGN KEY (parent_run_id) REFERENCES flows_runs (run_id)\n )`\n\n yield* sql`CREATE INDEX flows_runs_parent_run_id_idx ON flows_runs (parent_run_id)`\n yield* sql`CREATE INDEX flows_runs_cancel_requested_idx ON flows_runs (cancel_requested_at_ms) WHERE cancel_requested_at_ms IS NOT NULL`\n yield* sql`CREATE INDEX flows_runs_waiting_reason_wake_at_idx ON flows_runs (waiting_reason, waiting_wake_at_ms) WHERE waiting_reason IS NOT NULL`\n\n yield* sql`CREATE TABLE flows_attempts (\n run_id TEXT NOT NULL CHECK (length(run_id) > 0),\n step_key_digest TEXT NOT NULL CHECK (length(step_key_digest) > 0),\n attempt ${Dialect.integer(sql)} NOT NULL CHECK (${\n Dialect.isInteger(sql, sql`attempt`)\n } AND attempt >= 0 AND attempt <= 9007199254740991),\n state TEXT NOT NULL CHECK (length(state) > 0),\n started_at_ms ${Dialect.integer(sql)} NOT NULL CHECK (${\n Dialect.isInteger(sql, sql`started_at_ms`)\n } AND started_at_ms >= 0 AND started_at_ms <= 9007199254740991),\n finished_at_ms ${Dialect.integer(sql)} CHECK (finished_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`finished_at_ms`)\n } AND finished_at_ms >= 0 AND finished_at_ms <= 9007199254740991)),\n heartbeat_at_ms ${Dialect.integer(sql)} CHECK (heartbeat_at_ms IS NULL OR (${\n Dialect.isInteger(sql, sql`heartbeat_at_ms`)\n } AND heartbeat_at_ms >= 0 AND heartbeat_at_ms <= 9007199254740991)),\n checkpoint_json TEXT CHECK (checkpoint_json IS NULL OR ${Dialect.jsonValid(sql, sql`checkpoint_json`)}),\n error_json TEXT CHECK (error_json IS NULL OR ${Dialect.jsonValid(sql, sql`error_json`)}),\n outcome_json TEXT CHECK (outcome_json IS NULL OR ${Dialect.jsonValid(sql, sql`outcome_json`)}),\n meta_json TEXT NOT NULL CHECK (${Dialect.jsonValid(sql, sql`meta_json`)}),\n PRIMARY KEY (run_id, step_key_digest, attempt),\n FOREIGN KEY (run_id) REFERENCES flows_runs (run_id)\n )`\n})\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Trampoline lineage columns on `flows_runs`.
3
+ *
4
+ * A round is its own run row, chained to the previous round through the `parent_run_id`
5
+ * column this table already reserved for continue-as-new, and these two
6
+ * columns are what make the chain readable as a lineage rather than as an
7
+ * anonymous ancestry: which lineage a round belongs to, and which round of it
8
+ * this is. They are NOT `flows_run_parents` edges — that table is the subflow
9
+ * DAG cycle detection walks, and a lineage is a chain of successive
10
+ * executions of one run rather than a parent spawning a child.
11
+ *
12
+ * Append-only, as every migration after the first must be: both columns are
13
+ * nullable, so every row an earlier build wrote stays valid and reads back as
14
+ * "round 0 of its own lineage".
15
+ *
16
+ * @since 0.1.0
17
+ */
18
+ import * as Effect from "effect/Effect";
19
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
20
+ /**
21
+ * Adds `lineage_id` and `round_ordinal` to `flows_runs` and indexes the pair.
22
+ *
23
+ * @category migrations
24
+ * @since 0.1.0
25
+ */
26
+ export declare const lineage: Effect.Effect<void, unknown, SqlClient.SqlClient>;
27
+ //# sourceMappingURL=0002_lineage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0002_lineage.d.ts","sourceRoot":"","sources":["../../../src/migrations/0002_lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,SAAS,CASpE,CAAA"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Trampoline lineage columns on `flows_runs`.
3
+ *
4
+ * A round is its own run row, chained to the previous round through the `parent_run_id`
5
+ * column this table already reserved for continue-as-new, and these two
6
+ * columns are what make the chain readable as a lineage rather than as an
7
+ * anonymous ancestry: which lineage a round belongs to, and which round of it
8
+ * this is. They are NOT `flows_run_parents` edges — that table is the subflow
9
+ * DAG cycle detection walks, and a lineage is a chain of successive
10
+ * executions of one run rather than a parent spawning a child.
11
+ *
12
+ * Append-only, as every migration after the first must be: both columns are
13
+ * nullable, so every row an earlier build wrote stays valid and reads back as
14
+ * "round 0 of its own lineage".
15
+ *
16
+ * @since 0.1.0
17
+ */
18
+ import * as Dialect from "@smthrs/database/Dialect";
19
+ import * as Effect from "effect/Effect";
20
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
21
+ /**
22
+ * Adds `lineage_id` and `round_ordinal` to `flows_runs` and indexes the pair.
23
+ *
24
+ * @category migrations
25
+ * @since 0.1.0
26
+ */
27
+ export const lineage = Effect.gen(function* () {
28
+ const sql = yield* SqlClient.SqlClient;
29
+ yield* sql `ALTER TABLE flows_runs ADD COLUMN lineage_id TEXT CHECK (lineage_id IS NULL OR length(lineage_id) > 0)`;
30
+ yield* sql `ALTER TABLE flows_runs ADD COLUMN round_ordinal ${Dialect.integer(sql)} CHECK (round_ordinal IS NULL OR (${Dialect.isInteger(sql, sql `round_ordinal`)} AND round_ordinal >= 0 AND round_ordinal <= 9007199254740991))`;
31
+ yield* sql `CREATE UNIQUE INDEX flows_runs_lineage_idx ON flows_runs (lineage_id, round_ordinal)`;
32
+ });
33
+ //# sourceMappingURL=0002_lineage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0002_lineage.js","sourceRoot":"","sources":["../../../src/migrations/0002_lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAA;AACnD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAsD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC5F,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAA;IAEtC,KAAK,CAAC,CAAC,GAAG,CAAA,wGAAwG,CAAA;IAClH,KAAK,CAAC,CAAC,GAAG,CAAA,mDAAmD,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qCAC/E,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,eAAe,CAC3C,iEAAiE,CAAA;IAEjE,KAAK,CAAC,CAAC,GAAG,CAAA,sFAAsF,CAAA;AAClG,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Trampoline lineage columns on `flows_runs`.\n *\n * A round is its own run row, chained to the previous round through the `parent_run_id`\n * column this table already reserved for continue-as-new, and these two\n * columns are what make the chain readable as a lineage rather than as an\n * anonymous ancestry: which lineage a round belongs to, and which round of it\n * this is. They are NOT `flows_run_parents` edges — that table is the subflow\n * DAG cycle detection walks, and a lineage is a chain of successive\n * executions of one run rather than a parent spawning a child.\n *\n * Append-only, as every migration after the first must be: both columns are\n * nullable, so every row an earlier build wrote stays valid and reads back as\n * \"round 0 of its own lineage\".\n *\n * @since 0.1.0\n */\n\nimport * as Dialect from \"@smthrs/database/Dialect\"\nimport * as Effect from \"effect/Effect\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\n\n/**\n * Adds `lineage_id` and `round_ordinal` to `flows_runs` and indexes the pair.\n *\n * @category migrations\n * @since 0.1.0\n */\nexport const lineage: Effect.Effect<void, unknown, SqlClient.SqlClient> = Effect.gen(function*() {\n const sql = yield* SqlClient.SqlClient\n\n yield* sql`ALTER TABLE flows_runs ADD COLUMN lineage_id TEXT CHECK (lineage_id IS NULL OR length(lineage_id) > 0)`\n yield* sql`ALTER TABLE flows_runs ADD COLUMN round_ordinal ${Dialect.integer(sql)} CHECK (round_ordinal IS NULL OR (${\n Dialect.isInteger(sql, sql`round_ordinal`)\n } AND round_ordinal >= 0 AND round_ordinal <= 9007199254740991))`\n\n yield* sql`CREATE UNIQUE INDEX flows_runs_lineage_idx ON flows_runs (lineage_id, round_ordinal)`\n})\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Durable database identity and coalesced execution change tracking.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
8
+ /**
9
+ * Retains one current revision per run ID, including deleted IDs forever.
10
+ * Triggers participate in the mutating transaction, including raw SQL writes.
11
+ * @category migrations
12
+ * @since 1.0.0
13
+ */
14
+ export declare const executionRevisions: Effect.Effect<void, unknown, SqlClient.SqlClient>;
15
+ //# sourceMappingURL=0003_execution_revisions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0003_execution_revisions.d.ts","sourceRoot":"","sources":["../../../src/migrations/0003_execution_revisions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,SAAS,CAoD/E,CAAA"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Durable database identity and coalesced execution change tracking.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ import * as Dialect from "@smthrs/database/Dialect";
7
+ import * as Effect from "effect/Effect";
8
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
9
+ /**
10
+ * Retains one current revision per run ID, including deleted IDs forever.
11
+ * Triggers participate in the mutating transaction, including raw SQL writes.
12
+ * @category migrations
13
+ * @since 1.0.0
14
+ */
15
+ export const executionRevisions = Effect.gen(function* () {
16
+ const sql = yield* SqlClient.SqlClient;
17
+ yield* sql `CREATE TABLE flows_run_source (
18
+ singleton ${Dialect.integer(sql)} PRIMARY KEY CHECK (singleton = 1),
19
+ source TEXT NOT NULL CHECK (length(source) = 32 AND ${sql.literal(Dialect.isPostgres(sql) ? "source !~ '[^0-9a-f]'" : "source NOT GLOB '*[^0-9a-f]*'")}),
20
+ revision ${Dialect.integer(sql)} NOT NULL CHECK (${Dialect.isInteger(sql, sql `revision`)} AND revision >= 0 AND revision <= 9007199254740991)
21
+ )`;
22
+ yield* sql `INSERT INTO flows_run_source VALUES (1, ${sql.literal(Dialect.isPostgres(sql) ? "replace(gen_random_uuid()::text, '-', '')" : "lower(hex(randomblob(16)))")}, 0)`;
23
+ yield* sql `CREATE TABLE flows_run_changes (
24
+ run_id TEXT PRIMARY KEY NOT NULL CHECK (length(run_id) > 0),
25
+ revision ${Dialect.integer(sql)} NOT NULL UNIQUE CHECK (${Dialect.isInteger(sql, sql `revision`)} AND revision > 0 AND revision <= 9007199254740991),
26
+ deleted ${Dialect.integer(sql)} NOT NULL CHECK (deleted IN (0, 1))
27
+ )`;
28
+ yield* sql `ALTER TABLE flows_runs ADD COLUMN cancel_acknowledgement_json TEXT
29
+ CHECK (cancel_acknowledgement_json IS NULL OR ${Dialect.jsonValid(sql, sql `cancel_acknowledgement_json`)})`;
30
+ // Old rows receive a real baseline revision; their prior mutation order is unknown.
31
+ yield* sql `INSERT INTO flows_run_changes (run_id, revision, deleted)
32
+ SELECT run_id, ROW_NUMBER() OVER (ORDER BY created_at_ms, run_id), 0 FROM flows_runs`;
33
+ yield* sql `UPDATE flows_run_source SET revision = (SELECT COUNT(*) FROM flows_run_changes)`;
34
+ for (const [operation, row, deleted] of [["INSERT", "NEW", 0], ["UPDATE", "NEW", 0], ["DELETE", "OLD", 1]]) {
35
+ yield* Dialect.trigger(sql, {
36
+ name: `flows_runs_revision_${operation.toLowerCase()}`,
37
+ table: "flows_runs",
38
+ event: `AFTER ${operation}`,
39
+ body: `
40
+ ${Dialect.isPostgres(sql)
41
+ ? "IF NOT EXISTS (SELECT 1 FROM flows_run_source WHERE singleton = 1) THEN RAISE EXCEPTION 'execution source identity is missing' USING ERRCODE = '23514'; END IF;"
42
+ : "SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM flows_run_source WHERE singleton = 1) THEN RAISE(ABORT, 'execution source identity is missing') END;"}
43
+ UPDATE flows_run_source SET revision = revision + 1 WHERE singleton = 1;
44
+ INSERT INTO flows_run_changes (run_id, revision, deleted)
45
+ SELECT ${row}.run_id, revision, ${deleted} FROM flows_run_source WHERE singleton = 1
46
+ ON CONFLICT (run_id) DO UPDATE SET revision = excluded.revision, deleted = excluded.deleted;
47
+ `
48
+ });
49
+ }
50
+ yield* Dialect.trigger(sql, {
51
+ name: `flows_runs_identity_immutable`,
52
+ table: `flows_runs`,
53
+ event: "BEFORE UPDATE OF run_id",
54
+ when: `NEW.run_id <> OLD.run_id`,
55
+ reject: "run identity is immutable"
56
+ });
57
+ });
58
+ //# sourceMappingURL=0003_execution_revisions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0003_execution_revisions.js","sourceRoot":"","sources":["../../../src/migrations/0003_execution_revisions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAA;AACnD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAsD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACvG,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAA;IACtC,KAAK,CAAC,CAAC,GAAG,CAAA;gBACI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;0DAEhC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,+BAA+B,CACjG;eACa,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAC/B,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,UAAU,CACtC;IACE,CAAA;IACF,KAAK,CAAC,CAAC,GAAG,CAAA,2CACR,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,2CAA2C,CAAC,CAAC,CAAC,4BAA4B,CAClH,MAAM,CAAA;IACN,KAAK,CAAC,CAAC,GAAG,CAAA;;eAEG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,2BAC/B,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,UAAU,CACtC;cACY,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;IAC9B,CAAA;IACF,KAAK,CAAC,CAAC,GAAG,CAAA;oDACwC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,6BAA6B,CAAC,GAAG,CAAA;IAC7G,oFAAoF;IACpF,KAAK,CAAC,CAAC,GAAG,CAAA;yFAC6E,CAAA;IACvF,KAAK,CAAC,CAAC,GAAG,CAAA,iFAAiF,CAAA;IAC3F,KAAK,MAAM,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAU,EAAE,CAAC;QACpH,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;YAC1B,IAAI,EAAE,uBAAuB,SAAS,CAAC,WAAW,EAAE,EAAE;YACtD,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,SAAS,SAAS,EAAE;YAC3B,IAAI,EAAE;UAEJ,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;gBACrB,CAAC,CAAC,iKAAiK;gBACnK,CAAC,CAAC,iJACN;;;mBAGa,GAAG,sBAAsB,OAAO;;OAE5C;SACF,CAAC,CAAA;IACJ,CAAC;IACD,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;QAC1B,IAAI,EAAE,+BAA+B;QACrC,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,yBAAyB;QAChC,IAAI,EAAE,0BAA0B;QAChC,MAAM,EAAE,2BAA2B;KACpC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Durable database identity and coalesced execution change tracking.\n *\n * @since 1.0.0\n */\n\nimport * as Dialect from \"@smthrs/database/Dialect\"\nimport * as Effect from \"effect/Effect\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\n\n/**\n * Retains one current revision per run ID, including deleted IDs forever.\n * Triggers participate in the mutating transaction, including raw SQL writes.\n * @category migrations\n * @since 1.0.0\n */\nexport const executionRevisions: Effect.Effect<void, unknown, SqlClient.SqlClient> = Effect.gen(function*() {\n const sql = yield* SqlClient.SqlClient\n yield* sql`CREATE TABLE flows_run_source (\n singleton ${Dialect.integer(sql)} PRIMARY KEY CHECK (singleton = 1),\n source TEXT NOT NULL CHECK (length(source) = 32 AND ${\n sql.literal(Dialect.isPostgres(sql) ? \"source !~ '[^0-9a-f]'\" : \"source NOT GLOB '*[^0-9a-f]*'\")\n }),\n revision ${Dialect.integer(sql)} NOT NULL CHECK (${\n Dialect.isInteger(sql, sql`revision`)\n } AND revision >= 0 AND revision <= 9007199254740991)\n )`\n yield* sql`INSERT INTO flows_run_source VALUES (1, ${\n sql.literal(Dialect.isPostgres(sql) ? \"replace(gen_random_uuid()::text, '-', '')\" : \"lower(hex(randomblob(16)))\")\n }, 0)`\n yield* sql`CREATE TABLE flows_run_changes (\n run_id TEXT PRIMARY KEY NOT NULL CHECK (length(run_id) > 0),\n revision ${Dialect.integer(sql)} NOT NULL UNIQUE CHECK (${\n Dialect.isInteger(sql, sql`revision`)\n } AND revision > 0 AND revision <= 9007199254740991),\n deleted ${Dialect.integer(sql)} NOT NULL CHECK (deleted IN (0, 1))\n )`\n yield* sql`ALTER TABLE flows_runs ADD COLUMN cancel_acknowledgement_json TEXT\n CHECK (cancel_acknowledgement_json IS NULL OR ${Dialect.jsonValid(sql, sql`cancel_acknowledgement_json`)})`\n // Old rows receive a real baseline revision; their prior mutation order is unknown.\n yield* sql`INSERT INTO flows_run_changes (run_id, revision, deleted)\n SELECT run_id, ROW_NUMBER() OVER (ORDER BY created_at_ms, run_id), 0 FROM flows_runs`\n yield* sql`UPDATE flows_run_source SET revision = (SELECT COUNT(*) FROM flows_run_changes)`\n for (const [operation, row, deleted] of [[\"INSERT\", \"NEW\", 0], [\"UPDATE\", \"NEW\", 0], [\"DELETE\", \"OLD\", 1]] as const) {\n yield* Dialect.trigger(sql, {\n name: `flows_runs_revision_${operation.toLowerCase()}`,\n table: \"flows_runs\",\n event: `AFTER ${operation}`,\n body: `\n ${\n Dialect.isPostgres(sql)\n ? \"IF NOT EXISTS (SELECT 1 FROM flows_run_source WHERE singleton = 1) THEN RAISE EXCEPTION 'execution source identity is missing' USING ERRCODE = '23514'; END IF;\"\n : \"SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM flows_run_source WHERE singleton = 1) THEN RAISE(ABORT, 'execution source identity is missing') END;\"\n }\n UPDATE flows_run_source SET revision = revision + 1 WHERE singleton = 1;\n INSERT INTO flows_run_changes (run_id, revision, deleted)\n SELECT ${row}.run_id, revision, ${deleted} FROM flows_run_source WHERE singleton = 1\n ON CONFLICT (run_id) DO UPDATE SET revision = excluded.revision, deleted = excluded.deleted;\n `\n })\n }\n yield* Dialect.trigger(sql, {\n name: `flows_runs_identity_immutable`,\n table: `flows_runs`,\n event: \"BEFORE UPDATE OF run_id\",\n when: `NEW.run_id <> OLD.run_id`,\n reject: \"run identity is immutable\"\n })\n})\n"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The question a parked run is waiting on an answer to.
3
+ *
4
+ * `waiting_reason` and `waiting_token` say THAT a run is parked on a human
5
+ * decision and which wait point holds it; neither says what was asked. A
6
+ * `HumanTask` park therefore left the prompt, the kind of answer it wants, and
7
+ * the attempt budget nowhere a reader could find them: the question lived only
8
+ * in the action payload of an execution that had already released its owner,
9
+ * so the approvals inbox could describe the gate as "something is waiting" and
10
+ * no more (run-3, `coding-clarification`).
11
+ *
12
+ * This column is that missing half. It is the JSON the wait declared about
13
+ * itself — for `HumanTask`, `{kind, name, prompt, attempt, maxAttempts}` — and
14
+ * it is written and cleared with the other three waiting columns, so it can
15
+ * never describe a wait the run is no longer holding.
16
+ *
17
+ * Append-only, as every migration after the first must be: the column is
18
+ * nullable, so every row an earlier build wrote stays valid and reads back as
19
+ * a park that declared no question.
20
+ *
21
+ * @since 1.0.0
22
+ */
23
+ import * as Effect from "effect/Effect";
24
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
25
+ /**
26
+ * Adds `waiting_request` to `flows_runs`.
27
+ *
28
+ * @category migrations
29
+ * @since 1.0.0
30
+ */
31
+ export declare const waitingRequest: Effect.Effect<void, unknown, SqlClient.SqlClient>;
32
+ //# sourceMappingURL=0004_waiting_request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0004_waiting_request.d.ts","sourceRoot":"","sources":["../../../src/migrations/0004_waiting_request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,SAAS,CAK3E,CAAA"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The question a parked run is waiting on an answer to.
3
+ *
4
+ * `waiting_reason` and `waiting_token` say THAT a run is parked on a human
5
+ * decision and which wait point holds it; neither says what was asked. A
6
+ * `HumanTask` park therefore left the prompt, the kind of answer it wants, and
7
+ * the attempt budget nowhere a reader could find them: the question lived only
8
+ * in the action payload of an execution that had already released its owner,
9
+ * so the approvals inbox could describe the gate as "something is waiting" and
10
+ * no more (run-3, `coding-clarification`).
11
+ *
12
+ * This column is that missing half. It is the JSON the wait declared about
13
+ * itself — for `HumanTask`, `{kind, name, prompt, attempt, maxAttempts}` — and
14
+ * it is written and cleared with the other three waiting columns, so it can
15
+ * never describe a wait the run is no longer holding.
16
+ *
17
+ * Append-only, as every migration after the first must be: the column is
18
+ * nullable, so every row an earlier build wrote stays valid and reads back as
19
+ * a park that declared no question.
20
+ *
21
+ * @since 1.0.0
22
+ */
23
+ import * as Dialect from "@smthrs/database/Dialect";
24
+ import * as Effect from "effect/Effect";
25
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
26
+ /**
27
+ * Adds `waiting_request` to `flows_runs`.
28
+ *
29
+ * @category migrations
30
+ * @since 1.0.0
31
+ */
32
+ export const waitingRequest = Effect.gen(function* () {
33
+ const sql = yield* SqlClient.SqlClient;
34
+ yield* sql `ALTER TABLE flows_runs ADD COLUMN waiting_request TEXT
35
+ CHECK (waiting_request IS NULL OR ${Dialect.jsonValid(sql, sql `waiting_request`)})`;
36
+ });
37
+ //# sourceMappingURL=0004_waiting_request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0004_waiting_request.js","sourceRoot":"","sources":["../../../src/migrations/0004_waiting_request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAA;AACnD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,+BAA+B,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnG,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAA;IAEtC,KAAK,CAAC,CAAC,GAAG,CAAA;wCAC4B,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAA,iBAAiB,CAAC,GAAG,CAAA;AACvF,CAAC,CAAC,CAAA","sourcesContent":["/**\n * The question a parked run is waiting on an answer to.\n *\n * `waiting_reason` and `waiting_token` say THAT a run is parked on a human\n * decision and which wait point holds it; neither says what was asked. A\n * `HumanTask` park therefore left the prompt, the kind of answer it wants, and\n * the attempt budget nowhere a reader could find them: the question lived only\n * in the action payload of an execution that had already released its owner,\n * so the approvals inbox could describe the gate as \"something is waiting\" and\n * no more (run-3, `coding-clarification`).\n *\n * This column is that missing half. It is the JSON the wait declared about\n * itself — for `HumanTask`, `{kind, name, prompt, attempt, maxAttempts}` — and\n * it is written and cleared with the other three waiting columns, so it can\n * never describe a wait the run is no longer holding.\n *\n * Append-only, as every migration after the first must be: the column is\n * nullable, so every row an earlier build wrote stays valid and reads back as\n * a park that declared no question.\n *\n * @since 1.0.0\n */\n\nimport * as Dialect from \"@smthrs/database/Dialect\"\nimport * as Effect from \"effect/Effect\"\nimport * as SqlClient from \"effect/unstable/sql/SqlClient\"\n\n/**\n * Adds `waiting_request` to `flows_runs`.\n *\n * @category migrations\n * @since 1.0.0\n */\nexport const waitingRequest: Effect.Effect<void, unknown, SqlClient.SqlClient> = Effect.gen(function*() {\n const sql = yield* SqlClient.SqlClient\n\n yield* sql`ALTER TABLE flows_runs ADD COLUMN waiting_request TEXT\n CHECK (waiting_request IS NULL OR ${Dialect.jsonValid(sql, sql`waiting_request`)})`\n})\n"]}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Deterministic bundle of the production run and attempt services.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Layer from "effect/Layer";
7
+ import * as AttemptStore from "../AttemptStore.ts";
8
+ import * as RunStore from "../RunStore.ts";
9
+ /**
10
+ * Provides the production SQLite run and attempt stores over an in-memory
11
+ * database. Migrations run before either service is exposed.
12
+ *
13
+ * @category layers
14
+ * @since 0.1.0
15
+ */
16
+ export declare const layer: Layer.Layer<AttemptStore.AttemptStore | RunStore.RunStore, import("effect/unstable/sql/SqlError").SqlError | import("effect/unstable/sql/Migrator").MigrationError, never>;
17
+ //# sourceMappingURL=TestRunStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestRunStore.d.ts","sourceRoot":"","sources":["../../../src/test/TestRunStore.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAElD,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,4KAEjB,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Deterministic bundle of the production run and attempt services.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as TestDatabase from "@smthrs/database/test/TestDatabase";
7
+ import * as Layer from "effect/Layer";
8
+ import * as AttemptStore from "../AttemptStore.js";
9
+ import * as Migrations from "../Migrations.js";
10
+ import * as RunStore from "../RunStore.js";
11
+ /**
12
+ * Provides the production SQLite run and attempt stores over an in-memory
13
+ * database. Migrations run before either service is exposed.
14
+ *
15
+ * @category layers
16
+ * @since 0.1.0
17
+ */
18
+ export const layer = Layer.mergeAll(RunStore.layer, AttemptStore.layer).pipe(Layer.provide(Layer.provideMerge(Migrations.layer, TestDatabase.layer)));
19
+ //# sourceMappingURL=TestRunStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestRunStore.js","sourceRoot":"","sources":["../../../src/test/TestRunStore.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAClE,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,CAC1E,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CACxE,CAAA","sourcesContent":["/**\n * Deterministic bundle of the production run and attempt services.\n *\n * @since 0.1.0\n */\n\nimport * as TestDatabase from \"@smthrs/database/test/TestDatabase\"\nimport * as Layer from \"effect/Layer\"\nimport * as AttemptStore from \"../AttemptStore.ts\"\nimport * as Migrations from \"../Migrations.ts\"\nimport * as RunStore from \"../RunStore.ts\"\n\n/**\n * Provides the production SQLite run and attempt stores over an in-memory\n * database. Migrations run before either service is exposed.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = Layer.mergeAll(RunStore.layer, AttemptStore.layer).pipe(\n Layer.provide(Layer.provideMerge(Migrations.layer, TestDatabase.layer))\n)\n"]}
package/docs/README.md ADDED
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: "@smthrs/run-store"
3
+ description: "Durable run state and fenced ownership for long-running jobs: which runs exist, what each one is doing, which process owns it, and how a restart takes one back safely."
4
+ ---
5
+
6
+ `@smthrs/run-store` keeps the live state of long-running jobs in a SQL database.
7
+ It answers two questions about every run: what is it doing right now, and which
8
+ process is allowed to touch it.
9
+
10
+ It is an [Effect](https://effect.website) library of two services, `RunStore`
11
+ and `AttemptStore`, plus the ownership arbitration that decides who holds a run.
12
+
13
+ ## What it solves
14
+
15
+ A process that owns a long job can die at any moment. Two things then go wrong.
16
+ The work is stranded, because nothing else knows the job existed or how far it
17
+ got. And picking the job back up is dangerous, because the process you assume is
18
+ dead may still be alive and writing.
19
+
20
+ This package handles both in the database rather than in your code:
21
+
22
+ - **One row is the authority.** `RunStore` keeps a row per run holding its
23
+ status, its owner, its heartbeat, whether cancellation was requested, and the
24
+ executable state a resume re-enters. A restarted process reads that row
25
+ instead of replaying a log to work out where it was.
26
+ - **Every owned write carries a fence.** An owner identity is
27
+ `{ hostId, pid, nonce }`, and all three fields are compared inside the same
28
+ SQL statement as the mutation they guard. There is no window between checking
29
+ ownership and using it, so two processes cannot both win.
30
+ - **Competition is a value, not an error.** Losing a race returns
31
+ `AlreadyClaimed`, `HeartbeatFresh`, or `FenceLost` as an ordinary success
32
+ value you branch on. The error channel is reserved for real defects such as an
33
+ invalid argument or a corrupt row.
34
+ - **Step attempts inherit the run's fence.** `AttemptStore` records when each
35
+ execution of a step started, the checkpoints it wrote, and how it ended, and
36
+ refuses every write from a process that no longer owns the run.
37
+
38
+ The stores carry no database of their own. They are written against the
39
+ driver-neutral [`@smthrs/database`](/api/database) contract, so the same code
40
+ runs over a local SQLite file, over a server, or over an in-memory database in a
41
+ test.
42
+
43
+ ## Install
44
+
45
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
46
+
47
+ Node.js 26.4.0 or later. [Installation](./installation.md) covers the import
48
+ forms and the two services a composition has to supply.
49
+
50
+ ## Take a run, do the work, settle it
51
+
52
+ This program creates a run, claims ownership of it, and finishes it. The stores
53
+ are the production ones. `TestRunStore.layer` provides them over a migrated
54
+ in-memory database, so the file runs with no configuration:
55
+
56
+ ```ts
57
+ import { RunStore } from "@smthrs/run-store"
58
+ import type { OwnerId } from "@smthrs/run-store/Ownership"
59
+ import * as TestRunStore from "@smthrs/run-store/test/TestRunStore"
60
+ import * as Clock from "effect/Clock"
61
+ import * as Effect from "effect/Effect"
62
+
63
+ const owner: OwnerId = { hostId: "worker-1", pid: 4102, nonce: "9c31-af02" }
64
+
65
+ const program = Effect.gen(function*() {
66
+ const runs = yield* RunStore.RunStore
67
+
68
+ yield* runs.create("build-42", JSON.stringify({ step: "checkout" }))
69
+
70
+ const nowMs = yield* Clock.currentTimeMillis
71
+ const taken = yield* runs.claimAndOwn(
72
+ "build-42",
73
+ { status: "pending", owner: null, heartbeatAtMs: null },
74
+ owner,
75
+ nowMs
76
+ )
77
+ if (taken._tag !== "Activated") return `another process has it: ${taken._tag}`
78
+
79
+ // Do the work here, heartbeating while it runs.
80
+
81
+ const settled = yield* runs.transitionOwned(
82
+ "build-42",
83
+ owner,
84
+ "completed",
85
+ JSON.stringify({ step: "done" })
86
+ )
87
+ return `run ended as ${settled._tag}`
88
+ })
89
+
90
+ console.log(
91
+ await Effect.runPromise(
92
+ program.pipe(Effect.provide(TestRunStore.layer), Effect.scoped)
93
+ )
94
+ )
95
+ ```
96
+
97
+ ```text
98
+ run ended as Transitioned
99
+ ```
100
+
101
+ The second argument to `claimAndOwn` is the row as you last read it, restated as
102
+ the three fields a claim guards. The third argument is the new owner. The claim
103
+ is admitted only while the row still matches. If a peer took the run between
104
+ your read and your write, this caller loses the race and leaves the peer's
105
+ ownership unchanged. The example then returns before calling `transitionOwned`.
106
+
107
+ ## How this relates to @smthrs/flows
108
+
109
+ `@smthrs/run-store` is one of the storage services inside
110
+ [`@smthrs/flows`](/api/flows), the package that carries the whole Smithers
111
+ durable flow engine in a single dependency. When you write a flow with
112
+ `@smthrs/flows` and run it on a Node host, this package is the code deciding
113
+ which process owns your run and what a restart re-enters. Flow authors never
114
+ call it.
115
+
116
+ Reach for `@smthrs/run-store` directly when you are building the host rather
117
+ than the flow: a scheduler, a worker pool, or any service that hands durable
118
+ work between processes and needs ownership arbitration it can trust. Most hosts
119
+ do not wire it by hand even then.
120
+ [`@smthrs/engine-store`](/api/engine-store) already composes these stores with
121
+ the journal, the step cache, and the engine's own state into one storage ladder,
122
+ and `@smthrs/flows/NodeRuntime` builds that ladder over a single SQLite file.
123
+
124
+ Above all of it sits [`smithers`](/api/cli), the command line that plans, runs,
125
+ and inspects durable flows without your writing a host at all. Start there if
126
+ you want to run flows rather than build the machinery underneath them.
127
+
128
+ ## Where to go next
129
+
130
+ - [Quickstart](./quickstart.md) drives one run through its whole lifecycle,
131
+ including a step attempt, against an in-memory database.
132
+ - [Fencing and ownership](./concepts/fencing.md) explains the compare-and-swap
133
+ every write goes through, and [the heartbeat lease](./concepts/leases.md)
134
+ explains how a run is judged stale.
135
+ - [Claim a run and finish it](./guides/claim-and-finish-a-run.md) and
136
+ [take over a stalled run](./guides/recover-a-stalled-run.md) are the two
137
+ paths a host has to get right.
138
+ - [Compose the stores into a host](./guides/compose-the-stores.md) gives the
139
+ layer order that satisfies both services.
140
+ - [API reference](./api.md) documents every export.
141
+ - [Troubleshooting](./troubleshooting.md) lists each refused outcome and typed
142
+ error, and what to change.