@voltro/workflow 0.33.0 → 0.35.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.
@@ -3,6 +3,7 @@ import { DurableQueue as durableQueueModule } from '@effect/workflow';
3
3
  import { DurableRateLimiter as durableRateLimiterModule } from '@effect/workflow';
4
4
  import { Duration } from 'effect';
5
5
  import { Effect } from 'effect';
6
+ import { FiberRef } from 'effect';
6
7
  import { Schema } from 'effect';
7
8
  import { Activity as stepModule } from '@effect/workflow';
8
9
  import { Workflow as workflowModule } from '@effect/workflow';
@@ -24,12 +25,25 @@ import { Workflow as workflowModule } from '@effect/workflow';
24
25
  */
25
26
  export declare const assertConsistentPools: (controls: ReadonlyArray<ResolvedFlowControl | undefined>) => void;
26
27
 
28
+ /**
29
+ * The tracker for the CURRENT body entry, or `undefined` when there is nothing
30
+ * to compare against.
31
+ *
32
+ * Installed by `wrapWorkflowExecuteWithRunRecording` ONLY on a re-entry (a run
33
+ * whose row already exists). A first entry has no prior shape, so the whole
34
+ * mechanism costs one `FiberRef` read per step and nothing else.
35
+ */
36
+ export declare const CurrentWorkflowReplayShape: FiberRef.FiberRef<ReplayShapeTracker | undefined>;
37
+
27
38
  /** The DECODED payload type. Mirrors `Workflow.make`'s own `idempotencyKey`
28
39
  * parameter, so the two never disagree about what a payload is. */
29
40
  declare type DecodedPayload<P> = P extends Schema.Struct.Fields ? Schema.Struct.Type<P> : P extends {
30
41
  readonly Type: infer T;
31
42
  } ? T : never;
32
43
 
44
+ /** 24 h — see {@link WorkflowScheduleDeclaration.maxRuntime}. */
45
+ export declare const DEFAULT_WORKFLOW_SCHEDULE_MAX_RUNTIME_MS: number;
46
+
33
47
  /**
34
48
  * Controls that can DEFER a start rather than answer it immediately.
35
49
  *
@@ -66,6 +80,9 @@ export declare type FlowDuration = Duration.DurationInput;
66
80
  * the path it took before this feature existed. */
67
81
  export declare const getWorkflowFlowControl: (value: unknown) => ResolvedFlowControl | undefined;
68
82
 
83
+ /** The resolved schedule attached to a workflow definition, or undefined. */
84
+ export declare const getWorkflowSchedule: (value: unknown) => ResolvedWorkflowSchedule | undefined;
85
+
69
86
  export declare const getWorkflowVersionMetadata: (value: unknown) => WorkflowVersionMetadata;
70
87
 
