@voltro/workflow 0.80.0 → 0.81.0
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 +90 -0
- package/dist/{cluster-DJittOqj.js → cluster-D7VuRhM0.js} +80 -69
- package/dist/cluster.d.ts +23 -0
- package/dist/cluster.js +2 -2
- package/dist/clusterTestSuite.d.ts +8 -0
- package/dist/clusterTestSuite.js +3 -3
- package/dist/index.d.ts +22 -0
- package/dist/index.js +4 -4
- package/dist/{primitives-bv2JHUfk.js → primitives-BJBn8tiH.js} +83 -77
- package/dist/primitives.js +2 -2
- package/dist/{src-C8oxXvy6.js → src-D_GRCNhv.js} +44 -44
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,96 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.81.0] — 2026-09-25
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **The compose baseline gives each api app its own database** — `@voltro/cli`
|
|
47
|
+
|
|
48
|
+
`voltro baseline sync` now gives every api app in a `project.json` its own database, login and migrate service. The primary api (the app named `api`, else the first api app) keeps the project's database; every other api app connects to `<project>_<app>` with `<PROJECT>_<APP>_DB_PASSWORD`, and each api app gets `VOLTRO_API_<APP>_URL` for its sibling api apps by service name. Migration: sync, move each secondary app's own tables into its new database, and set the new password variables in production.
|
|
49
|
+
|
|
50
|
+
**`voltro update` carries you across this** — codemod `0.81.0/03_compose-baseline-one-database-per-api-app`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
51
|
+
- **A crashed process blocks its idempotency key for 30 seconds, not a day** — `@voltro/protocol`, `@voltro/runtime`, `@voltro/client`, `@voltro/voltro`
|
|
52
|
+
|
|
53
|
+
A process that died inside an idempotent mutation left its key `in_flight` for the whole replay window — 24 hours by default — so every retry with that key was refused with `MutationInFlight` (REST: `409`), including the retry of a workflow step whose callee had simply restarted. A running claim now holds on a 30-second lease that the server renews while the handler runs, over the socket and on REST alike; a dead process stops renewing, and the next send with the key takes over and runs the mutation once — the dead call's transaction never committed. A completed response still replays for the whole window. A takeover is conditional on the claim still being stale, so a claim renewed a moment earlier is never taken from a live handler. `ctx.apis` calls treat `MutationInFlight` as no answer yet: the same key is re-sent when the callee says, until it answers or the call's deadline passes (then `ApiUnavailable`). BREAKING only for an app that implements `IdempotencyStore` itself: `claim` takes the lease, and `renew(scope, key, now)` is required.
|
|
54
|
+
|
|
55
|
+
**`voltro update` carries you across this** — codemod `0.81.0/05_idempotency-store-renews-its-lease`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
56
|
+
- **Each api app owns its database schema; a second app on it refuses to boot** — `@voltro/database`, `@voltro/cli`
|
|
57
|
+
|
|
58
|
+
The first boot that migrates a database records which app owns it (`_voltro_schema_owner`). A different app booting against the same database and schema now refuses — naming both apps — before it plans anything, under `voltro dev`, `voltro serve` and `VOLTRO_AUTO_MIGRATE=0` alike; `voltro db apply` refuses the same way and claims an unclaimed database — as its first write, after every other check has passed, so an apply refused for drift or a blocked operation leaves the database as it found it. `voltro data` export and import treat the owner row as the deployment's own: it never travels to another database. Two apps on one schema each diffed it against their own declarations and planned to drop the other's tables. Migration: give every api app its own database (`DB_URL`) or schema (`DB_SCHEMA`) and reach the other app's data through `apis:` and `service:` procedures; `voltro doctor` lists api apps of a project that resolve the same target. `voltro db claim --yes` hands a schema to a RENAMED app.
|
|
59
|
+
|
|
60
|
+
**`voltro update` carries you across this** — codemod `0.81.0/01_one-database-per-app`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
61
|
+
- **ReactionDefinition is the union of the table form and the query form** — `@voltro/runtime`, `@voltro/voltro`
|
|
62
|
+
|
|
63
|
+
With reactions that watch a live query's rows, `ReactionDefinition` is now `EdgeReactionDefinition | LevelReactionDefinition`. Code holding a value of that type and reading a member only the table form has — `watch.table`, `watch.on`, `guards.dedupeKey`, `when(event)` — no longer compiles: type it `EdgeReactionDefinition`, or narrow with `isLevelReaction(def)` first. `defineReaction({ … })` is unchanged: an object literal resolves to its own form, and a value typed as the union is still accepted and validated by its `watch`.
|
|
64
|
+
|
|
65
|
+
**`voltro update` carries you across this** — codemod `0.81.0/04_reaction-definition-is-a-union`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
66
|
+
- **Store calls on a table the app does not declare now fail** — `@voltro/runtime`, `@voltro/cli`
|
|
67
|
+
|
|
68
|
+
Every store verb — in handlers, startups, schedules and workflows — now fails with `UndeclaredTableAccess` on a table that is neither declared by the app (its `table()`s, a plugin's `extendSchema.tables`) nor framework-owned (`_voltro_*`), in every environment. A string table name used to reach another app's table past its guards, audit and tenant rules. `store.raw(sql)` is unchanged. Migration: call the owning app through `apis:` and a `service:` procedure, or declare the table if it is this app's own.
|
|
69
|
+
|
|
70
|
+
**`voltro update` carries you across this** — codemod `0.81.0/02_store-refuses-undeclared-tables`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.81.0).
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **A plugin can bring app-shaped building blocks with `sources`** — `@voltro/protocol`, `@voltro/cli`
|
|
75
|
+
|
|
76
|
+
`definePlugin({ sources: fileURLToPath(new URL('./app', import.meta.url)) })` points at a directory of the same file conventions an app uses — queries and mutations with their executors, `schema.ts` tables, `*.workflow.tsx`, `*.cron.tsx`, subscribers, reactions — and every app that installs the plugin discovers them exactly as it discovers its own: the same codegen, browser-safety guard, access gate, migrations, and both boot paths. A reusable building block (a ledger, an audit trail, an approval flow) is shared between apps of a project without copying it; each installing app gets its own copy of the block's tables in its own database. `voltro build` keys the block's modules by plugin and path, so the serve bundle finds them wherever the block lives after a deploy.
|
|
77
|
+
- **A reaction can watch the rows of a live query, including another app's** — `@voltro/runtime`, `@voltro/cli`
|
|
78
|
+
|
|
79
|
+
`defineReaction({ watch: { query: 'deployments.pending', api: 'cloud' }, key: (row) => row.id, act: { kind: 'workflow', workflow: 'deploy.run' } })` reacts to every ROW of a live query's current result — the set of open work — instead of to each change of a table. Every delivery is the whole set, so a row that appeared while the reacting process was down, restarting or deploying is acted on when it comes back; nothing is lost. The workflow's `idempotencyKey` makes each row's act exactly-once across every replica, so no claim table is involved and a start that failed is tried again on the next delivery or on `resync` (default 5 minutes). `resync` and `stuckAfter` take milliseconds or a duration from `'30s'` to `'8 days'`. With `api:` the query is another app's `service:` query, subscribed over `ctx.apis` — push without the watched app knowing its watcher; without it, one of this app's own queries, read through the same guards and row filter as its REST projection. A row still in the set long after its workflow started is reported as `stuck` and never restarted. `voltro_reaction_pending`, `voltro_reaction_oldest_seconds` and `voltro_reaction_connected` expose the queue per reaction. The table form (`watch: { table }`) is unchanged; `runReaction` now takes the table form's type, `EdgeReactionDefinition`.
|
|
80
|
+
- **Apps of one project call each other through typed service procedures** — `@voltro/voltro`, `@voltro/protocol`, `@voltro/runtime`, `@voltro/client`, `@voltro/cli`, `@voltro/testing`, `@voltro/workflow`
|
|
81
|
+
|
|
82
|
+
An api app can now call another app of the same project, typed, over the socket, without touching its tables. The called app declares `service: { callers: ['worker'] }` on a query, mutation or action and lists the caller under `serviceCallers: { worker: {} }`; codegen emits those procedures into `serviceGroup.generated.ts` and never into the browser's `rpcGroup.generated.ts`. The calling app binds it under `apis: { cloud: { package: '@acme/api' } }` and calls `ctx.apis.cloud.deployments.activate(input)` (Promise) or `.effect(input)` (Effect), with every input, output and typed error inferred from the callee's declarations. A service authenticates with a per-edge credential (`VOLTRO_API_<BINDING>_TOKEN` on the caller, `VOLTRO_SERVICE_CALLER_<CALLER>_TOKEN` on the callee, `_NEXT` for rotation); `voltro dev` derives and mints it into both apps' `.env.local`, production supplies it (`voltro secret generate service`) and a missing one fails boot. A caller name is letters, digits and dashes, at most 56 characters, so its `service:<name>` identity fits an `id()` column. The called app plants an `actors` row for each accepted caller at boot (`service:<name>`, `kind: 'serviceAccount'`), so `audit()`-stamped writes name the calling app instead of failing on the foreign key. A service subject holds no scopes: it reaches exactly the procedures that name it, is refused everywhere else — framework built-ins included — and no user, API key or `admin:full` session reaches a service procedure. A write made inside a workflow step carries an idempotency key derived from the step, so a step retried after a lost reply replays instead of writing twice; an app with `serviceCallers` deduplicates mutations by default. A call without an answer fails with `ApiUnavailable` within a deadline (10 s by default); calls are measured as `voltro_api_call_duration_seconds` and `voltro_api_call_errors_total`. `voltro doctor` reports a caller listed without a procedure, a procedure naming an unlisted caller and a binding the callee does not accept. `@voltro/testing` adds `makeServiceApiMock`, which decodes every input and output through the callee's real schemas.
|
|
83
|
+
|
|
84
|
+
`apiSurface: compatible` — the replaced golden lines in `@voltro/protocol` (and the `@voltro/voltro` aggregate re-exporting it) are `AnyCheckSpec` and `DeclaredAccess` gaining the member `ServiceAccessSpec`. Every existing member is unchanged and an app keeps writing the guards it wrote; the framework is the only producer of the new member, which it normalises from `service:`. Only a switch exhaustive over guard specs needs an arm.
|
|
85
|
+
- **startTestApps boots several apps of one project, each service edge wired** — `@voltro/testing`
|
|
86
|
+
|
|
87
|
+
`startTestApps({ apps, edges })` from `@voltro/testing` boots the apps of one project as real `voltro` processes (`dev` by default, `serve` with `mode: 'serve'`), each on a free port — every callee before its callers, because a caller imports the contract its callee's codegen writes — and resolves once every app answers `/internal/readiness`; a cycle of edges is refused before anything starts. Every listed edge gets a freshly minted credential written to both ends, under the variables the apps read (`VOLTRO_API_<BINDING>_TOKEN` / `_URL` on the caller, `VOLTRO_SERVICE_CALLER_<CALLER>_TOKEN` on the callee); `apps.token(caller, callee)` returns it so a test can call the callee as the caller. An app that exits or is not ready in time stops the others and the call rejects with its log. Processes rather than one process, because apps with their own tables cannot share one process's table registry.
|
|
88
|
+
- **makeTestApp requests carry the caller's address** — `@voltro/testing`
|
|
89
|
+
|
|
90
|
+
A route that reads `ctx.remoteAddr` (a per-client rate limit, an audit field) could not be tested per client: every `makeTestApp` request arrived without an address. A request's options now take `remoteAddr`, the address the connection comes from, defaulting to `127.0.0.1`. The route sees it as `ctx.remoteAddr`, resolved by the function the server's listener uses, and a `publicApi` procedure's plugin interceptors see the same address, as on the server. `makeTestApp({ security: { trustedProxies, clientAddressHeader } })` takes the api's own settings, so a forwarding header is believed in a test exactly when it would be in production.
|
|
91
|
+
|
|
92
|
+
`markRestInvocation` takes the request's address as an optional third argument, and `restInvocationOf` returns it beside the spec; both only gain an optional member, so existing calls and reads are unchanged.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- **Codegen removes an rpc group the app no longer has procedures for** — `@voltro/cli`
|
|
97
|
+
|
|
98
|
+
An app whose last client-reachable procedure went away — deleted, or marked `internal: true` — kept its `rpcGroup.generated.ts`: codegen reported "no rpc primitives found — nothing to write" and left the old file, a typed contract for procedures the server no longer served. The next `voltro dev` or `voltro codegen` now removes it, as it already removed an empty `serviceGroup.generated.ts` and `apis.generated.ts`. A hand-written file of that name is left alone.
|
|
99
|
+
- **The auto-launched dashboard binds `VOLTRO_DASHBOARD_PORT`** — `@voltro/cli`
|
|
100
|
+
|
|
101
|
+
`VOLTRO_DASHBOARD_PORT` was read by `voltro dashboard` only. The dashboard that `voltro dev` and `voltro start` launch on their own — api and web app alike — always came up on `:5179`, and the launcher wrote `5179` into the child's `VOLTRO_DASHBOARD_PORT`, overriding the value the developer had set. The launcher and `voltro dashboard` now resolve the port through one reader: the variable, else `5179`. A value that is not a port (`1..65535`) is refused with a warning naming it instead of silently becoming `5179`.
|
|
102
|
+
- **The db pool boot line counts a connection outside the pool once** — `@voltro/cli`
|
|
103
|
+
|
|
104
|
+
With change data capture on, the line read `max=5 … + 1 = 6` and then, separately, `PLUS 1 outside the pool (CDC LISTEN consumer)`, which reads as 7 when the process holds 6. The outside connection is now named once, inside the sum it belongs to: `max=5 per replica (DB_MAX_CONNECTIONS) + 1 outside the pool (CDC LISTEN consumer) = 6 per process`.
|
|
105
|
+
- **A declared `utf8mb4` store reaches the workflow engine's tables** — `@voltro/database`, `@voltro/workflow`, `@voltro/cli`
|
|
106
|
+
|
|
107
|
+
`store: { charset: 'utf8mb4' }` converted every table the app declares and left the tables the framework creates without declaring them in the database default: the durable workflow engine's `cluster_*` storage, which holds every workflow call's input and result as JSON text, and the deploy runner's `_voltro_deploys` / `_voltro_migration_ops`. On a `utf8mb3` database a workflow whose input carried an emoji was refused there. Every table the framework creates is now created in the declared charset: the runner's ledgers directly, and the workflow engine's tables by the process that creates them on its first boot, while they are still empty. A workflow table a booting process finds is never rewritten at boot; tables from before the declaration are converted by `voltro db apply` (also when the plan has nothing to do, and with `--plan`) once the deploy has closed and by the `voltro dev` auto-migrate, keeping type, nullability and column CHECKs, and `voltro db plan` lists them beside the plan (under `--json` too, on the log, never in the plan file). The database's own default is left alone. The step only ever widens, and a deploy that fails and is undone never runs it.
|
|
108
|
+
- **The MySQL/MariaDB pool keeps its connections instead of reconnecting** — `@voltro/sql-mysql`
|
|
109
|
+
|
|
110
|
+
The driver pool was created with `maxIdle: 0`, so every connection released to it was closed within a second. A process reconnected after each second of quiet: a TCP handshake per burst, plus a TLS one where configured. Measured on MySQL and MariaDB: a query after two quiet seconds ran on a new connection. The pool now keeps the connections it opened, up to the limit.
|
|
111
|
+
- **Workflows started by a reaction or an incoming webhook start again** — `@voltro/cli`
|
|
112
|
+
|
|
113
|
+
A workflow started by a `*.reaction.ts` act or an incoming `*.webhook.tsx` was refused with `Workflow placement requires explicit caller identity`, under `voltro dev` and `voltro serve`: neither path named a caller, and placement refuses a start without one. Both now start as the framework itself, placed in the changed row's tenant when it has one.
|
|
114
|
+
- **The first boot on an empty database no longer warns about replica observations** — `@voltro/cli`
|
|
115
|
+
|
|
116
|
+
`voltro dev` started publishing this replica's observations before its auto-migrate, so the first write on an empty database hit a table that did not exist yet and printed `could not record this replica's observation — fleet views will report this replica as stale` on the first boot of every new project. The publisher now writes its first observation once the schema is applied — at once on `voltro serve`, whose fingerprint check runs before it — and never when the boot stops earlier.
|
|
117
|
+
- **A 405 on a shared REST path names every method the path serves** — `@voltro/protocol`
|
|
118
|
+
|
|
119
|
+
A `DELETE` to a path served by `GET` and `POST` answered `405` with `Allow: POST`: each route refused with its own method, and the last refusal won. `Allow` and the error body's `allow` now list every method declared on the path, `GET, POST`, as the REST documentation already promised.
|
|
120
|
+
- **A large REST surface no longer holds the boot for seconds registering routes** — `@voltro/runtime`, `@voltro/protocol`
|
|
121
|
+
|
|
122
|
+
Every REST route used to be mounted for all 34 HTTP methods the router knows, at its path and at its sub-path, and each insert compares itself against every entry already held. A public REST surface of about 1000 procedures therefore held about 34 000 router entries, and building them kept the thread busy for over a second after the port was bound — long enough for a liveness probe to time out. A path is now mounted for exactly the methods its routes declare: the same 1000 procedures hold 4 000 entries. Behaviour is unchanged. A method the path does not declare still reaches its routes and gets the precise `405` with `Allow`, a path nobody declared is still a `404`, and `HEAD` still answers from `GET`. For this, a REST route now declares its real `method` instead of `'*'` and sets the new `PluginHttpRoute.answersOtherMethods: true`. A plugin route that declares one method without that flag never sees another method, as before. A server of your own that mounts `restRoutesToHttpRoutes` output by method must hand the other methods to routes with `answersOtherMethods`, or they become `404` instead of `405`.
|
|
123
|
+
- **The testing guide's tsconfig example no longer sets baseUrl** — `@voltro/cli`
|
|
124
|
+
|
|
125
|
+
The `paths` alias example in the testing guide set `"baseUrl": "."`, an option TypeScript 7 removed. It now maps `paths` alone, which resolves against the tsconfig's own directory. `voltro test` still reads a `baseUrl` an older config sets.
|
|
126
|
+
- **voltro update lists a note's code and config lines before prose** — `@voltro/cli`
|
|
127
|
+
|
|
128
|
+
A manual note lists the lines it found in code and config first, under `── found in code or config (N lines)`. Lines in Markdown and text files follow in their own `── mentioned in prose (N lines)` block, and a note that matched only prose says `nothing in code or config matched`. Past the lines shown, the rest are named by file with a count instead of a bare `… and N more`. Before, the lines were sorted by path, and 40 lines of documentation hid a Helm env entry, the one line to change. The guide `voltro agents-md` copies into `agent-docs/` is no longer searched. It names every change of a release, so refreshing it made notes report "found" for changes the project never used.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
42
132
|
## [0.80.0] — 2026-09-24
|
|
43
133
|
|
|
44
134
|
### ⚠ BREAKING
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { a as e, l as t, n, o as r, t as i, u as a } from "./clusterDiagnostics-Cwt-WFec.js";
|
|
2
2
|
import { Context as o, Duration as s, Effect as c, Layer as l, Option as u, Runtime as d, Schema as f } from "effect";
|
|
3
3
|
import { SqlClient as p } from "@effect/sql";
|
|
4
|
-
import { ClusterCron as m, ClusterSchema as h, ClusterWorkflowEngine as g, EntityAddress as _, EntityId as
|
|
5
|
-
import { fnv1a64 as
|
|
6
|
-
import { createLogger as
|
|
4
|
+
import { ClusterCron as m, ClusterSchema as h, ClusterWorkflowEngine as g, EntityAddress as _, EntityId as ee, EntityType as te, MessageStorage as ne, RunnerAddress as v, ShardId as re, Sharding as y, ShardingConfig as ie, SingleRunner as ae, SqlMessageStorage as oe, SqlRunnerStorage as se } from "@effect/cluster";
|
|
5
|
+
import { applyUndeclaredCharsetWork as b, fnv1a64 as x, readUndeclaredCharsetColumns as ce, resolveMigrationLockSchema as S, sqlPoolCapacity as le, undeclaredCharsetWork as ue } from "@voltro/database/sql";
|
|
6
|
+
import { createLogger as de } from "@voltro/logger";
|
|
7
7
|
//#region src/workflowJournalReset.ts
|
|
8
|
-
var
|
|
8
|
+
var C = f.parseJson(f.Struct({
|
|
9
9
|
_tag: f.Literal("Success"),
|
|
10
10
|
value: f.Struct({ _tag: f.Literal("Suspended") })
|
|
11
|
-
})),
|
|
11
|
+
})), fe = f.parseJson(f.Record({
|
|
12
12
|
key: f.String,
|
|
13
13
|
value: f.String
|
|
14
|
-
})),
|
|
14
|
+
})), pe = (e) => c.gen(function* () {
|
|
15
15
|
if (!/^[1-9]\d*$/.test(e.requestId) || e.workflowName.length === 0 || e.executionId.length === 0) return yield* c.die(/* @__PURE__ */ Error("Suspension observation requires an exact engine delivery owner"));
|
|
16
16
|
let [t] = yield* (yield* p.SqlClient).withoutTransforms()`
|
|
17
17
|
SELECT m.entity_type, m.entity_id, m.tag, m.headers, r.id AS reply_id, r.payload
|
|
@@ -20,13 +20,13 @@ var w = f.parseJson(f.Struct({
|
|
|
20
20
|
WHERE m.id = ${e.requestId} AND m.kind = 0`;
|
|
21
21
|
if (t === void 0) return;
|
|
22
22
|
if (t.entity_type !== `Workflow/${e.workflowName}` || t.entity_id !== e.executionId || t.tag !== "run") return yield* c.die(/* @__PURE__ */ Error("Suspension observation belongs to another execution or procedure"));
|
|
23
|
-
if (((yield* c.sync(() => f.decodeUnknownSync(
|
|
23
|
+
if (((yield* c.sync(() => f.decodeUnknownSync(fe)(t.headers)))["voltro-workflow-restart"] ?? null) !== e.restartToken || t.reply_id === null || f.decodeUnknownOption(C)(t.payload)._tag === "None") return;
|
|
24
24
|
let n = String(t.reply_id);
|
|
25
25
|
return /^[1-9]\d*$/.test(n) ? {
|
|
26
26
|
...e,
|
|
27
27
|
replyId: n
|
|
28
28
|
} : yield* c.die(/* @__PURE__ */ Error("Suspension reply has an invalid identity"));
|
|
29
|
-
}).pipe(c.provideService(p.SafeIntegers, !0)),
|
|
29
|
+
}).pipe(c.provideService(p.SafeIntegers, !0)), me = (t, n) => c.gen(function* () {
|
|
30
30
|
let { restart: r, suspension: i } = n, a = /* @__PURE__ */ new Map();
|
|
31
31
|
for (let e of t) {
|
|
32
32
|
if (!/^[1-9]\d*$/.test(e.requestId) || !/^[1-9]\d*$/.test(e.replyId)) return yield* c.die(/* @__PURE__ */ Error("Journal reset requires positive decimal request and reply identities"));
|
|
@@ -101,7 +101,7 @@ var w = f.parseJson(f.Struct({
|
|
|
101
101
|
};
|
|
102
102
|
let r = yield* s`SELECT payload FROM ${u}
|
|
103
103
|
WHERE id = ${n} AND request_id = ${e} AND kind = 0`;
|
|
104
|
-
if (r.length !== 1 || i !== void 0 && f.decodeUnknownOption(
|
|
104
|
+
if (r.length !== 1 || i !== void 0 && f.decodeUnknownOption(C)(r[0]?.payload)._tag === "None") return {
|
|
105
105
|
kind: "changed",
|
|
106
106
|
requestId: e
|
|
107
107
|
};
|
|
@@ -118,60 +118,60 @@ var w = f.parseJson(f.Struct({
|
|
|
118
118
|
count: o.length
|
|
119
119
|
};
|
|
120
120
|
}).pipe(s.withTransaction, c.provideService(p.SafeIntegers, !0));
|
|
121
|
-
}),
|
|
122
|
-
let n = yield* c.runtime(), r = yield* c.promise(() => e.transaction((t) => a(t, e.requestId, (e) => d.runPromise(n)(
|
|
121
|
+
}), he = (e) => me([e], { suspension: e }), ge = (e) => c.gen(function* () {
|
|
122
|
+
let n = yield* c.runtime(), r = yield* c.promise(() => e.transaction((t) => a(t, e.requestId, (e) => d.runPromise(n)(pe(e)))));
|
|
123
123
|
if (r.kind !== "bound") return r;
|
|
124
124
|
let i = yield* c.promise(() => e.transaction((n) => t(n, e.requestId)));
|
|
125
125
|
if (i.kind !== "dispatch") return i;
|
|
126
126
|
let { request: o } = i;
|
|
127
127
|
if (o.operation !== "resume" || o.resumeJournal === null) return yield* c.die(/* @__PURE__ */ Error("Resume dispatch requires its committed suspension binding"));
|
|
128
|
-
let s = yield*
|
|
128
|
+
let s = yield* he({
|
|
129
129
|
workflowName: o.workflowName,
|
|
130
130
|
executionId: o.executionId,
|
|
131
131
|
...o.resumeJournal
|
|
132
132
|
});
|
|
133
|
-
return yield* (yield*
|
|
133
|
+
return yield* (yield* y.Sharding).pollStorage, {
|
|
134
134
|
kind: "attempted",
|
|
135
135
|
request: o,
|
|
136
136
|
journal: s
|
|
137
137
|
};
|
|
138
|
-
}),
|
|
139
|
-
let e = yield* p.SqlClient, t = yield*
|
|
140
|
-
return
|
|
141
|
-
}),
|
|
142
|
-
let e =
|
|
143
|
-
return e === void 0 ?
|
|
144
|
-
},
|
|
145
|
-
let e = `voltro_cluster_migration_${
|
|
138
|
+
}), w = class extends o.Tag("@voltro/workflow/WorkflowResumeDispatcher")() {}, _e = c.gen(function* () {
|
|
139
|
+
let e = yield* p.SqlClient, t = yield* y.Sharding;
|
|
140
|
+
return w.of({ dispatch: (n) => ge(n).pipe(c.provideService(p.SqlClient, e), c.provideService(y.Sharding, t)) });
|
|
141
|
+
}), T = 6322741009312601n, E = () => {
|
|
142
|
+
let e = S();
|
|
143
|
+
return e === void 0 ? T : x(`voltro_cluster_migration_lock:${e}`) & 9223372036854775807n;
|
|
144
|
+
}, D = () => {
|
|
145
|
+
let e = `voltro_cluster_migration_${T.toString(36)}`, t = S();
|
|
146
146
|
if (t === void 0) return e;
|
|
147
147
|
let n = `${e}_${t}`;
|
|
148
|
-
return /^[A-Za-z0-9_]+$/.test(t) && n.length <= 64 ? n : `${e}_h${
|
|
149
|
-
},
|
|
148
|
+
return /^[A-Za-z0-9_]+$/.test(t) && n.length <= 64 ? n : `${e}_h${x(`voltro_cluster_migration_lock:${t}`).toString(16)}`;
|
|
149
|
+
}, O = () => {
|
|
150
150
|
let e = Number(process.env.VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS);
|
|
151
151
|
return Number.isFinite(e) && e > 0 ? e : 6e4;
|
|
152
|
-
},
|
|
152
|
+
}, k = 250, A = (e) => e[0]?.[0], j = (e, t) => e.onDialectOrElse({
|
|
153
153
|
sqlite: () => c.void,
|
|
154
|
-
mysql: () => t.executeValues(`SELECT GET_LOCK('${
|
|
155
|
-
mssql: () => t.executeValues(`DECLARE @res int; EXEC @res = sp_getapplock @Resource = '${
|
|
154
|
+
mysql: () => t.executeValues(`SELECT GET_LOCK('${D()}', ${Math.ceil(O() / 1e3)})`, []).pipe(c.flatMap((e) => A(e) === 1 || A(e) === "1" ? c.void : c.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${D()}' (GET_LOCK timed out)`)))),
|
|
155
|
+
mssql: () => t.executeValues(`DECLARE @res int; EXEC @res = sp_getapplock @Resource = '${D()}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${O()}; SELECT @res`, []).pipe(c.flatMap((e) => Number(A(e) ?? -999) >= 0 ? c.void : c.die(/* @__PURE__ */ Error(`could not acquire cluster migration lock '${D()}' (sp_getapplock ${String(A(e))})`)))),
|
|
156
156
|
orElse: () => {
|
|
157
|
-
let e =
|
|
158
|
-
let t =
|
|
159
|
-
return t === !0 || t === "t" || t === 1 ? c.void : Date.now() - n >=
|
|
157
|
+
let e = E().toString(), n = Date.now(), r = t.executeValues(`SELECT pg_try_advisory_lock(${e}::bigint)`, []).pipe(c.flatMap((e) => {
|
|
158
|
+
let t = A(e);
|
|
159
|
+
return t === !0 || t === "t" || t === 1 ? c.void : Date.now() - n >= O() ? c.die(/* @__PURE__ */ Error(`could not acquire the cluster migration advisory lock within ${Math.round(O() / 1e3)}s. Another runner is migrating the cluster schema, or a prior boot wedged while holding it. (Raise VOLTRO_CLUSTER_MIGRATION_LOCK_TIMEOUT_MS, or inspect pg_locks for the holder.)`)) : c.sleep(`${k} millis`).pipe(c.zipRight(r));
|
|
160
160
|
}));
|
|
161
161
|
return r;
|
|
162
162
|
}
|
|
163
|
-
}),
|
|
163
|
+
}), M = (e, t) => e.onDialectOrElse({
|
|
164
164
|
sqlite: () => c.void,
|
|
165
|
-
mysql: () => c.asVoid(t.executeValues(`SELECT RELEASE_LOCK('${
|
|
166
|
-
mssql: () => c.asVoid(t.executeValues(`EXEC sp_releaseapplock @Resource = '${
|
|
167
|
-
orElse: () => c.asVoid(t.executeValues(`SELECT pg_advisory_unlock(${
|
|
168
|
-
}),
|
|
165
|
+
mysql: () => c.asVoid(t.executeValues(`SELECT RELEASE_LOCK('${D()}')`, [])),
|
|
166
|
+
mssql: () => c.asVoid(t.executeValues(`EXEC sp_releaseapplock @Resource = '${D()}', @LockOwner = 'Session'`, [])),
|
|
167
|
+
orElse: () => c.asVoid(t.executeValues(`SELECT pg_advisory_unlock(${E().toString()}::bigint)`, []))
|
|
168
|
+
}), N = (e, t) => e.onDialectOrElse({
|
|
169
169
|
sqlite: () => !0,
|
|
170
170
|
orElse: () => !1
|
|
171
171
|
}) ? t : c.scoped(c.gen(function* () {
|
|
172
172
|
let n = yield* e.reserve;
|
|
173
|
-
return yield* c.acquireRelease(
|
|
174
|
-
})),
|
|
173
|
+
return yield* c.acquireRelease(j(e, n), () => M(e, n).pipe(c.orDie)), yield* t;
|
|
174
|
+
})), P = (e) => e === "sql" ? ["cluster_messages", "cluster_runners"] : ["cluster_messages"], F = (e, t) => e.onDialectOrElse({
|
|
175
175
|
pg: () => e`SELECT (to_regclass(${t}) IS NOT NULL) AS present`,
|
|
176
176
|
mysql: () => e`SELECT (COUNT(*) > 0) AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${t}`,
|
|
177
177
|
mssql: () => e`SELECT CASE WHEN OBJECT_ID(${t}, 'U') IS NULL THEN 0 ELSE 1 END AS present`,
|
|
@@ -179,9 +179,17 @@ var w = f.parseJson(f.Struct({
|
|
|
179
179
|
}).pipe(c.map((e) => {
|
|
180
180
|
let t = e[0]?.present;
|
|
181
181
|
return t === !0 || t === 1 || t === "1" || t === "t" || typeof t == "bigint" && t === 1n;
|
|
182
|
-
}), c.catchAll(() => c.succeed(!1))),
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
}), c.catchAll(() => c.succeed(!1))), I = (e, t) => c.forEach(P(t), (t) => F(e, t), { concurrency: 1 }).pipe(c.map((e) => e.every(Boolean))), L = (e, t) => c.scoped(c.asVoid(l.build(t.pipe(l.provide(ie.layerDefaults), l.provide(l.succeed(p.SqlClient, e)))))), R = (e, t) => L(e, oe.layer).pipe(c.zipRight(t === "sql" ? L(e, se.layer) : c.void)), z = (e) => c.map(e.unsafe("SELECT table_name AS t FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'cluster\\_%'"), (e) => new Set(e.map((e) => String(e.t ?? e.T)))), ve = (e, t, n) => n === void 0 || !e.onDialectOrElse({
|
|
183
|
+
mysql: () => !0,
|
|
184
|
+
orElse: () => !1
|
|
185
|
+
}) ? R(e, t) : c.gen(function* () {
|
|
186
|
+
let r = yield* z(e);
|
|
187
|
+
yield* R(e, t);
|
|
188
|
+
let i = new Set([...yield* z(e)].filter((e) => !r.has(e)));
|
|
189
|
+
i.size !== 0 && (yield* b(e, ue(yield* ce(e), /* @__PURE__ */ new Set(), n, i)));
|
|
190
|
+
}), ye = (e) => e === "sql" ? 3 : 2, B = (e) => l.effect(p.SqlClient, c.gen(function* () {
|
|
191
|
+
let t = yield* p.SqlClient, n = ye(e.runnerStorage), r = le(t);
|
|
192
|
+
return r !== void 0 && r < n ? yield* c.dieMessage(`the workflow runtime needs ${n} database connections at once and the pool has ${r}: ` + (e.runnerStorage === "sql" ? "on a first boot it holds its migration lock and its runner lock while the migration runs on a third, and once running it keeps the runner lock and needs one more to work. " : "on a first boot it holds its migration lock while the migration runs on a second. ") + `Set DB_MAX_CONNECTIONS (or PG_MAX_CONNECTIONS) to at least ${n}` + (e.runnerStorage === "sql" ? ", or VOLTRO_WORKFLOW_RUNNER_STORAGE=memory for a single-process runtime that needs 2 (no cross-pod shard handoff)." : ".")) : ((yield* I(t, e.runnerStorage)) || (yield* N(t, ve(t, e.runnerStorage, e.charset))), t);
|
|
185
193
|
})), V = (e) => {
|
|
186
194
|
if (e instanceof Date) return Number.isNaN(e.getTime()) ? null : e;
|
|
187
195
|
if (typeof e == "string" || typeof e == "number") {
|
|
@@ -189,7 +197,7 @@ var w = f.parseJson(f.Struct({
|
|
|
189
197
|
return Number.isNaN(n.getTime()) ? null : n;
|
|
190
198
|
}
|
|
191
199
|
return null;
|
|
192
|
-
}, H = (e) => e === !0 || e === 1 || e === "1" || e instanceof Uint8Array && e[0] === 1,
|
|
200
|
+
}, H = (e) => e === !0 || e === 1 || e === "1" || e instanceof Uint8Array && e[0] === 1, be = (e, t) => {
|
|
193
201
|
let n = e.lastIndexOf(":");
|
|
194
202
|
if (n < 0) return;
|
|
195
203
|
let r = e.slice(0, n), i = Number(e.slice(n + 1)), a = t.indexOf(r);
|
|
@@ -257,7 +265,7 @@ var w = f.parseJson(f.Struct({
|
|
|
257
265
|
}))), g = (r) => {
|
|
258
266
|
let i = /* @__PURE__ */ new Map();
|
|
259
267
|
for (let e of r) {
|
|
260
|
-
let t =
|
|
268
|
+
let t = be(e, n);
|
|
261
269
|
t !== void 0 && i.set(t, e);
|
|
262
270
|
}
|
|
263
271
|
return i.size === 0 ? c.succeed([]) : c.gen(function* () {
|
|
@@ -315,14 +323,14 @@ var w = f.parseJson(f.Struct({
|
|
|
315
323
|
}).pipe(t.withTransaction, c.catchAll(() => c.succeed(0)));
|
|
316
324
|
}
|
|
317
325
|
};
|
|
318
|
-
}), W =
|
|
326
|
+
}), W = de({ scope: "voltro:workflow" }), xe = 34e3, G = (e) => {
|
|
319
327
|
let t = e.runnerListenHost ?? process.env.POD_IP ?? process.env.VOLTRO_WORKFLOW_RUNNER_HOST ?? "localhost";
|
|
320
328
|
return {
|
|
321
329
|
host: t,
|
|
322
330
|
port: e.runnerListenPort ?? 34e3,
|
|
323
331
|
localhostRisk: e.runnerStorage === "sql" && (t === "localhost" || t === "127.0.0.1")
|
|
324
332
|
};
|
|
325
|
-
},
|
|
333
|
+
}, Se = (e) => {
|
|
326
334
|
let t = e.sqliteFamily ? "memory" : "sql", n = e.override ?? process.env.VOLTRO_WORKFLOW_RUNNER_STORAGE;
|
|
327
335
|
if (n === void 0 || n.trim() === "") return t;
|
|
328
336
|
let r = n.trim().toLowerCase();
|
|
@@ -341,12 +349,12 @@ var w = f.parseJson(f.Struct({
|
|
|
341
349
|
return t === 1 || t === "1" || t === !0 || t === "ON" || t === "on";
|
|
342
350
|
}), J = (e) => c.gen(function* () {
|
|
343
351
|
return e.runnerStorage === "memory" ? !1 : e.mode === "row" ? !0 : e.mode === "advisory" || e.dialectId !== "mysql" && e.dialectId !== "mariadb" ? !1 : yield* q;
|
|
344
|
-
}),
|
|
352
|
+
}), Ce = (e) => ({
|
|
345
353
|
coordination: e.disableAdvisory ? "row-based" : "advisory",
|
|
346
354
|
dialect: e.dialectId ?? "unknown",
|
|
347
355
|
mode: e.mode,
|
|
348
356
|
...e.disableAdvisory && e.mode === "auto" ? { reason: "wsrep/Galera cluster detected" } : {}
|
|
349
|
-
}),
|
|
357
|
+
}), we = () => {
|
|
350
358
|
let e = (e) => {
|
|
351
359
|
if (e === void 0) return;
|
|
352
360
|
let t = Number(e);
|
|
@@ -357,72 +365,75 @@ var w = f.parseJson(f.Struct({
|
|
|
357
365
|
...n === void 0 ? {} : { failoverHeartbeatSeconds: n },
|
|
358
366
|
...r === void 0 ? {} : { messagePollSeconds: r }
|
|
359
367
|
};
|
|
360
|
-
},
|
|
368
|
+
}, Te = c.flatMap(y.Sharding, (e) => e.pollStorage), Y = /* @__PURE__ */ new Set(), Ee = (e) => {
|
|
361
369
|
let t = G({
|
|
362
370
|
runnerStorage: e.runnerStorage,
|
|
363
371
|
runnerListenHost: e.runnerListenHost,
|
|
364
372
|
runnerListenPort: e.runnerListenPort
|
|
365
373
|
}), i = t.host, a = t.port;
|
|
366
374
|
t.localhostRisk && !Y.has(i) && (Y.add(i), W.warn("runner host is not routable from other pods with SQL cluster storage — a workflow cannot resume on another pod after a reschedule. Inject POD_IP via the K8s downward API (fieldRef: status.podIP) or set runnerListenHost / VOLTRO_WORKFLOW_RUNNER_HOST.", { runnerHost: i }));
|
|
367
|
-
let d =
|
|
375
|
+
let d = B({
|
|
376
|
+
runnerStorage: e.runnerStorage,
|
|
377
|
+
charset: e.charset
|
|
378
|
+
}).pipe(l.provide(e.sqlClientLayer)), f = e.shardLockMode ?? K(), p = l.unwrapEffect(J({
|
|
368
379
|
dialectId: e.dialectId,
|
|
369
380
|
runnerStorage: e.runnerStorage,
|
|
370
381
|
mode: f
|
|
371
382
|
}).pipe(c.map((t) => {
|
|
372
|
-
e.runnerStorage === "sql" && W.info("shard-lock coordination",
|
|
383
|
+
e.runnerStorage === "sql" && W.info("shard-lock coordination", Ce({
|
|
373
384
|
disableAdvisory: t,
|
|
374
385
|
dialectId: e.dialectId,
|
|
375
386
|
mode: f
|
|
376
387
|
}));
|
|
377
|
-
let r = l.effect(n, c.flatMap(
|
|
378
|
-
ownAddress:
|
|
388
|
+
let r = l.effect(n, c.flatMap(y.Sharding, (n) => U({
|
|
389
|
+
ownAddress: v.make(i, a).toString(),
|
|
379
390
|
shardLockMode: e.runnerStorage === "sql" ? t ? "row" : "advisory" : "none",
|
|
380
391
|
runnerStorage: e.runnerStorage,
|
|
381
392
|
dialectId: e.dialectId,
|
|
382
|
-
hasShard: (e) => n.hasShardId(
|
|
383
|
-
}))), o =
|
|
393
|
+
hasShard: (e) => n.hasShardId(re.ShardId.fromString(e))
|
|
394
|
+
}))), o = ae.layer({
|
|
384
395
|
runnerStorage: e.runnerStorage,
|
|
385
396
|
shardingConfig: {
|
|
386
|
-
runnerAddress: u.some(
|
|
387
|
-
runnerListenAddress: u.some(
|
|
397
|
+
runnerAddress: u.some(v.make(i, a)),
|
|
398
|
+
runnerListenAddress: u.some(v.make(i, a)),
|
|
388
399
|
...t ? { shardLockDisableAdvisory: !0 } : {},
|
|
389
400
|
...e.failoverLeaseSeconds === void 0 ? {} : { shardLockExpiration: s.seconds(e.failoverLeaseSeconds) },
|
|
390
401
|
...e.failoverHeartbeatSeconds === void 0 ? {} : { shardLockRefreshInterval: s.seconds(e.failoverHeartbeatSeconds) },
|
|
391
402
|
...e.messagePollSeconds === void 0 ? {} : { entityMessagePollInterval: s.seconds(e.messagePollSeconds) }
|
|
392
403
|
}
|
|
393
404
|
});
|
|
394
|
-
return l.merge(r, l.effect(
|
|
395
|
-
}))).pipe(l.provide(d)), m = e.extraShardingLayers ?? [], h = m.length === 0 ? g.layer : l.mergeAll(g.layer, ...m), _ = p.pipe(l.map((e) => o.add(e,
|
|
405
|
+
return l.merge(r, l.effect(w, _e)).pipe(l.provideMerge(o));
|
|
406
|
+
}))).pipe(l.provide(d)), m = e.extraShardingLayers ?? [], h = m.length === 0 ? g.layer : l.mergeAll(g.layer, ...m), _ = p.pipe(l.map((e) => o.add(e, y.Sharding, r(o.get(e, y.Sharding)))));
|
|
396
407
|
return h.pipe(l.provideMerge(_));
|
|
397
|
-
},
|
|
408
|
+
}, X = (e) => m.make({
|
|
398
409
|
name: e.name,
|
|
399
410
|
cron: e.cron,
|
|
400
411
|
execute: c.promise(() => e.execute()),
|
|
401
412
|
skipIfOlderThan: e.skipIfOlderThan ?? s.days(1)
|
|
402
|
-
}),
|
|
403
|
-
let r =
|
|
413
|
+
}), Z = (e, t) => `${e}/${t}`, De = (e, t, n) => {
|
|
414
|
+
let r = ee.make(t), i = o.get(e.annotations, h.ShardGroup)(r);
|
|
404
415
|
return new _.EntityAddress({
|
|
405
|
-
entityType:
|
|
416
|
+
entityType: te.EntityType.make(`Workflow/${e.name}`),
|
|
406
417
|
entityId: r,
|
|
407
418
|
shardId: n.getShardId(r, i)
|
|
408
419
|
});
|
|
409
|
-
},
|
|
420
|
+
}, Q = f.decodeUnknownOption(f.Exit({
|
|
410
421
|
success: f.Unknown,
|
|
411
422
|
failure: f.Unknown,
|
|
412
423
|
defect: f.Unknown
|
|
413
|
-
})),
|
|
414
|
-
let t = (e) => e && typeof e == "object" && "_tag" in e && typeof e._tag == "string" ? e._tag : void 0, n =
|
|
424
|
+
})), Oe = (e) => {
|
|
425
|
+
let t = (e) => e && typeof e == "object" && "_tag" in e && typeof e._tag == "string" ? e._tag : void 0, n = Q(e);
|
|
415
426
|
if (u.isNone(n)) return "unrecognized-reply";
|
|
416
427
|
if (n.value._tag === "Failure") return "failed";
|
|
417
428
|
let r = n.value.value, i = t(r);
|
|
418
429
|
if (i === "Suspended") return "suspended";
|
|
419
430
|
if (i === "Complete") {
|
|
420
|
-
let e = r.exit, t =
|
|
431
|
+
let e = r.exit, t = Q(e);
|
|
421
432
|
return u.isNone(t) ? "unrecognized-reply" : t.value._tag === "Success" ? "succeeded" : "failed";
|
|
422
433
|
}
|
|
423
434
|
return "unrecognized-reply";
|
|
424
|
-
},
|
|
425
|
-
let r = yield*
|
|
435
|
+
}, ke = (e) => $(e.workflow, e.executionId, e.failedActivities), Ae = (e) => $(e.workflow, e.executionId, e.resetActivities), $ = (e, t, n) => c.gen(function* () {
|
|
436
|
+
let r = yield* y.Sharding, i = yield* ne.MessageStorage, a = De(e, t, r), o = yield* i.requestIdForPrimaryKey({
|
|
426
437
|
address: a,
|
|
427
438
|
tag: "run",
|
|
428
439
|
id: ""
|
|
@@ -440,7 +451,7 @@ var w = f.parseJson(f.Struct({
|
|
|
440
451
|
activitiesReset: 0,
|
|
441
452
|
reason: "no-reply"
|
|
442
453
|
};
|
|
443
|
-
let l =
|
|
454
|
+
let l = Oe(s.exit);
|
|
444
455
|
if (l !== "failed") return {
|
|
445
456
|
redriven: !1,
|
|
446
457
|
runReplyCleared: !1,
|
|
@@ -452,7 +463,7 @@ var w = f.parseJson(f.Struct({
|
|
|
452
463
|
let t = yield* i.requestIdForPrimaryKey({
|
|
453
464
|
address: a,
|
|
454
465
|
tag: "activity",
|
|
455
|
-
id:
|
|
466
|
+
id: Z(e.name, e.attempt)
|
|
456
467
|
}).pipe(c.orDie);
|
|
457
468
|
if (u.isNone(t)) return {
|
|
458
469
|
redriven: !1,
|
|
@@ -486,4 +497,4 @@ var w = f.parseJson(f.Struct({
|
|
|
486
497
|
};
|
|
487
498
|
});
|
|
488
499
|
//#endregion
|
|
489
|
-
export {
|
|
500
|
+
export { xe as a, Te as c, Se as d, J as f, w as g, B as h, X as i, we as l, U as m, ke as n, q as o, Ee as p, Ae as r, K as s, Z as t, G as u };
|
package/dist/cluster.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Duration } from 'effect';
|
|
|
6
6
|
import { Effect } from 'effect';
|
|
7
7
|
import { Layer } from 'effect';
|
|
8
8
|
import { MessageStorage } from '@effect/cluster';
|
|
9
|
+
import { MysqlCharset } from '@voltro/database/sql';
|
|
9
10
|
import { Sharding } from '@effect/cluster';
|
|
10
11
|
import { SqlClient } from '@effect/sql';
|
|
11
12
|
import { SqlError } from '@effect/sql';
|
|
@@ -349,6 +350,21 @@ declare interface ClusterMessageState {
|
|
|
349
350
|
readonly lastRead: Date | null;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
/**
|
|
354
|
+
* A pass-through `SqlClient` layer that runs the cluster first-boot migration
|
|
355
|
+
* (serialized behind the reserved-connection migration lock) as a build
|
|
356
|
+
* side-effect, then re-exposes the SAME `SqlClient`. Provided in place of the raw
|
|
357
|
+
* dialect SqlClient layer so the cluster engine consumes THIS one — forcing the
|
|
358
|
+
* migration to complete before `SingleRunner` builds and hits its own (now no-op)
|
|
359
|
+
* DDL. Skipped entirely on the warm path, so a running cluster and single-runner
|
|
360
|
+
* restarts cost nothing.
|
|
361
|
+
*/
|
|
362
|
+
export declare const clusterMigrationGateLayer: (opts: {
|
|
363
|
+
readonly runnerStorage: RunnerStorage;
|
|
364
|
+
/** The declared store charset (mysql family) — the tables this gate creates are created in it. */
|
|
365
|
+
readonly charset?: MysqlCharset | undefined;
|
|
366
|
+
}) => Layer.Layer<SqlClient.SqlClient, SqlError.SqlError, SqlClient.SqlClient>;
|
|
367
|
+
|
|
352
368
|
declare interface ClusterRunner {
|
|
353
369
|
readonly address: string;
|
|
354
370
|
readonly lastHeartbeat: Date | null;
|
|
@@ -978,6 +994,13 @@ export declare interface WorkflowEngineLayerOptions {
|
|
|
978
994
|
readonly shardLockMode?: ShardLockMode;
|
|
979
995
|
readonly runnerListenHost?: string;
|
|
980
996
|
readonly runnerListenPort?: number;
|
|
997
|
+
/**
|
|
998
|
+
* The declared store charset (`store: { dialect, charset }`, mysql family).
|
|
999
|
+
* The engine's tables are created by the library with no character set named;
|
|
1000
|
+
* the migration gate converts the ones it creates to this, so a workflow input
|
|
1001
|
+
* with a 4-byte character is not refused on a `utf8mb3` server.
|
|
1002
|
+
*/
|
|
1003
|
+
readonly charset?: MysqlCharset | undefined;
|
|
981
1004
|
/**
|
|
982
1005
|
* FAILOVER TUNING — the two knobs that govern how fast a SURVIVING replica
|
|
983
1006
|
* takes over a CRASHED replica's in-flight workflows. Reclaim is a
|
package/dist/cluster.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r,
|
|
2
|
-
export { e as DEFAULT_RUNNER_LISTEN_PORT, i as WorkflowResumeDispatcher,
|
|
1
|
+
import { a as e, c as t, d as n, f as r, g as i, h as a, i as o, l as s, m as c, n as l, o as u, p as d, r as f, s as p, t as m, u as h } from "./cluster-D7VuRhM0.js";
|
|
2
|
+
export { e as DEFAULT_RUNNER_LISTEN_PORT, i as WorkflowResumeDispatcher, m as activityPrimaryKey, a as clusterMigrationGateLayer, u as detectWsrepCluster, o as makeClusterCronLayer, c as makeClusterDiagnostics, p as parseShardLockMode, t as pollWorkflowStorage, l as redriveFailedRun, s as resolveFailoverTuning, h as resolveRunnerIdentity, n as resolveRunnerStorage, r as resolveShardLockDisableAdvisory, f as resumeRunFromStep, d as workflowEngineLayer };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigError } from 'effect';
|
|
2
2
|
import { Layer } from 'effect';
|
|
3
|
+
import { MysqlCharset } from '@voltro/database/sql';
|
|
3
4
|
import { Sharding } from '@effect/cluster';
|
|
4
5
|
import { SqlClient } from '@effect/sql';
|
|
5
6
|
import { SqlError } from '@effect/sql';
|
|
@@ -145,6 +146,13 @@ declare interface WorkflowEngineLayerOptions {
|
|
|
145
146
|
readonly shardLockMode?: ShardLockMode;
|
|
146
147
|
readonly runnerListenHost?: string;
|
|
147
148
|
readonly runnerListenPort?: number;
|
|
149
|
+
/**
|
|
150
|
+
* The declared store charset (`store: { dialect, charset }`, mysql family).
|
|
151
|
+
* The engine's tables are created by the library with no character set named;
|
|
152
|
+
* the migration gate converts the ones it creates to this, so a workflow input
|
|
153
|
+
* with a 4-byte character is not refused on a `utf8mb3` server.
|
|
154
|
+
*/
|
|
155
|
+
readonly charset?: MysqlCharset | undefined;
|
|
148
156
|
/**
|
|
149
157
|
* FAILOVER TUNING — the two knobs that govern how fast a SURVIVING replica
|
|
150
158
|
* takes over a CRASHED replica's in-flight workflows. Reclaim is a
|
package/dist/clusterTestSuite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { F as e,
|
|
2
|
-
import { r as a } from "./src-
|
|
3
|
-
import { i as o, p as s } from "./cluster-
|
|
1
|
+
import { F as e, R as t, S as n, g as r, v as i } from "./primitives-BJBn8tiH.js";
|
|
2
|
+
import { r as a } from "./src-D_GRCNhv.js";
|
|
3
|
+
import { i as o, p as s } from "./cluster-D7VuRhM0.js";
|
|
4
4
|
import { Cron as c, Deferred as l, Effect as u, Fiber as d, Layer as f, Schema as p } from "effect";
|
|
5
5
|
import { SqlClient as m } from "@effect/sql";
|
|
6
6
|
import { beforeAll as h, describe as g, expect as _, test as v } from "vitest";
|
package/dist/index.d.ts
CHANGED
|
@@ -1368,6 +1368,28 @@ export declare const CurrentWorkflowReplayShape: FiberRef.FiberRef<ReplayShapeTr
|
|
|
1368
1368
|
* `Effect.locally(CurrentWorkflowRunId, runId)`. */
|
|
1369
1369
|
export declare const CurrentWorkflowRunId: FiberRef.FiberRef<string | undefined>;
|
|
1370
1370
|
|
|
1371
|
+
/**
|
|
1372
|
+
* FiberRef carrying the name of the `step()` whose body is running — set by
|
|
1373
|
+
* `step()` around its execute, `undefined` everywhere else.
|
|
1374
|
+
*
|
|
1375
|
+
* It exists so work a step does OUTSIDE the process can be deduplicated
|
|
1376
|
+
* without the author spelling a key: a call to another app of the project made
|
|
1377
|
+
* inside a step is re-sent when the step is retried after its reply was lost,
|
|
1378
|
+
* and `(executionId, step, call)` is the identity that makes the second send a
|
|
1379
|
+
* replay. Read through `currentWorkflowStep`, never directly.
|
|
1380
|
+
*/
|
|
1381
|
+
export declare const CurrentWorkflowStep: FiberRef.FiberRef<string | undefined>;
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* The step this fiber is running inside, with its durable execution — or
|
|
1385
|
+
* `undefined` outside a recorded step. Stable across replays and retries of the
|
|
1386
|
+
* same step, and different for every other step and every other run.
|
|
1387
|
+
*/
|
|
1388
|
+
export declare const currentWorkflowStep: Effect.Effect<{
|
|
1389
|
+
readonly executionId: string;
|
|
1390
|
+
readonly step: string;
|
|
1391
|
+
} | undefined>;
|
|
1392
|
+
|
|
1371
1393
|
/**
|
|
1372
1394
|
* Stop retrying an intent whose exact payload can never start.
|
|
1373
1395
|
*
|