@voltro/workflow 0.25.0 → 0.26.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 +244 -0
- package/dist/cluster-BQ0H5M9n.js +131 -0
- package/dist/cluster.d.ts +140 -13
- package/dist/cluster.js +2 -2
- package/dist/clusterTestSuite.d.ts +34 -0
- package/dist/clusterTestSuite.js +22 -20
- package/dist/index.d.ts +70 -29
- package/dist/index.js +2 -2
- package/dist/primitives-Dgu3O55Q.js +164 -0
- package/dist/primitives.d.ts +63 -28
- package/dist/primitives.js +1 -1
- package/dist/{src-DQww25rP.js → src-KDx7NTsa.js} +97 -73
- package/package.json +3 -3
- package/dist/cluster-DuFweKq0.js +0 -58
- package/dist/primitives-CWy1iu5w.js +0 -111
|
@@ -116,6 +116,40 @@ declare interface WorkflowEngineLayerOptions {
|
|
|
116
116
|
readonly shardLockMode?: ShardLockMode;
|
|
117
117
|
readonly runnerListenHost?: string;
|
|
118
118
|
readonly runnerListenPort?: number;
|
|
119
|
+
/**
|
|
120
|
+
* FAILOVER TUNING — the two knobs that govern how fast a SURVIVING replica
|
|
121
|
+
* takes over a CRASHED replica's in-flight workflows. Reclaim is a
|
|
122
|
+
* lease-expiry problem, NOT a polling one: a dead runner keeps its shards
|
|
123
|
+
* until its heartbeat goes stale, so the floor is the lease TTL.
|
|
124
|
+
*
|
|
125
|
+
* - `failoverLeaseSeconds` (default 35, `@effect/cluster`
|
|
126
|
+
* `shardLockExpiration`) — how long after a runner's last heartbeat its
|
|
127
|
+
* shards become claimable. Lower = faster failover on a hard crash, at the
|
|
128
|
+
* cost of false-positive reclaims if a healthy runner is paused longer than
|
|
129
|
+
* this by a GC pause or a DB-latency spike (two runners could then briefly
|
|
130
|
+
* both think they own a shard).
|
|
131
|
+
* - `failoverHeartbeatSeconds` (default 10, `shardLockRefreshInterval`) — how
|
|
132
|
+
* often a runner refreshes its lease + heartbeat. Keep it well below the
|
|
133
|
+
* lease (≈ lease/3) so a single slow refresh doesn't trip a false reclaim.
|
|
134
|
+
*
|
|
135
|
+
* Omit both for production defaults. A push wake (LISTEN/NOTIFY) does NOT help
|
|
136
|
+
* here — reclaim is time-bound, not notification-bound; only these two (or a
|
|
137
|
+
* K8s liveness probe) move the floor.
|
|
138
|
+
*/
|
|
139
|
+
readonly failoverLeaseSeconds?: number;
|
|
140
|
+
readonly failoverHeartbeatSeconds?: number;
|
|
141
|
+
/**
|
|
142
|
+
* NEW-MESSAGE latency across replicas (default 10, `@effect/cluster`
|
|
143
|
+
* `entityMessagePollInterval`). DISTINCT from failover: this is how long a
|
|
144
|
+
* freshly-triggered workflow can wait before the replica that OWNS its shard
|
|
145
|
+
* picks it up. When the triggering replica owns the shard it's immediate
|
|
146
|
+
* (a same-process push); when ANOTHER replica owns it, Voltro's single-runner
|
|
147
|
+
* topology has no cross-runner push, so the owner sees it on its next poll —
|
|
148
|
+
* up to this interval. Lower it for latency-sensitive multi-replica workloads,
|
|
149
|
+
* at the cost of more idle poll queries. (The lower-load fix is a pg
|
|
150
|
+
* LISTEN/NOTIFY wake, not yet wired.) No effect on a single replica.
|
|
151
|
+
*/
|
|
152
|
+
readonly messagePollSeconds?: number;
|
|
119
153
|
/**
|
|
120
154
|
* Extra layers that need the cluster's `Sharding` service — most
|
|
121
155
|
* prominently `ClusterCron` layers for `cluster`-coordinated
|
package/dist/clusterTestSuite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as e, m as t, p as n, v as r, x as i } from "./primitives-
|
|
2
|
-
import { i as a } from "./src-
|
|
3
|
-
import {
|
|
1
|
+
import { C as e, m as t, p as n, v as r, x as i } from "./primitives-Dgu3O55Q.js";
|
|
2
|
+
import { i as a } from "./src-KDx7NTsa.js";
|
|
3
|
+
import { f as o, r as s } from "./cluster-BQ0H5M9n.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";
|
|
@@ -33,14 +33,14 @@ var y = async (e, t, n = 750) => {
|
|
|
33
33
|
for (let t of b) yield* e.unsafe(`DELETE FROM ${t}`).pipe(u.ignore);
|
|
34
34
|
}).pipe(u.scoped, u.provide(e.sqlClientLayer)));
|
|
35
35
|
} catch {}
|
|
36
|
-
}, S = (e, t) => u.gen(function* () {
|
|
36
|
+
}, S = "10 seconds", C = "30 seconds", w = (e, t) => u.gen(function* () {
|
|
37
37
|
let n = yield* u.sync(() => Date.now() + t);
|
|
38
38
|
for (; !e();) {
|
|
39
39
|
if (Date.now() > n) return !1;
|
|
40
40
|
yield* u.sleep("250 millis");
|
|
41
41
|
}
|
|
42
42
|
return !0;
|
|
43
|
-
}),
|
|
43
|
+
}), T = (m) => {
|
|
44
44
|
let y = `${m.name}-${Date.now()}`, b = (e, t) => o({
|
|
45
45
|
sqlClientLayer: m.sqlClientLayer,
|
|
46
46
|
runnerStorage: m.runnerStorage,
|
|
@@ -104,7 +104,9 @@ var y = async (e, t, n = 750) => {
|
|
|
104
104
|
_(await u.runPromise(u.scoped(u.gen(function* () {
|
|
105
105
|
return yield* f.build(b(m.runnerPort + 1, [n])), yield* l.await(e).pipe(u.timeout("25 seconds"), u.orElseSucceed(() => !1));
|
|
106
106
|
})))).toBe(!0), _(t).toBeGreaterThanOrEqual(1);
|
|
107
|
-
}, 35e3)
|
|
107
|
+
}, 35e3);
|
|
108
|
+
let g = m.clusterResume ? v : (() => {});
|
|
109
|
+
g("a sleeping workflow resumes on a fresh runner after the first is torn down", async () => {
|
|
108
110
|
if (!o) return;
|
|
109
111
|
let e = `dormancyResume_${y}`, i = [], a = [], s = [], c = () => r({
|
|
110
112
|
name: e,
|
|
@@ -126,7 +128,7 @@ var y = async (e, t, n = 750) => {
|
|
|
126
128
|
})
|
|
127
129
|
}), yield* n({
|
|
128
130
|
name: "nap",
|
|
129
|
-
duration:
|
|
131
|
+
duration: S
|
|
130
132
|
}), yield* t({
|
|
131
133
|
name: "step2",
|
|
132
134
|
success: p.Void,
|
|
@@ -142,13 +144,13 @@ var y = async (e, t, n = 750) => {
|
|
|
142
144
|
};
|
|
143
145
|
}, v = { id: `resume-${y}` }, x = await u.runPromise(u.scoped(u.gen(function* () {
|
|
144
146
|
let { wf: e, layer: t } = g("A"), n = yield* f.build(t), r = yield* u.forkDaemon(e.execute(v).pipe(u.provide(n), u.ignore));
|
|
145
|
-
return (yield*
|
|
147
|
+
return (yield* w(() => s.length > 0, 6e4)) ? r : yield* u.die(/* @__PURE__ */ Error("runner A never journaled step1"));
|
|
146
148
|
})));
|
|
147
149
|
await u.runPromise(d.interrupt(x).pipe(u.ignore)), _(i).toEqual(["A"]), _(a).toEqual([]), _(await u.runPromise(u.scoped(u.gen(function* () {
|
|
148
150
|
let { wf: e, layer: t } = g("B"), n = yield* f.build(t);
|
|
149
151
|
return yield* e.execute(v).pipe(u.provide(n), u.timeout("240 seconds"), u.orElseSucceed(() => "TIMEOUT"));
|
|
150
152
|
})))).toBe("done"), _(i).toEqual(["A"]), _(a).toEqual(["B"]);
|
|
151
|
-
}, 48e4),
|
|
153
|
+
}, 48e4), g("dormancy loop: a wake signal survives teardown and is cleared on resume", async () => {
|
|
152
154
|
if (!o) return;
|
|
153
155
|
let s = `dormancyLoop_${y}`, c = `wr_${s}`, l = [], h = /* @__PURE__ */ new Map(), g = a({
|
|
154
156
|
store: {
|
|
@@ -176,7 +178,7 @@ var y = async (e, t, n = 750) => {
|
|
|
176
178
|
execute: u.void
|
|
177
179
|
}), yield* n({
|
|
178
180
|
name: "nap",
|
|
179
|
-
duration:
|
|
181
|
+
duration: C
|
|
180
182
|
}), yield* t({
|
|
181
183
|
name: "step2",
|
|
182
184
|
success: p.Void,
|
|
@@ -184,22 +186,22 @@ var y = async (e, t, n = 750) => {
|
|
|
184
186
|
l.push(a);
|
|
185
187
|
})
|
|
186
188
|
}), "done";
|
|
187
|
-
}).pipe(u.locally(i, c), u.provideService(e, g))),
|
|
189
|
+
}).pipe(u.locally(i, c), u.provideService(e, g))), S = m.runnerPort + 2, T = (e) => {
|
|
188
190
|
let t = v();
|
|
189
191
|
return {
|
|
190
192
|
wf: t,
|
|
191
|
-
layer: x(t, e).pipe(f.provideMerge(b(
|
|
193
|
+
layer: x(t, e).pipe(f.provideMerge(b(S)))
|
|
192
194
|
};
|
|
193
|
-
},
|
|
194
|
-
let { wf: e, layer: t } =
|
|
195
|
-
return (yield*
|
|
195
|
+
}, E = { id: `loop-${y}` }, D = await u.runPromise(u.scoped(u.gen(function* () {
|
|
196
|
+
let { wf: e, layer: t } = T("A"), n = yield* f.build(t), r = yield* u.forkDaemon(e.execute(E).pipe(u.provide(n), u.ignore));
|
|
197
|
+
return (yield* w(() => h.has(c), 6e4)) ? (yield* u.sleep("1 second"), r) : yield* u.die(/* @__PURE__ */ Error("workflow never registered a wakeup"));
|
|
196
198
|
})));
|
|
197
|
-
await u.runPromise(d.interrupt(
|
|
198
|
-
let { wf: e, layer: t } =
|
|
199
|
-
return yield* e.execute(
|
|
199
|
+
await u.runPromise(d.interrupt(D).pipe(u.ignore)), _(h.has(c)).toBe(!0), _(l).toEqual([]), _(await u.runPromise(u.scoped(u.gen(function* () {
|
|
200
|
+
let { wf: e, layer: t } = T("B"), n = yield* f.build(t);
|
|
201
|
+
return yield* e.execute(E).pipe(u.provide(n), u.timeout("600 seconds"), u.orElseSucceed(() => "TIMEOUT"));
|
|
200
202
|
})))).toBe("done"), _(l).toEqual(["B"]), _(h.has(c)).toBe(!1);
|
|
201
|
-
},
|
|
203
|
+
}, 9e5);
|
|
202
204
|
});
|
|
203
205
|
};
|
|
204
206
|
//#endregion
|
|
205
|
-
export {
|
|
207
|
+
export { T as runClusterEngineSuite, y as tcpReachable };
|
package/dist/index.d.ts
CHANGED
|
@@ -426,42 +426,58 @@ declare type StepOptions<R, Success extends Schema.Schema.Any, Error extends Sch
|
|
|
426
426
|
/** Structured value recorded into `_voltro_workflow_run_steps.input`.
|
|
427
427
|
* Pass whatever makes the step debuggable in isolation. */
|
|
428
428
|
readonly input?: unknown;
|
|
429
|
-
/** Declarative retry
|
|
430
|
-
* `
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
429
|
+
/** Declarative retry policy — ENFORCED. The framework compiles it to an
|
|
430
|
+
* Effect `Schedule` and retries `execute` accordingly (backoff, jitter,
|
|
431
|
+
* attempt cap, time budget, error classification, provider `Retry-After`).
|
|
432
|
+
* See {@link StepRetryPolicy}. Retries run INSIDE this one step and are
|
|
433
|
+
* transparent to the durable engine; the step's FINAL outcome is recorded.
|
|
434
|
+
* The serialisable knobs are also stored as `_voltro_workflow_run_steps.
|
|
435
|
+
* retryPolicy` for the dashboard. `retry: { maxAttempts: 5 }` is enough. */
|
|
436
436
|
readonly retry?: StepRetryPolicy;
|
|
437
437
|
};
|
|
438
438
|
|
|
439
439
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* Shape is intentionally close to Effect's Schedule combinators so a
|
|
446
|
-
* future framework version can auto-translate declarative policies
|
|
447
|
-
* into a Schedule + apply it. For now: record + display.
|
|
448
|
-
*
|
|
449
|
-
* { strategy: 'exponential', maxAttempts: 5, baseDelay: '1 second', maxDelay: '30 seconds' }
|
|
450
|
-
* { strategy: 'fixed', maxAttempts: 3, baseDelay: '500 millis' }
|
|
451
|
-
* { strategy: 'linear', maxAttempts: 4, baseDelay: '1 second', step: '2 seconds' }
|
|
440
|
+
* A step's retry policy. Every field is optional — `retry: {}` is already a
|
|
441
|
+
* sensible policy (3 attempts, exponential backoff, jittered). All fields are
|
|
442
|
+
* JSON-serialisable EXCEPT `retryable` (a predicate), which the recorder drops
|
|
443
|
+
* when it stores the policy as dashboard metadata.
|
|
452
444
|
*/
|
|
453
445
|
export declare interface StepRetryPolicy {
|
|
454
|
-
|
|
455
|
-
readonly maxAttempts
|
|
456
|
-
/**
|
|
446
|
+
/** Total attempts INCLUDING the first. Default `3`. `1` disables retry. */
|
|
447
|
+
readonly maxAttempts?: number;
|
|
448
|
+
/** Backoff shape between attempts. Default `'exponential'`. */
|
|
449
|
+
readonly strategy?: 'exponential' | 'fixed' | 'linear';
|
|
450
|
+
/** First delay (Effect Duration string, e.g. `'500 millis'`). Default `'200 millis'`. */
|
|
457
451
|
readonly baseDelay?: string;
|
|
458
|
-
/**
|
|
459
|
-
readonly
|
|
460
|
-
/** Increment for `linear`
|
|
452
|
+
/** Growth factor for `'exponential'`. Default `2`. */
|
|
453
|
+
readonly factor?: number;
|
|
454
|
+
/** Increment per attempt for `'linear'`. Default = `baseDelay`. */
|
|
461
455
|
readonly step?: string;
|
|
462
|
-
/**
|
|
463
|
-
*
|
|
464
|
-
|
|
456
|
+
/** Ceiling on any single delay, so exponential growth can't run away
|
|
457
|
+
* (e.g. `'30 seconds'`). Applied before jitter. */
|
|
458
|
+
readonly maxDelay?: string;
|
|
459
|
+
/** Full jitter on each delay — spreads retries so a fleet doesn't
|
|
460
|
+
* re-hit a recovering dependency in lockstep. Default `true`. */
|
|
461
|
+
readonly jitter?: boolean;
|
|
462
|
+
/** A total wall-clock BUDGET across all attempts: stop retrying once
|
|
463
|
+
* this much time has elapsed since the first attempt, even if attempts
|
|
464
|
+
* remain (e.g. `'5 minutes'`). A deadline, not a count. */
|
|
465
|
+
readonly maxElapsed?: string;
|
|
466
|
+
/** Retry ONLY failures whose typed-error `_tag` is in this list; every
|
|
467
|
+
* other error fails fast. The declarative "retry transient, fail
|
|
468
|
+
* permanent" — e.g. `['ProviderDown', 'RateLimited']`. */
|
|
469
|
+
readonly retryableErrors?: ReadonlyArray<string>;
|
|
470
|
+
/** Retry-predicate on the raw error (wins over `retryableErrors` when
|
|
471
|
+
* both are set). Not serialised into dashboard metadata. Default:
|
|
472
|
+
* retry every failure. */
|
|
473
|
+
readonly retryable?: (error: unknown) => boolean;
|
|
474
|
+
/** Honor a provider's own backoff: if a retryable error carries a
|
|
475
|
+
* `retryAfterMillis` number (or `retryAfter` in seconds), use exactly that as
|
|
476
|
+
* the delay before the next attempt — REPLACING the computed backoff for
|
|
477
|
+
* that attempt (a 429 `Retry-After`, say). Falls back to the normal backoff
|
|
478
|
+
* when the error carries no hint. Default `false`. */
|
|
479
|
+
readonly respectRetryAfter?: boolean;
|
|
480
|
+
/** Free-form note rendered as a chip in the dashboard. */
|
|
465
481
|
readonly note?: string;
|
|
466
482
|
}
|
|
467
483
|
|
|
@@ -637,7 +653,13 @@ export declare interface WorkflowResolvedRun {
|
|
|
637
653
|
* open-text on purpose (future phases add new types without a column
|
|
638
654
|
* migration) — but at emit time we still want type-safety.
|
|
639
655
|
*/
|
|
640
|
-
export declare type WorkflowRunEventType = 'run-started' | 'run-suspended' | 'run-resumed'
|
|
656
|
+
export declare type WorkflowRunEventType = 'run-started' | 'run-suspended' | 'run-resumed'
|
|
657
|
+
/** Emitted when an operator re-drives a terminally-FAILED run from its
|
|
658
|
+
* durable journal (`voltro workflows redrive` / `ctx.workflows.redrive`).
|
|
659
|
+
* `payload` carries `{ reason, activitiesReset }` — how many failed step
|
|
660
|
+
* attempts were reset so they re-execute (completed steps replay). Distinct
|
|
661
|
+
* from `run-resumed`, which continues a *suspended* run. */
|
|
662
|
+
| 'run-redriven' | 'run-cancelled' | 'run-succeeded' | 'run-failed'
|
|
641
663
|
/** Emitted when `sleep({ name, duration })` enters the wait — `payload`
|
|
642
664
|
* carries `{ name, durationMs, scheduledWakeAt }`. The dashboard
|
|
643
665
|
* Gantt renders these as a hatched bar in the timeline so a
|
|
@@ -843,6 +865,25 @@ export declare interface WorkflowVersionOptions {
|
|
|
843
865
|
readonly version?: string | number;
|
|
844
866
|
readonly compatibleWith?: ReadonlyArray<string | number>;
|
|
845
867
|
readonly patches?: ReadonlyArray<string>;
|
|
868
|
+
/**
|
|
869
|
+
* When `true`, a workflow whose top-level body FAILS does not become a
|
|
870
|
+
* terminal `failed` run — it **suspends** with its durable journal intact, so
|
|
871
|
+
* `voltro workflows resume <id>` (or `ctx.workflows.resume`) re-drives it from
|
|
872
|
+
* the point of failure: every completed activity replays from the journal
|
|
873
|
+
* (NOT re-executed) and only the failed activity runs again. This is the
|
|
874
|
+
* durable-execution way to make a workflow recoverable across a transient
|
|
875
|
+
* downstream outage without re-doing prior work.
|
|
876
|
+
*
|
|
877
|
+
* Default `false` — a failure is terminal (the dead-letter state; see
|
|
878
|
+
* `voltro workflows list --dead-letter`). Choose per workflow: `true` for a
|
|
879
|
+
* long multi-step pipeline where re-doing step 1..N-1 is expensive or unsafe;
|
|
880
|
+
* `false` for a short idempotent job where a fresh `retry` is simpler. A
|
|
881
|
+
* suspended-on-failure run shows up under `--status suspended`, NOT in the
|
|
882
|
+
* dead-letter view — it is recoverable, not dead.
|
|
883
|
+
*
|
|
884
|
+
* Maps to `@effect/workflow`'s `SuspendOnFailure` annotation.
|
|
885
|
+
*/
|
|
886
|
+
readonly suspendOnFailure?: boolean;
|
|
846
887
|
}
|
|
847
888
|
|
|
848
889
|
export declare const WorkflowVersionTypeId: unique symbol;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as e, E as t, T as n, _ as r, a as i, b as a, c as o, d as s, f as c, g as l, h as u, i as d, l as f, m as p, n as m, o as h, p as g, r as _, s as v, t as y, u as b, v as x, w as S, x as C, y as w } from "./primitives-
|
|
2
|
-
import { _ as T, a as E, b as D, c as O, d as k, f as A, g as j, h as M, i as N, l as P, m as F, n as I, o as L, p as R, r as z, s as B, t as V, u as H, v as U, x as W, y as G } from "./src-
|
|
1
|
+
import { C as e, E as t, T as n, _ as r, a as i, b as a, c as o, d as s, f as c, g as l, h as u, i as d, l as f, m as p, n as m, o as h, p as g, r as _, s as v, t as y, u as b, v as x, w as S, x as C, y as w } from "./primitives-Dgu3O55Q.js";
|
|
2
|
+
import { _ as T, a as E, b as D, c as O, d as k, f as A, g as j, h as M, i as N, l as P, m as F, n as I, o as L, p as R, r as z, s as B, t as V, u as H, v as U, x as W, y as G } from "./src-KDx7NTsa.js";
|
|
3
3
|
export { a as CurrentWorkflowExecutionId, C as CurrentWorkflowRunId, y as WorkflowMessagesProperty, e as WorkflowRunRecorder, S as WorkflowStepInterceptorTag, m as WorkflowVersionTypeId, _ as WorkflowWorkerLayerTypeId, U as _voltroWorkflowRunEventsTable, G as _voltroWorkflowRunStepsTable, D as _voltroWorkflowRunsTable, T as _voltroWorkflowStartContextsTable, j as awaitSignal, P as awaitSignalSuspending, O as awaitUpdate, z as closeWorkflowChildrenForParent, H as completeSuspendingSignal, d as durableClock, i as durableQueue, h as durableQueueModule, v as durableRateLimiterModule, n as getCurrentWorkflowExecutionId, t as getCurrentWorkflowRunId, o as getWorkflowVersionMetadata, W as inMemoryWorkflowEngineLayer, I as inspectWorkflow, f as isWorkflowWorkerLayer, V as makeInMemoryRecorder, N as makeWorkflowRunRecorder, A as makeWorkflowUpdateId, b as processQueue, s as queueWorker, c as rateLimit, R as resolveWorkflowMessageRun, F as sendWorkflowSignal, M as sendWorkflowUpdate, E as serialiseWorkflowRowForWire, g as sleep, p as step, u as stepIdempotencyKey, l as stepModule, k as suspendingSignalDeferredName, L as truncateWorkflowValue, r as withCompensation, x as workflow, w as workflowModule, B as wrapWorkflowExecuteWithRunRecording };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Activity as e, Activity as t, DurableClock as n, DurableClock as r, DurableQueue as i, DurableQueue as a, DurableRateLimiter as o, DurableRateLimiter as s, Workflow as c, Workflow as l } from "@effect/workflow";
|
|
2
|
+
import { Cause as u, Context as d, Duration as f, Effect as p, Exit as m, FiberRef as h, Option as g, Schedule as _ } from "effect";
|
|
3
|
+
//#region src/recorder.ts
|
|
4
|
+
var v = class extends d.Tag("@voltro/WorkflowRunRecorder")() {}, y = h.unsafeMake(void 0), b = h.unsafeMake(void 0), x = h.unsafeMake(void 0), S = () => h.get(y), C = () => h.get(b), w = class extends d.Tag("@voltro/WorkflowStepInterceptor")() {}, T = (e) => {
|
|
5
|
+
if (typeof e != "object" || !e) return;
|
|
6
|
+
let t = e.retryAfterMillis;
|
|
7
|
+
if (typeof t == "number" && Number.isFinite(t) && t >= 0) return f.millis(t);
|
|
8
|
+
let n = e.retryAfter;
|
|
9
|
+
if (typeof n == "number" && Number.isFinite(n) && n >= 0) return f.seconds(n);
|
|
10
|
+
}, E = (e) => f.decode(e), D = (e) => typeof e == "object" && e && "_tag" in e ? String(e._tag) : void 0, O = (e) => e !== void 0 && (e.maxAttempts ?? 3) > 1, k = (e) => {
|
|
11
|
+
if (e.retryable) return e.retryable;
|
|
12
|
+
if (e.retryableErrors && e.retryableErrors.length > 0) {
|
|
13
|
+
let t = new Set(e.retryableErrors);
|
|
14
|
+
return (e) => {
|
|
15
|
+
let n = D(e);
|
|
16
|
+
return n !== void 0 && t.has(n);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return () => !0;
|
|
20
|
+
}, A = (e) => {
|
|
21
|
+
let t = e.baseDelay ?? "200 millis", n = e.strategy ?? "exponential", r = e.maxAttempts ?? 3, i = n === "linear" ? _.linear(E(e.step ?? t)) : n === "fixed" ? _.spaced(E(t)) : _.exponential(E(t), e.factor ?? 2);
|
|
22
|
+
if (e.maxDelay !== void 0) {
|
|
23
|
+
let t = E(e.maxDelay);
|
|
24
|
+
i = _.modifyDelay(i, (e, n) => f.min(n, t));
|
|
25
|
+
}
|
|
26
|
+
(e.jitter ?? !0) === !0 && (i = _.jittered(i));
|
|
27
|
+
let a = _.intersect(i, _.recurs(Math.max(0, r - 1)));
|
|
28
|
+
return e.maxElapsed !== void 0 && (a = _.upTo(E(e.maxElapsed))(a)), a;
|
|
29
|
+
}, j = (e, t) => {
|
|
30
|
+
let n = f.toMillis(E(e.baseDelay ?? "200 millis")), r = e.strategy ?? "exponential", i = r === "fixed" ? n : r === "linear" ? n + f.toMillis(E(e.step ?? e.baseDelay ?? "200 millis")) * (t - 1) : n * (e.factor ?? 2) ** (t - 1);
|
|
31
|
+
return e.maxDelay !== void 0 && (i = Math.min(i, f.toMillis(E(e.maxDelay)))), (e.jitter ?? !0) === !0 && (i *= Math.random()), i;
|
|
32
|
+
}, M = (e, t, n, r) => p.gen(function* () {
|
|
33
|
+
let i = t.maxAttempts ?? 3, a = t.maxElapsed === void 0 ? Infinity : f.toMillis(E(t.maxElapsed)), o = yield* p.sync(() => Date.now()), s = 0;
|
|
34
|
+
for (;;) {
|
|
35
|
+
let c = yield* p.exit(e);
|
|
36
|
+
if (m.isSuccess(c)) return c.value;
|
|
37
|
+
let l = c.cause, d = u.failureOption(l), h = g.isSome(d) ? d.value : u.squash(l);
|
|
38
|
+
s += 1;
|
|
39
|
+
let _ = (yield* p.sync(() => Date.now())) - o;
|
|
40
|
+
if (s >= i || !n(h) || _ >= a) return yield* p.failCause(l);
|
|
41
|
+
r?.(h);
|
|
42
|
+
let v = T(h), y = v === void 0 ? j(t, s) : f.toMillis(v);
|
|
43
|
+
yield* p.sleep(f.millis(y));
|
|
44
|
+
}
|
|
45
|
+
}), N = (e, t, n) => {
|
|
46
|
+
if (!O(t)) return e;
|
|
47
|
+
let r = k(t);
|
|
48
|
+
if (t.respectRetryAfter === !0) return M(e, t, r, n);
|
|
49
|
+
let i = A(t), a = n ? p.tapError(e, (e) => p.sync(() => {
|
|
50
|
+
r(e) && n(e);
|
|
51
|
+
})) : e;
|
|
52
|
+
return p.retry(a, {
|
|
53
|
+
schedule: i,
|
|
54
|
+
while: r
|
|
55
|
+
});
|
|
56
|
+
}, P = Symbol.for("voltro.workflow.workerLayer"), F = Symbol.for("voltro.workflow.version"), I = "__voltroWorkflowMessages", L = (e) => typeof e == "object" && !!e && e[P] === !0, R = (e) => typeof e == "object" && e && e[F] !== void 0 ? e[F] : {
|
|
57
|
+
version: "1",
|
|
58
|
+
compatibleWith: ["1"],
|
|
59
|
+
patches: []
|
|
60
|
+
}, z = ((e) => {
|
|
61
|
+
let t = c.make(e), n = e.suspendOnFailure === !0 ? t.annotate(c.SuspendOnFailure, !0) : t, r = String(e.version ?? "1"), i = {
|
|
62
|
+
version: r,
|
|
63
|
+
compatibleWith: (e.compatibleWith ?? [r]).map(String),
|
|
64
|
+
patches: [...e.patches ?? []]
|
|
65
|
+
}, a = {
|
|
66
|
+
signals: { ...e.messages?.signals ?? {} },
|
|
67
|
+
updates: { ...e.messages?.updates ?? {} },
|
|
68
|
+
queries: { ...e.messages?.queries ?? {} }
|
|
69
|
+
};
|
|
70
|
+
return Object.defineProperty(n, F, {
|
|
71
|
+
value: i,
|
|
72
|
+
enumerable: !1
|
|
73
|
+
}), Object.defineProperty(n, I, {
|
|
74
|
+
value: a,
|
|
75
|
+
enumerable: !1
|
|
76
|
+
}), n;
|
|
77
|
+
}), B = i.make, V = i.process, H = ((...e) => {
|
|
78
|
+
let t = i.worker(...e);
|
|
79
|
+
return Object.defineProperty(t, P, {
|
|
80
|
+
value: !0,
|
|
81
|
+
enumerable: !1
|
|
82
|
+
}), t;
|
|
83
|
+
}), U = o.rateLimit, W = (e) => p.gen(function* () {
|
|
84
|
+
let t = yield* y, r = yield* p.serviceOption(v), i = f.toMillis(f.decode(e.duration));
|
|
85
|
+
if (t !== void 0 && r._tag === "Some") {
|
|
86
|
+
let a = r.value, o = new Date(Date.now() + i).toISOString(), s = yield* x;
|
|
87
|
+
yield* p.promise(() => a.recordEvent({
|
|
88
|
+
runId: t,
|
|
89
|
+
eventType: "timer-set",
|
|
90
|
+
payload: {
|
|
91
|
+
name: e.name,
|
|
92
|
+
durationMs: i,
|
|
93
|
+
scheduledWakeAt: o,
|
|
94
|
+
...s === void 0 ? {} : { tenantId: s }
|
|
95
|
+
}
|
|
96
|
+
}).catch(() => {}));
|
|
97
|
+
let c = Date.now();
|
|
98
|
+
yield* n.sleep(e), yield* p.promise(() => a.recordEvent({
|
|
99
|
+
runId: t,
|
|
100
|
+
eventType: "timer-fired",
|
|
101
|
+
payload: {
|
|
102
|
+
name: e.name,
|
|
103
|
+
actualDurationMs: Date.now() - c,
|
|
104
|
+
...s === void 0 ? {} : { tenantId: s }
|
|
105
|
+
}
|
|
106
|
+
}).catch(() => {}));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
yield* n.sleep(e);
|
|
110
|
+
}), G = (t, n, r, i) => p.gen(function* () {
|
|
111
|
+
let a = yield* y, o = yield* e.CurrentAttempt, s = yield* p.serviceOption(v), c = yield* p.serviceOption(w), l = c._tag === "Some" ? c.value.interceptor(n, {
|
|
112
|
+
runId: a ?? "unrecorded",
|
|
113
|
+
stepName: t,
|
|
114
|
+
attempt: o
|
|
115
|
+
}) : n, d, f;
|
|
116
|
+
if (a !== void 0 && s._tag === "Some") {
|
|
117
|
+
f = s.value;
|
|
118
|
+
let e = i === void 0 ? void 0 : (({ retryable: e, ...t }) => t)(i);
|
|
119
|
+
try {
|
|
120
|
+
d = yield* p.promise(() => f.startStep({
|
|
121
|
+
runId: a,
|
|
122
|
+
stepName: t,
|
|
123
|
+
attempt: o,
|
|
124
|
+
stepInput: r,
|
|
125
|
+
...e === void 0 ? {} : { retryPolicy: e }
|
|
126
|
+
}));
|
|
127
|
+
} catch {
|
|
128
|
+
d = void 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let m = Date.now();
|
|
132
|
+
return yield* N(l, i).pipe(p.tap((e) => d && f ? p.promise(async () => {
|
|
133
|
+
try {
|
|
134
|
+
await f.endStepSuccess({
|
|
135
|
+
stepRecordId: d,
|
|
136
|
+
output: e,
|
|
137
|
+
durationMs: Date.now() - m
|
|
138
|
+
});
|
|
139
|
+
} catch {}
|
|
140
|
+
}) : p.void), p.tapErrorCause((e) => d && f && !u.isInterruptedOnly(e) ? p.promise(async () => {
|
|
141
|
+
try {
|
|
142
|
+
let t = u.failureOption(e), n = Array.from(u.failures(e)), r = Array.from(u.defects(e)), i = t._tag === "Some" ? t.value : r[0] ?? u.squash(e), a = i?.message ?? String(i), o = i && typeof i == "object" && "_tag" in i ? String(i._tag) : null;
|
|
143
|
+
await f.endStepFailure({
|
|
144
|
+
stepRecordId: d,
|
|
145
|
+
errorTag: o,
|
|
146
|
+
errorMessage: a,
|
|
147
|
+
errorCause: {
|
|
148
|
+
pretty: u.pretty(e),
|
|
149
|
+
failures: n,
|
|
150
|
+
defects: r
|
|
151
|
+
},
|
|
152
|
+
durationMs: Date.now() - m
|
|
153
|
+
});
|
|
154
|
+
} catch {}
|
|
155
|
+
}) : p.void));
|
|
156
|
+
}), K = (t) => {
|
|
157
|
+
let { input: n, retry: r, ...i } = t;
|
|
158
|
+
return e.make({
|
|
159
|
+
...i,
|
|
160
|
+
execute: G(i.name, i.execute, n, r)
|
|
161
|
+
});
|
|
162
|
+
}, q = c.withCompensation, J = e.idempotencyKey;
|
|
163
|
+
//#endregion
|
|
164
|
+
export { v as C, S as E, x as S, C as T, q as _, B as a, b, R as c, H as d, U as f, t as g, J as h, r as i, L as l, K as m, F as n, a as o, W as p, P as r, s, I as t, V as u, z as v, w, y as x, l as y };
|
package/dist/primitives.d.ts
CHANGED
|
@@ -107,42 +107,58 @@ declare type StepOptions<R, Success extends Schema.Schema.Any, Error extends Sch
|
|
|
107
107
|
/** Structured value recorded into `_voltro_workflow_run_steps.input`.
|
|
108
108
|
* Pass whatever makes the step debuggable in isolation. */
|
|
109
109
|
readonly input?: unknown;
|
|
110
|
-
/** Declarative retry
|
|
111
|
-
* `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
110
|
+
/** Declarative retry policy — ENFORCED. The framework compiles it to an
|
|
111
|
+
* Effect `Schedule` and retries `execute` accordingly (backoff, jitter,
|
|
112
|
+
* attempt cap, time budget, error classification, provider `Retry-After`).
|
|
113
|
+
* See {@link StepRetryPolicy}. Retries run INSIDE this one step and are
|
|
114
|
+
* transparent to the durable engine; the step's FINAL outcome is recorded.
|
|
115
|
+
* The serialisable knobs are also stored as `_voltro_workflow_run_steps.
|
|
116
|
+
* retryPolicy` for the dashboard. `retry: { maxAttempts: 5 }` is enough. */
|
|
117
117
|
readonly retry?: StepRetryPolicy;
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* Shape is intentionally close to Effect's Schedule combinators so a
|
|
127
|
-
* future framework version can auto-translate declarative policies
|
|
128
|
-
* into a Schedule + apply it. For now: record + display.
|
|
129
|
-
*
|
|
130
|
-
* { strategy: 'exponential', maxAttempts: 5, baseDelay: '1 second', maxDelay: '30 seconds' }
|
|
131
|
-
* { strategy: 'fixed', maxAttempts: 3, baseDelay: '500 millis' }
|
|
132
|
-
* { strategy: 'linear', maxAttempts: 4, baseDelay: '1 second', step: '2 seconds' }
|
|
121
|
+
* A step's retry policy. Every field is optional — `retry: {}` is already a
|
|
122
|
+
* sensible policy (3 attempts, exponential backoff, jittered). All fields are
|
|
123
|
+
* JSON-serialisable EXCEPT `retryable` (a predicate), which the recorder drops
|
|
124
|
+
* when it stores the policy as dashboard metadata.
|
|
133
125
|
*/
|
|
134
126
|
declare interface StepRetryPolicy {
|
|
135
|
-
|
|
136
|
-
readonly maxAttempts
|
|
137
|
-
/**
|
|
127
|
+
/** Total attempts INCLUDING the first. Default `3`. `1` disables retry. */
|
|
128
|
+
readonly maxAttempts?: number;
|
|
129
|
+
/** Backoff shape between attempts. Default `'exponential'`. */
|
|
130
|
+
readonly strategy?: 'exponential' | 'fixed' | 'linear';
|
|
131
|
+
/** First delay (Effect Duration string, e.g. `'500 millis'`). Default `'200 millis'`. */
|
|
138
132
|
readonly baseDelay?: string;
|
|
139
|
-
/**
|
|
140
|
-
readonly
|
|
141
|
-
/** Increment for `linear`
|
|
133
|
+
/** Growth factor for `'exponential'`. Default `2`. */
|
|
134
|
+
readonly factor?: number;
|
|
135
|
+
/** Increment per attempt for `'linear'`. Default = `baseDelay`. */
|
|
142
136
|
readonly step?: string;
|
|
143
|
-
/**
|
|
144
|
-
*
|
|
145
|
-
|
|
137
|
+
/** Ceiling on any single delay, so exponential growth can't run away
|
|
138
|
+
* (e.g. `'30 seconds'`). Applied before jitter. */
|
|
139
|
+
readonly maxDelay?: string;
|
|
140
|
+
/** Full jitter on each delay — spreads retries so a fleet doesn't
|
|
141
|
+
* re-hit a recovering dependency in lockstep. Default `true`. */
|
|
142
|
+
readonly jitter?: boolean;
|
|
143
|
+
/** A total wall-clock BUDGET across all attempts: stop retrying once
|
|
144
|
+
* this much time has elapsed since the first attempt, even if attempts
|
|
145
|
+
* remain (e.g. `'5 minutes'`). A deadline, not a count. */
|
|
146
|
+
readonly maxElapsed?: string;
|
|
147
|
+
/** Retry ONLY failures whose typed-error `_tag` is in this list; every
|
|
148
|
+
* other error fails fast. The declarative "retry transient, fail
|
|
149
|
+
* permanent" — e.g. `['ProviderDown', 'RateLimited']`. */
|
|
150
|
+
readonly retryableErrors?: ReadonlyArray<string>;
|
|
151
|
+
/** Retry-predicate on the raw error (wins over `retryableErrors` when
|
|
152
|
+
* both are set). Not serialised into dashboard metadata. Default:
|
|
153
|
+
* retry every failure. */
|
|
154
|
+
readonly retryable?: (error: unknown) => boolean;
|
|
155
|
+
/** Honor a provider's own backoff: if a retryable error carries a
|
|
156
|
+
* `retryAfterMillis` number (or `retryAfter` in seconds), use exactly that as
|
|
157
|
+
* the delay before the next attempt — REPLACING the computed backoff for
|
|
158
|
+
* that attempt (a 429 `Retry-After`, say). Falls back to the normal backoff
|
|
159
|
+
* when the error carries no hint. Default `false`. */
|
|
160
|
+
readonly respectRetryAfter?: boolean;
|
|
161
|
+
/** Free-form note rendered as a chip in the dashboard. */
|
|
146
162
|
readonly note?: string;
|
|
147
163
|
}
|
|
148
164
|
|
|
@@ -216,6 +232,25 @@ export declare interface WorkflowVersionOptions {
|
|
|
216
232
|
readonly version?: string | number;
|
|
217
233
|
readonly compatibleWith?: ReadonlyArray<string | number>;
|
|
218
234
|
readonly patches?: ReadonlyArray<string>;
|
|
235
|
+
/**
|
|
236
|
+
* When `true`, a workflow whose top-level body FAILS does not become a
|
|
237
|
+
* terminal `failed` run — it **suspends** with its durable journal intact, so
|
|
238
|
+
* `voltro workflows resume <id>` (or `ctx.workflows.resume`) re-drives it from
|
|
239
|
+
* the point of failure: every completed activity replays from the journal
|
|
240
|
+
* (NOT re-executed) and only the failed activity runs again. This is the
|
|
241
|
+
* durable-execution way to make a workflow recoverable across a transient
|
|
242
|
+
* downstream outage without re-doing prior work.
|
|
243
|
+
*
|
|
244
|
+
* Default `false` — a failure is terminal (the dead-letter state; see
|
|
245
|
+
* `voltro workflows list --dead-letter`). Choose per workflow: `true` for a
|
|
246
|
+
* long multi-step pipeline where re-doing step 1..N-1 is expensive or unsafe;
|
|
247
|
+
* `false` for a short idempotent job where a fresh `retry` is simpler. A
|
|
248
|
+
* suspended-on-failure run shows up under `--status suspended`, NOT in the
|
|
249
|
+
* dead-letter view — it is recoverable, not dead.
|
|
250
|
+
*
|
|
251
|
+
* Maps to `@effect/workflow`'s `SuspendOnFailure` annotation.
|
|
252
|
+
*/
|
|
253
|
+
readonly suspendOnFailure?: boolean;
|
|
219
254
|
}
|
|
220
255
|
|
|
221
256
|
export declare const WorkflowVersionTypeId: unique symbol;
|
package/dist/primitives.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as e, a as t, c as n, d as r, f as i, g as a, h as o, i as s, l as c, m as l, n as u, o as d, p as f, r as p, s as m, t as h, u as g, v as _, y as v } from "./primitives-
|
|
1
|
+
import { _ as e, a as t, c as n, d as r, f as i, g as a, h as o, i as s, l as c, m as l, n as u, o as d, p as f, r as p, s as m, t as h, u as g, v as _, y as v } from "./primitives-Dgu3O55Q.js";
|
|
2
2
|
export { h as WorkflowMessagesProperty, u as WorkflowVersionTypeId, p as WorkflowWorkerLayerTypeId, s as durableClock, t as durableQueue, d as durableQueueModule, m as durableRateLimiterModule, n as getWorkflowVersionMetadata, c as isWorkflowWorkerLayer, g as processQueue, r as queueWorker, i as rateLimit, f as sleep, l as step, o as stepIdempotencyKey, a as stepModule, e as withCompensation, _ as workflow, v as workflowModule };
|