71
88
  /** `true` when this workflow declares at least one `cancelOn` entry — i.e. when
@@ -84,6 +101,34 @@ export declare const hasFlowControl: (control: ResolvedFlowControl | undefined)
84
101
 
85
102
  export declare const isWorkflowWorkerLayer: (value: unknown) => value is WorkflowWorkerLayerBrand;
86
103
 
104
+ /**
105
+ * Build a tracker from the step rows this run recorded on EARLIER body entries.
106
+ *
107
+ * Pass the rows as they are: one row per step ATTEMPT, so a step that retried
108
+ * three times contributes three. Counting rows rather than distinct names is
109
+ * what makes the per-name sequence rule work, and the asymmetry is deliberate —
110
+ * only reaching a name MORE often than recorded is a finding. Fewer is normal
111
+ * (a crash mid-step records two rows for one logical reach).
112
+ */
113
+ export declare const makeReplayShapeTracker: (recordedRows: ReadonlyArray<RecordedStepShape>) => ReplayShapeTracker;
114
+
115
+ /** The `nondeterminism-suspected` event body. Kept here so the two emit sites
116
+ * (the step wrapper, and the run wrapper's `settle`) cannot drift. */
117
+ export declare const nondeterminismEventPayload: (finding: NondeterminismFinding) => Record<string, unknown>;
118
+
119
+ export declare interface NondeterminismFinding {
120
+ readonly kind: NondeterminismKind;
121
+ readonly stepName: string;
122
+ /** How many rows this run recorded for `stepName` on EARLIER body entries. */
123
+ readonly recorded: number;
124
+ /** How many times the CURRENT body entry reached it. */
125
+ readonly reached: number;
126
+ /** One sentence an operator can act on, without opening the code. */
127
+ readonly detail: string;
128
+ }
129
+
130
+ export declare type NondeterminismKind = 'unreached-step' | 'extra-step-occurrence';
131
+
87
132
  declare type NormaliseWorkflowMessages<M extends WorkflowMessageSchemas | undefined> = {
88
133
  readonly signals: M extends {
89
134
  readonly signals: infer Signals;
@@ -91,11 +136,60 @@ declare type NormaliseWorkflowMessages<M extends WorkflowMessageSchemas | undefi
91
136
  readonly updates: M extends {
92
137
  readonly updates: infer Updates;
93
138
  } ? NonNullable<Updates> & Readonly<Record<string, WorkflowMessagePairSchemas>> : {};
94
- readonly queries: M extends {
95
- readonly queries: infer Queries;
96
- } ? NonNullable<Queries> & Readonly<Record<string, WorkflowMessagePairSchemas>> : {};
97
139
  };
98
140
 
141
+ /**
142
+ * Is a named change marker in effect for the run currently executing?
143
+ *
144
+ * This is the escape valve for the versioning trap. `compatibleWith` is
145
+ * all-or-nothing: leave an old version out and every in-flight run on it is
146
+ * terminally failed; leave it in and the old runs replay against the new body
147
+ * with no detection. `patch` is the third option — the body itself branches, so
148
+ * runs that started BEFORE the change keep taking the old path to completion
149
+ * while new runs take the new one.
150
+ *
151
+ * ```ts
152
+ * export const Charge = workflow({
153
+ * name: 'billing.charge',
154
+ * payload: { orderId: Schema.String },
155
+ * idempotencyKey: ({ orderId }) => `billing.charge:${orderId}`,
156
+ * patches: ['split-tax-calculation'], // ← declare it
157
+ * })
158
+ *
159
+ * // inside the body:
160
+ * if (yield* patch('split-tax-calculation')) {
161
+ * const net = yield* step({ name: 'net-total', execute })
162
+ * const tax = yield* step({ name: 'tax', execute })
163
+ * return net + tax
164
+ * }
165
+ * return yield* step({ name: 'total', execute }) // the pre-patch path
166
+ * ```
167
+ *
168
+ * ── The answer is pinned to the RUN, not to the code ────────────────────────
169
+ *
170
+ * `patches` is stamped onto `_voltro_workflow_runs.workflowPatches` when the
171
+ * run starts and read back from that row on every resume. So a run started
172
+ * before you added the marker answers `false` for the rest of its life,
173
+ * however many times it replays and whatever the deployed code says — which is
174
+ * the property that makes the branch deterministic across a redeploy.
175
+ *
176
+ * Under `@effect/workflow` the naive implementation does the opposite: an
177
+ * activity that is ABSENT from an old run's journal EXECUTES on replay (the
178
+ * journal is keyed name/attempt), so a marker journaled by an activity would
179
+ * answer `true` for exactly the old runs it must answer `false` for. Reading
180
+ * the run row sidesteps that entirely.
181
+ *
182
+ * ── Retiring a patch ───────────────────────────────────────────────────────
183
+ *
184
+ * Once no run predating the marker can still be in flight, delete the old
185
+ * branch and the entry from `patches`. Runs that stamped it keep the marker on
186
+ * their row for the audit trail; `patch()` simply stops being called.
187
+ *
188
+ * Outside a recorded workflow body (a unit test, a bare `step()` call) this is
189
+ * `false` — the pre-patch branch, which is the safe direction.
190
+ */
191
+ export declare const patch: (id: string) => Effect.Effect<boolean>;
192
+
99
193
  /** The SCHEMA a payload input denotes — `Fields` lifted into a `Struct`. */
100
194
  declare type PayloadSchemaOf<P> = P extends Schema.Struct.Fields ? Schema.Struct<P> : P;
101
195
 
@@ -114,6 +208,39 @@ export declare const queueWorker: typeof durableQueueModule.worker;
114
208
  /** Durable rate limiter activity. Delays through the workflow clock. */
115
209
  export declare const rateLimit: typeof durableRateLimiterModule.rateLimit;
116
210
 
211
+ /** The slice of a `_voltro_workflow_run_steps` row this needs. */
212
+ export declare interface RecordedStepShape {
213
+ readonly stepName: string;
214
+ }
215
+
216
+ export declare interface ReplayShapeTracker {
217
+ /**
218
+ * The body reached `stepName`. Returns a finding the FIRST time that reach
219
+ * is anomalous, `undefined` otherwise — so a step in a hot loop reports once
220
+ * rather than once per iteration.
221
+ */
222
+ readonly reach: (stepName: string) => NondeterminismFinding | undefined;
223
+ /**
224
+ * The body ran to a COMPLETE outcome. Returns the set-membership findings —
225
+ * recorded names the body never reached.
226
+ *
227
+ * Call this ONLY on success/failure, never on a suspend: a suspended body
228
+ * stopped partway through on purpose, so everything after the suspension
229
+ * point is legitimately unreached.
230
+ */
231
+ readonly settle: () => ReadonlyArray<NondeterminismFinding>;
232
+ }
233
+
234
+ /**
235
+ * Emit findings from INSIDE the workflow body, where the run id and the
236
+ * recorder are only reachable through the fiber context.
237
+ *
238
+ * Best-effort in the strongest sense: no recorder, no run id, or a rejecting
239
+ * write all resolve to "nothing happened". A tripwire that can break the run it
240
+ * is watching is not a tripwire.
241
+ */
242
+ export declare const reportNondeterminism: (findings: ReadonlyArray<NondeterminismFinding>) => Effect.Effect<void>;
243
+
117
244
  /** One resolved `cancelOn` entry. The schema is kept (not pre-compiled into a
118
245
  * decoder) so the sweeper can report a decode failure naming the event, which
119
246
  * is the difference between "the predicate said no" and "we could not read the
@@ -182,6 +309,18 @@ export declare interface ResolvedFlowControl {
182
309
  readonly cancelOn?: ReadonlyArray<ResolvedCancelOn>;
183
310
  }
184
311
 
312
+ /** The declaration with defaults filled in — what the CLI lowering reads. */
313
+ export declare interface ResolvedWorkflowSchedule {
314
+ readonly cron: string;
315
+ readonly timezone: string;
316
+ readonly payload?: unknown | ((fire: {
317
+ readonly scheduledAt: Date;
318
+ }) => unknown | Promise<unknown>);
319
+ readonly onOverlap: WorkflowScheduleOverlap;
320
+ readonly backfill: 'skip' | 'latest' | 'all';
321
+ readonly maxRuntimeMs: number;
322
+ }
323
+
185
324
  /**
186
325
  * Durable sleep — wake time journaled in the cluster. Wrapper around
187
326
  * `DurableClock.sleep` that emits `timer-set` / `timer-fired` events
@@ -341,6 +480,14 @@ export declare interface ValidateFlowControlInput {
341
480
  readonly control: (WorkflowFlowControl<never> & WorkflowCancelOnDeclared) | undefined;
342
481
  }
343
482
 
483
+ /**
484
+ * Validate at DEFINITION time — module load, i.e. boot — with a message naming
485
+ * the workflow and the field. Same posture as `validateFlowControl` and
486
+ * `defineSchedule`: a cron typo that surfaces as "never fired" three days in
487
+ * is the failure class this repo keeps paying for.
488
+ */
489
+ export declare const validateWorkflowSchedule: (workflowName: string, declaration: WorkflowScheduleDeclaration<never> | undefined) => ResolvedWorkflowSchedule | undefined;
490
+
344
491
  /**
345
492
  * Add compensating (rollback) logic to a top-level effect in a workflow
346
493
  * body. The finalizer runs if the WHOLE workflow later fails — use it for
@@ -727,6 +874,12 @@ declare type WorkflowFn = <const Name extends string, Payload extends WorkflowPa
727
874
  * type comes from its OWN `schema` — see {@link WorkflowCancelOnList} for
728
875
  * why that needs a tuple type parameter rather than a plain array. */
729
876
  readonly cancelOn?: WorkflowCancelOnList<Cancels, DecodedPayload<Payload>>;
877
+ /** Run this workflow on a cron — the workflow-side spelling of a
878
+ * `defineSchedule({ workflow })` target, with Temporal's overlap
879
+ * vocabulary (`skip` / `buffer` / `cancelOther`). Lowered by the CLI into
880
+ * a real schedule named `workflow:<name>`; see
881
+ * {@link WorkflowScheduleDeclaration}. */
882
+ readonly schedule?: WorkflowScheduleDeclaration<DecodedPayload<Payload>>;
730
883
  } & Omit<WorkflowBaseOptions, 'name' | 'payload' | 'idempotencyKey' | 'success' | 'error'> & WorkflowVersionOptions & {
731
884
  readonly messages?: Messages;
732
885
  } & WorkflowFlowControl<DecodedPayload<Payload>>) => workflowModule.Workflow<Name, PayloadSchemaOf<Payload>, Success, Error> & WorkflowMessagesCarrier<NormaliseWorkflowMessages<Messages>>;
