@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.
- package/CHANGELOG.md +112 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/cjs/AttemptStore.d.ts +359 -0
- package/dist/cjs/AttemptStore.d.ts.map +1 -0
- package/dist/cjs/AttemptStore.js +731 -0
- package/dist/cjs/AttemptStore.js.map +7 -0
- package/dist/cjs/Heartbeat.d.ts +46 -0
- package/dist/cjs/Heartbeat.d.ts.map +1 -0
- package/dist/cjs/Heartbeat.js +28 -0
- package/dist/cjs/Heartbeat.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +42 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +55 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/Ownership.d.ts +275 -0
- package/dist/cjs/Ownership.d.ts.map +1 -0
- package/dist/cjs/Ownership.js +129 -0
- package/dist/cjs/Ownership.js.map +7 -0
- package/dist/cjs/RunStore.d.ts +571 -0
- package/dist/cjs/RunStore.d.ts.map +1 -0
- package/dist/cjs/RunStore.js +1079 -0
- package/dist/cjs/RunStore.js.map +7 -0
- package/dist/cjs/RunStoreMetrics.d.ts +151 -0
- package/dist/cjs/RunStoreMetrics.d.ts.map +1 -0
- package/dist/cjs/RunStoreMetrics.js +91 -0
- package/dist/cjs/RunStoreMetrics.js.map +7 -0
- package/dist/cjs/index.d.ts +47 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +43 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/Boundary.d.ts +80 -0
- package/dist/cjs/internal/Boundary.d.ts.map +1 -0
- package/dist/cjs/internal/Boundary.js +92 -0
- package/dist/cjs/internal/Boundary.js.map +7 -0
- package/dist/cjs/internal/Companion.d.ts +44 -0
- package/dist/cjs/internal/Companion.d.ts.map +1 -0
- package/dist/cjs/internal/Companion.js +50 -0
- package/dist/cjs/internal/Companion.js.map +7 -0
- package/dist/cjs/internal/SpanOutcome.d.ts +37 -0
- package/dist/cjs/internal/SpanOutcome.d.ts.map +1 -0
- package/dist/cjs/internal/SpanOutcome.js +62 -0
- package/dist/cjs/internal/SpanOutcome.js.map +7 -0
- package/dist/cjs/migrations/0001_initial.d.ts +15 -0
- package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_initial.js +110 -0
- package/dist/cjs/migrations/0001_initial.js.map +7 -0
- package/dist/cjs/migrations/0002_lineage.d.ts +27 -0
- package/dist/cjs/migrations/0002_lineage.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_lineage.js +43 -0
- package/dist/cjs/migrations/0002_lineage.js.map +7 -0
- package/dist/cjs/migrations/0003_execution_revisions.d.ts +15 -0
- package/dist/cjs/migrations/0003_execution_revisions.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_execution_revisions.js +77 -0
- package/dist/cjs/migrations/0003_execution_revisions.js.map +7 -0
- package/dist/cjs/migrations/0004_waiting_request.d.ts +32 -0
- package/dist/cjs/migrations/0004_waiting_request.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_waiting_request.js +42 -0
- package/dist/cjs/migrations/0004_waiting_request.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestRunStore.d.ts +17 -0
- package/dist/cjs/test/TestRunStore.d.ts.map +1 -0
- package/dist/cjs/test/TestRunStore.js +42 -0
- package/dist/cjs/test/TestRunStore.js.map +7 -0
- package/dist/esm/AttemptStore.d.ts +359 -0
- package/dist/esm/AttemptStore.d.ts.map +1 -0
- package/dist/esm/AttemptStore.js +835 -0
- package/dist/esm/AttemptStore.js.map +1 -0
- package/dist/esm/Heartbeat.d.ts +46 -0
- package/dist/esm/Heartbeat.d.ts.map +1 -0
- package/dist/esm/Heartbeat.js +46 -0
- package/dist/esm/Heartbeat.js.map +1 -0
- package/dist/esm/Migrations.d.ts +42 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +56 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/Ownership.d.ts +275 -0
- package/dist/esm/Ownership.d.ts.map +1 -0
- package/dist/esm/Ownership.js +299 -0
- package/dist/esm/Ownership.js.map +1 -0
- package/dist/esm/RunStore.d.ts +571 -0
- package/dist/esm/RunStore.d.ts.map +1 -0
- package/dist/esm/RunStore.js +1287 -0
- package/dist/esm/RunStore.js.map +1 -0
- package/dist/esm/RunStoreMetrics.d.ts +151 -0
- package/dist/esm/RunStoreMetrics.d.ts.map +1 -0
- package/dist/esm/RunStoreMetrics.js +120 -0
- package/dist/esm/RunStoreMetrics.js.map +1 -0
- package/dist/esm/index.d.ts +47 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +47 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/Boundary.d.ts +80 -0
- package/dist/esm/internal/Boundary.d.ts.map +1 -0
- package/dist/esm/internal/Boundary.js +109 -0
- package/dist/esm/internal/Boundary.js.map +1 -0
- package/dist/esm/internal/Companion.d.ts +44 -0
- package/dist/esm/internal/Companion.d.ts.map +1 -0
- package/dist/esm/internal/Companion.js +53 -0
- package/dist/esm/internal/Companion.js.map +1 -0
- package/dist/esm/internal/SpanOutcome.d.ts +37 -0
- package/dist/esm/internal/SpanOutcome.d.ts.map +1 -0
- package/dist/esm/internal/SpanOutcome.js +48 -0
- package/dist/esm/internal/SpanOutcome.js.map +1 -0
- package/dist/esm/migrations/0001_initial.d.ts +15 -0
- package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
- package/dist/esm/migrations/0001_initial.js +88 -0
- package/dist/esm/migrations/0001_initial.js.map +1 -0
- package/dist/esm/migrations/0002_lineage.d.ts +27 -0
- package/dist/esm/migrations/0002_lineage.d.ts.map +1 -0
- package/dist/esm/migrations/0002_lineage.js +33 -0
- package/dist/esm/migrations/0002_lineage.js.map +1 -0
- package/dist/esm/migrations/0003_execution_revisions.d.ts +15 -0
- package/dist/esm/migrations/0003_execution_revisions.d.ts.map +1 -0
- package/dist/esm/migrations/0003_execution_revisions.js +58 -0
- package/dist/esm/migrations/0003_execution_revisions.js.map +1 -0
- package/dist/esm/migrations/0004_waiting_request.d.ts +32 -0
- package/dist/esm/migrations/0004_waiting_request.d.ts.map +1 -0
- package/dist/esm/migrations/0004_waiting_request.js +37 -0
- package/dist/esm/migrations/0004_waiting_request.js.map +1 -0
- package/dist/esm/test/TestRunStore.d.ts +17 -0
- package/dist/esm/test/TestRunStore.d.ts.map +1 -0
- package/dist/esm/test/TestRunStore.js +19 -0
- package/dist/esm/test/TestRunStore.js.map +1 -0
- package/docs/README.md +142 -0
- package/docs/api.md +901 -0
- package/docs/concepts/durable-values.md +140 -0
- package/docs/concepts/execution-revisions.md +53 -0
- package/docs/concepts/fencing.md +146 -0
- package/docs/concepts/leases.md +154 -0
- package/docs/concepts/liveness-evidence.md +142 -0
- package/docs/guides/cancel-a-run.md +156 -0
- package/docs/guides/claim-and-finish-a-run.md +175 -0
- package/docs/guides/compose-the-stores.md +142 -0
- package/docs/guides/observe-outcomes.md +182 -0
- package/docs/guides/record-step-attempts.md +170 -0
- package/docs/guides/recover-a-stalled-run.md +173 -0
- package/docs/guides/testing.md +141 -0
- package/docs/installation.md +109 -0
- package/docs/quickstart.md +201 -0
- package/docs/troubleshooting.md +255 -0
- package/package.json +183 -3
- package/src/AttemptStore.ts +1195 -0
- package/src/Heartbeat.ts +47 -0
- package/src/Migrations.ts +59 -0
- package/src/Ownership.ts +407 -0
- package/src/RunStore.ts +2063 -0
- package/src/RunStoreMetrics.ts +165 -0
- package/src/index.ts +51 -0
- package/src/internal/Boundary.ts +130 -0
- package/src/internal/Companion.ts +64 -0
- package/src/internal/SpanOutcome.ts +71 -0
- package/src/migrations/0001_initial.ts +114 -0
- package/src/migrations/0002_lineage.ts +38 -0
- package/src/migrations/0003_execution_revisions.ts +69 -0
- package/src/migrations/0004_waiting_request.ts +39 -0
- 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.
|