@@ -742,16 +895,24 @@ export declare interface WorkflowMessagesCarrier<M extends WorkflowMessagesMetad
742
895
  readonly [WorkflowMessagesProperty]: M;
743
896
  }
744
897
 
898
+ /**
899
+ * The message channels a workflow declares.
900
+ *
901
+ * TWO, not three. `queries` used to sit here as a third channel and there was
902
+ * never a send path for it — no `sendWorkflowQuery`, no `awaitQuery`, nothing
903
+ * to receive one. It was normalised into metadata, projected into the generated
904
+ * rpcGroup and carried on the client's `WorkflowState`, so a user got a fully
905
+ * typed record they could not invoke from anywhere. Signals (fire-and-forget)
906
+ * and updates (synchronous, with a result) are the channels that exist.
907
+ */
745
908
  export declare interface WorkflowMessageSchemas {
746
909
  readonly signals?: Readonly<Record<string, Schema.Schema.Any>>;
747
910
  readonly updates?: Readonly<Record<string, WorkflowMessagePairSchemas>>;
748
- readonly queries?: Readonly<Record<string, WorkflowMessagePairSchemas>>;
749
911
  }
750
912
 
751
913
  export declare interface WorkflowMessagesMetadata {
752
914
  readonly signals: Readonly<Record<string, Schema.Schema.Any>>;
753
915
  readonly updates: Readonly<Record<string, WorkflowMessagePairSchemas>>;
754
- readonly queries: Readonly<Record<string, WorkflowMessagePairSchemas>>;
755
916
  }
756
917
 
757
918
  export declare const WorkflowMessagesProperty = "__voltroWorkflowMessages";
@@ -778,6 +939,68 @@ export declare interface WorkflowRateLimit<Payload> {
778
939
  readonly key?: (payload: Payload) => string;
779
940
  }
780
941
 
942
+ export declare interface WorkflowScheduleDeclaration<Payload> {
943
+ /** Standard cron expression — 5-field or 6-field (leading seconds). */
944
+ readonly cron: string;
945
+ /** IANA timezone the expression is interpreted in. REQUIRED, like
946
+ * `defineSchedule` — server-local time in a container is a bug factory. */
947
+ readonly timezone: string;
948
+ /**
949
+ * The payload each firing starts the workflow with. A value, or a function
950
+ * of the firing (`({ scheduledAt }) => …`) for payloads that carry the slot
951
+ * — a backfilled firing then computes against ITS instant, not "now".
952
+ * Omitted ⇒ `{}`, which only typechecks for workflows whose payload has no
953
+ * required fields; a mismatch fails the start with `WorkflowPayloadError`
954
+ * naming the missing fields, same as any other start.
955
+ */
956
+ readonly payload?: Payload | ((fire: {
957
+ readonly scheduledAt: Date;
958
+ }) => Payload | Promise<Payload>);
959
+ /** Default `'skip'` — Temporal's default too, and the only answer that is
960
+ * safe for every job shape. */
961
+ readonly onOverlap?: WorkflowScheduleOverlap;
962
+ /** Boot catch-up policy for firings missed during downtime — the scheduler's
963
+ * own vocabulary, unchanged. Default `'skip'`. For a RANGE older than boot
964
+ * backfill reaches, use `voltro schedule backfill`. */
965
+ readonly backfill?: 'skip' | 'latest' | 'all';
966
+ /**
967
+ * Watchdog on ONE firing — which, for a workflow schedule, INCLUDES the
968
+ * awaited run (that await is what makes `onOverlap` bind on the run's
969
+ * duration). Default 24 hours rather than `defineSchedule`'s 30 minutes,
970
+ * because a durable run legitimately outlives a handler; set it above your
971
+ * slowest expected run. Past it the FIRING records `failed`/timeout and
972
+ * releases the overlap guard — the workflow run itself is NOT cancelled
973
+ * (declare flow-control `timeouts.finish` to bound the run).
974
+ */
975
+ readonly maxRuntime?: FlowDuration;
976
+ }
977
+
978
+ /**
979
+ * What happens when a firing arrives while the RUN from the previous firing is
980
+ * still going — Temporal Schedules' overlap vocabulary, applied to the
981
+ * workflow run (not merely to the handler invocation):
982
+ *
983
+ * `'skip'` — the new firing stands down; recorded as `skipped`.
984
+ * `'buffer'` — the new firing waits and runs after the previous one
985
+ * finishes; firings serialize, none is lost.
986
+ * `'cancelOther'` — the new firing CANCELS the still-running previous run
987
+ * and starts fresh — for "recompute the latest state"
988
+ * jobs where the old run's partial work is worthless.
989
+ *
990
+ * The first two lower into the scheduler's own `onOverlap: 'skip' | 'queue'`;
991
+ * what makes them bind on the RUN's duration is that the synthesised handler
992
+ * AWAITS the workflow run to completion, so the schedule-run row is `running`
993
+ * for exactly as long as the workflow is.
994
+ */
995
+ export declare type WorkflowScheduleOverlap = 'skip' | 'buffer' | 'cancelOther';
996
+
997
+ /** Attached via `defineProperty` like the flow-control carrier — the value's
998
+ * TYPE does not know about it, and the web bundle never reads it. A STRING
999
+ * property rather than a symbol for the same reason as
1000
+ * `WorkflowFlowControlProperty`: the CLI's discovery reads it off a module
1001
+ * that crossed a bundler boundary, and symbols do not survive every one. */
1002
+ export declare const WorkflowScheduleProperty = "__voltroWorkflowSchedule";
1003
+
781
1004
  /**
782
1005
  * At most one run per key. The newcomer either stands down or evicts.
783
1006
  *
@@ -841,6 +1064,17 @@ export declare interface WorkflowVersionMetadata {
841
1064
  export declare interface WorkflowVersionOptions {
842
1065
  readonly version?: string | number;
843
1066
  readonly compatibleWith?: ReadonlyArray<string | number>;
1067
+ /**
1068
+ * Named change markers this workflow's body may branch on, via
1069
+ * {@link patch}. Declaring one here is what makes `yield* patch('id')`
1070
+ * answer `true` for runs started from now on and `false` for runs that were
1071
+ * already in flight — see {@link patch} for the semantics and the worked
1072
+ * example.
1073
+ *
1074
+ * The declared set is stamped onto `_voltro_workflow_runs.workflowPatches`
1075
+ * at run start and read back from THERE on every resume, so the answer is
1076
+ * pinned to the run and cannot change under a redeploy.
1077
+ */
844
1078
  readonly patches?: ReadonlyArray<string>;
845
1079
  /**
846
1080
  * When `true`, a workflow whose top-level body FAILS does not become a
@@ -1,2 +1,2 @@
1
- import { A as e, F as t, M as n, N as r, P as i, S as a, _ as o, a as s, b as c, c as l, d as u, f as d, g as f, h as p, i as m, j as h, l as g, m as _, n as v, o as y, p as b, r as x, s as S, t as C, u as w, v as T, x as E, y as D } from "./primitives-Bp98_F5L.js";
2
- export { e as DEFERRING_CONTROLS, C as WorkflowFlowControlProperty, v as WorkflowMessagesProperty, x as WorkflowVersionTypeId, m as WorkflowWorkerLayerTypeId, h as assertConsistentPools, n as deferringControlsOf, s as durableClock, y as durableQueue, S as durableQueueModule, l as durableRateLimiterModule, g as getWorkflowFlowControl, w as getWorkflowVersionMetadata, r as hasCancelOn, i as hasFlowControl, u as isWorkflowWorkerLayer, d as processQueue, b as queueWorker, _ as rateLimit, p as sleep, f as sleepUntil, o as step, T as stepIdempotencyKey, D as stepModule, t as validateFlowControl, c as withCompensation, E as workflow, a as workflowModule };
1
+ import { A as e, B as t, C as n, D as r, E as i, G as a, H as o, O as s, S as c, T as l, U as u, V as d, W as f, _ as p, a as m, b as h, c as g, d as _, f as v, g as y, h as b, i as x, j as S, k as C, l as w, m as T, n as E, o as D, p as O, r as k, s as A, t as j, u as M, v as N, w as P, x as F, y as I } from "./primitives-CyQZeT1t.js";
2
+ export { P as CurrentWorkflowReplayShape, s as DEFAULT_WORKFLOW_SCHEDULE_MAX_RUNTIME_MS, t as DEFERRING_CONTROLS, j as WorkflowFlowControlProperty, E as WorkflowMessagesProperty, C as WorkflowScheduleProperty, k as WorkflowVersionTypeId, x as WorkflowWorkerLayerTypeId, d as assertConsistentPools, o as deferringControlsOf, m as durableClock, D as durableQueue, A as durableQueueModule, g as durableRateLimiterModule, w as getWorkflowFlowControl, e as getWorkflowSchedule, M as getWorkflowVersionMetadata, u as hasCancelOn, f as hasFlowControl, _ as isWorkflowWorkerLayer, l as makeReplayShapeTracker, i as nondeterminismEventPayload, v as patch, O as processQueue, T as queueWorker, b as rateLimit, r as reportNondeterminism, y as sleep, p as sleepUntil, N as step, I as stepIdempotencyKey, h as stepModule, a as validateFlowControl, S as validateWorkflowSchedule, F as withCompensation, c as workflow, n as workflowModule };