@lunora/workflow 1.0.0-alpha.5 → 1.0.0-alpha.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +25 -2
  3. package/dist/do/index.d.mts +16 -16
  4. package/dist/do/index.d.ts +16 -16
  5. package/dist/do/index.mjs +1 -44
  6. package/dist/index.d.mts +187 -124
  7. package/dist/index.d.ts +187 -124
  8. package/dist/index.mjs +1 -9
  9. package/dist/packem_shared/MAX_BRANCHES-j59ZUUSu.mjs +1 -0
  10. package/dist/packem_shared/NonRetryableError-BPu01mU8.mjs +1 -0
  11. package/dist/packem_shared/WorkflowsRestError-Ccq1FUR1.mjs +1 -0
  12. package/dist/packem_shared/branch-marker-CCpWfS5k.mjs +1 -0
  13. package/dist/packem_shared/createRunStep-DnLETYtC.mjs +1 -0
  14. package/dist/packem_shared/createWaitForEvent-D5Ohn2Pz.mjs +1 -0
  15. package/dist/packem_shared/createWorkflowContext-zQR_9N-v.mjs +1 -0
  16. package/dist/packem_shared/createWorkflowRunContext-C03ogm48.mjs +1 -0
  17. package/dist/packem_shared/createWorkflows-LvAyUyKq.mjs +1 -0
  18. package/dist/packem_shared/defineStep-D1-9eOnA.mjs +1 -0
  19. package/dist/packem_shared/defineWorkflow-tKaIifbZ.mjs +1 -0
  20. package/dist/packem_shared/defineWorkflowEvent-suERPEEV.mjs +1 -0
  21. package/dist/packem_shared/isDuplicateInstanceError-CecnqNAa.mjs +1 -0
  22. package/dist/packem_shared/run-step-CFhxLFfp.mjs +1 -0
  23. package/dist/packem_shared/{types.d-C9W6rEWv.d.mts → types.d-C7jti0tm.d.mts} +221 -124
  24. package/dist/packem_shared/{types.d-C9W6rEWv.d.ts → types.d-C7jti0tm.d.ts} +221 -124
  25. package/package.json +3 -2
  26. package/dist/packem_shared/MAX_BRANCHES-D6cQabJ1.mjs +0 -132
  27. package/dist/packem_shared/NonRetryableError-Dn2dTyBS.mjs +0 -27
  28. package/dist/packem_shared/WorkflowsRestError-b06i7K5j.mjs +0 -118
  29. package/dist/packem_shared/createRunStep-8jOXxP2o.mjs +0 -54
  30. package/dist/packem_shared/createWorkflowContext-D6thzmlF.mjs +0 -14
  31. package/dist/packem_shared/createWorkflowRunContext-CYI9SeI9.mjs +0 -110
  32. package/dist/packem_shared/createWorkflows-BoSYVIXg.mjs +0 -23
  33. package/dist/packem_shared/defineStep-DJQtLw7g.mjs +0 -28
  34. package/dist/packem_shared/defineWorkflow-DbUC-oCN.mjs +0 -15
@@ -1,21 +1,46 @@
1
1
  import { ValidatorMap, InferValidatorMap, Validator } from '@lunora/values';
2
2
  /**
3
- * Opaque reference to a Lunora function. Mirrors the `FunctionReference` shape
4
- * emitted by `@lunora/codegen` (and consumed by `@lunora/client`). We avoid a
5
- * direct dependency to keep this package usable from the codegen pipeline
6
- * itself — identical rationale to `@lunora/scheduler`'s copy.
7
- *
8
- * The runtime identifier lives in `__lunoraRef` — this MUST stay in lockstep
9
- * with the codegen emit + `@lunora/client`'s `FunctionReference`.
10
- */
11
- interface FunctionReference {
3
+ * The generated function-reference type, shared by every package that needs to
4
+ * infer a call's args or return from `api.<file>.<fn>`.
5
+ *
6
+ * This lives in `shared/` rather than in one package because the consumers span
7
+ * a dependency boundary they must not cross: `@lunora/scheduler` and
8
+ * `@lunora/workflow` accept a reference in `runAfter`/`runAt`/`step.run*` but
9
+ * cannot depend on `@lunora/client`, which is a browser package. Each of them
10
+ * previously hand-copied this declaration, and both copies silently rotted when
11
+ * the phantom carrier was renamed — the conditional matched an OPTIONAL property
12
+ * that no longer existed, so `ArgsOf<F>` quietly resolved to `unknown` and every
13
+ * `step.run(ref, args)` in the repo lost its arg checking without a single error.
14
+ *
15
+ * `shared/` is the repo's answer to exactly that shape: bundler-inlined,
16
+ * zero-dependency source imported by relative path, so it creates no runtime
17
+ * edge between the packages that inline it. Being ONE declaration, there is
18
+ * nothing to keep in lockstep and no drift test to write.
19
+ */
20
+ /** The registered function kinds a {@link FunctionReference} can describe. `stream` is a query that yields multiple frames over the WS. */
21
+ type FunctionKind = "action" | "mutation" | "query" | "stream";
22
+ /**
23
+ * Opaque reference to a registered function emitted by `@lunora/codegen`.
24
+ *
25
+ * At runtime it carries the `<file>:<function>` identifier in `__lunoraRef`.
26
+ * Generated declarations decorate this with phantom type parameters so callers
27
+ * can infer args / return values per call site.
28
+ */
29
+ interface FunctionReference<Kind extends FunctionKind = FunctionKind, Args = unknown, Return = unknown> {
30
+ /**
31
+ * Phantom marker carrying the `Kind`/`Args`/`Return` type parameters for
32
+ * inference. Never present at runtime; declared as a covariant (output)
33
+ * position so a concrete reference stays assignable to a widened one.
34
+ */
35
+ readonly __lunoraPhantom?: {
36
+ args: Args;
37
+ kind: Kind;
38
+ returns: Return;
39
+ };
12
40
  readonly __lunoraRef: string;
13
- /** Marker phantom type — discriminates queries / mutations / actions. */
14
- readonly _kind?: "query" | "mutation" | "action";
15
41
  }
16
- type ArgsOf<F extends FunctionReference> = F extends {
17
- _args?: infer A;
18
- } ? A : Record<string, unknown>;
42
+ /** Extract the args type from a {@link FunctionReference}. */
43
+ type ArgsOf<F> = F extends FunctionReference<infer _K, infer A, infer _R> ? A : never;
19
44
  /** A workflow instance's lifecycle status. Mirrors `WorkflowInstanceStatus`. */
20
45
  type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
21
46
  /** Result of `WorkflowInstance.status()`. Mirrors Cloudflare's `InstanceStatus`. */
@@ -53,17 +78,17 @@ interface WorkflowInstanceLike {
53
78
  terminate: () => Promise<void>;
54
79
  }
55
80
  /**
56
- * The subset of the Cloudflare `Workflow` binding the package consumes.
57
- * `createBatch` is a non-optional method on the real binding (Cloudflare's
58
- * `Workflow` class declares it unconditionally), so it is required here too —
59
- * the handle never has to guard for its absence.
60
- */
81
+ * The subset of the Cloudflare `Workflow` binding the package consumes.
82
+ * `createBatch` is a non-optional method on the real binding (Cloudflare's
83
+ * `Workflow` class declares it unconditionally), so it is required here too —
84
+ * the handle never has to guard for its absence.
85
+ */
61
86
  interface WorkflowBindingLike<Params = Record<string, unknown>> {
62
87
  create: (options?: WorkflowCreateOptions<Params>) => Promise<WorkflowInstanceLike>;
63
88
  createBatch: (batch: ReadonlyArray<WorkflowCreateOptions<Params>>) => Promise<WorkflowInstanceLike[]>;
64
89
  get: (id: string) => Promise<WorkflowInstanceLike>;
65
90
  }
66
- /** The `event` argument a workflow `run` receives. Mirrors `WorkflowEvent&lt;T>`. */
91
+ /** The `event` argument a workflow `run` receives. Mirrors `WorkflowEvent<T>`. */
67
92
  interface WorkflowEventLike<Params = Record<string, unknown>> {
68
93
  readonly instanceId: string;
69
94
  readonly payload: Readonly<Params>;
@@ -80,10 +105,10 @@ interface WorkflowStepConfigLike {
80
105
  timeout?: number | string;
81
106
  }
82
107
  /**
83
- * The per-attempt info Cloudflare passes a `step.do` callback. Mirrors
84
- * `WorkflowStepContext` — `attempt` is the 1-based retry counter (`> 1` on
85
- * retries) and `step` carries the durable step's name + invocation count.
86
- */
108
+ * The per-attempt info Cloudflare passes a `step.do` callback. Mirrors
109
+ * `WorkflowStepContext` — `attempt` is the 1-based retry counter (`> 1` on
110
+ * retries) and `step` carries the durable step's name + invocation count.
111
+ */
87
112
  interface WorkflowStepContextLike {
88
113
  /** 1-based attempt counter — `> 1` means this is a retry. */
89
114
  attempt: number;
@@ -114,19 +139,19 @@ interface WorkflowStepRollbackOptionsLike<T = unknown> {
114
139
  rollbackConfig?: WorkflowStepConfigLike;
115
140
  }
116
141
  /**
117
- * The durable step API Cloudflare hands the workflow body. Mirrors
118
- * `WorkflowStep` — `do` memoizes + retries its callback, `sleep`/`sleepUntil`
119
- * are durable delays, and `waitForEvent` hibernates until an external event.
120
- * `do`'s two overloads mirror Cloudflare's: an optional leading `config` and an
121
- * optional trailing `rollback` (compensation run when a later step fails).
122
- *
123
- * Known mirror gap: Cloudflare constrains `do&lt;T extends Rpc.Serializable&lt;T>>` so
124
- * a non-serializable step result (a function, `Map`, class instance, …) is a
125
- * compile error there. `Rpc.Serializable` lives in `@cloudflare/workers-types`
126
- * and is not Node-importable, so this Node-safe mirror uses a bare `&lt;T>` and
127
- * cannot enforce that — a non-serializable result type-checks here but fails at
128
- * runtime on the platform. Keep step results JSON-serialisable.
129
- */
142
+ * The durable step API Cloudflare hands the workflow body. Mirrors
143
+ * `WorkflowStep` — `do` memoizes + retries its callback, `sleep`/`sleepUntil`
144
+ * are durable delays, and `waitForEvent` hibernates until an external event.
145
+ * `do`'s two overloads mirror Cloudflare's: an optional leading `config` and an
146
+ * optional trailing `rollback` (compensation run when a later step fails).
147
+ *
148
+ * Known mirror gap: Cloudflare constrains `do<T extends Rpc.Serializable<T>>` so
149
+ * a non-serializable step result (a function, `Map`, class instance, …) is a
150
+ * compile error there. `Rpc.Serializable` lives in `@cloudflare/workers-types`
151
+ * and is not Node-importable, so this Node-safe mirror uses a bare `<T>` and
152
+ * cannot enforce that — a non-serializable result type-checks here but fails at
153
+ * runtime on the platform. Keep step results JSON-serialisable.
154
+ */
130
155
  interface WorkflowStepLike {
131
156
  do: {
132
157
  <T>(name: string, callback: (context: WorkflowStepContextLike) => Promise<T>, rollback?: WorkflowStepRollbackOptionsLike<T>): Promise<T>;
@@ -155,25 +180,25 @@ interface RunFunctionOptions {
155
180
  shardKey?: string;
156
181
  }
157
182
  /**
158
- * Calls a Lunora query / mutation / action from inside a workflow and resolves
159
- * with its result. Wrap it in {@link WorkflowStepLike.do} to make the call a
160
- * durable, memoized, retried step:
161
- *
162
- * ```ts
163
- * const charge = await ctx.step.do("charge", () => ctx.run(api.payments.charge, { id }));
164
- * ```
165
- */
183
+ * Calls a Lunora query / mutation / action from inside a workflow and resolves
184
+ * with its result. Wrap it in {@link WorkflowStepLike.do} to make the call a
185
+ * durable, memoized, retried step:
186
+ *
187
+ * ```ts
188
+ * const charge = await ctx.step.do("charge", () => ctx.run(api.payments.charge, { id }));
189
+ * ```
190
+ */
166
191
  type WorkflowRunFunction = <F extends FunctionReference>(function_: F, args?: ArgsOf<F>, options?: RunFunctionOptions) => Promise<unknown>;
167
192
  /** Map of validators describing a step's args record — the same shape a Lunora function's `args` uses. Alias of `@lunora/values`' shared {@link ValidatorMap}. */
168
193
  type StepArgsValidator = ValidatorMap;
169
194
  /** Infer the args object type from a {@link StepArgsValidator} (optional validators → optional keys). Alias of `@lunora/values`' shared {@link InferValidatorMap}. */
170
195
  type InferStepArgs<A extends StepArgsValidator> = InferValidatorMap<A>;
171
196
  /**
172
- * The context a {@link StepDefinition} handler receives as its first argument
173
- * (the validated args are the second). Bundles the native per-attempt info
174
- * (`attempt`, `config`, `step`) with the Worker `env`, the Lunora runner, and a
175
- * logger.
176
- */
197
+ * The context a {@link StepDefinition} handler receives as its first argument
198
+ * (the validated args are the second). Bundles the native per-attempt info
199
+ * (`attempt`, `config`, `step`) with the Worker `env`, the Lunora runner, and a
200
+ * logger.
201
+ */
177
202
  interface StepRunContext {
178
203
  /** 1-based retry counter — `> 1` means Cloudflare is retrying the step. */
179
204
  readonly attempt: number;
@@ -226,10 +251,10 @@ interface StepConfig<A extends StepArgsValidator, Result> {
226
251
  rollbackConfig?: WorkflowStepConfigLike;
227
252
  }
228
253
  /**
229
- * A `defineStep` result — a reusable, schema-validated durable step. Run it from
230
- * a workflow body with `ctx.runStep(step, args)`. The phantom generics carry the
231
- * inferred args + result types to the call site.
232
- */
254
+ * A `defineStep` result — a reusable, schema-validated durable step. Run it from
255
+ * a workflow body with `ctx.runStep(step, args)`. The phantom generics carry the
256
+ * inferred args + result types to the call site.
257
+ */
233
258
  interface StepDefinition<A extends StepArgsValidator = StepArgsValidator, Result = unknown> {
234
259
  /** Validators for the step's args. */
235
260
  readonly args: A;
@@ -252,35 +277,93 @@ interface StepDefinition<A extends StepArgsValidator = StepArgsValidator, Result
252
277
  interface RunStepOptions {
253
278
  /** Override the step's declared durability config for this call. */
254
279
  config?: WorkflowStepConfigLike;
280
+ /**
281
+ * Override the durable step name for this call. Cloudflare keys a durable
282
+ * step by name **and occurrence** (`step.count` counts the calls made under
283
+ * one name in a run), so reusing a name in a loop is fine and each
284
+ * occurrence caches independently — this is for giving a call its own
285
+ * addressable name, e.g. to target it with `instance.restart({ from })`.
286
+ * Must be deterministic across replays, like any step name.
287
+ */
288
+ name?: string;
255
289
  }
256
290
  /**
257
- * Run a reusable {@link StepDefinition} as a durable, memoized, retried step:
258
- * validates the args before the body runs and the result after (when the step
259
- * declares `returns`), and forwards any rollback handler to Cloudflare.
260
- *
261
- * ```ts
262
- * const data = await ctx.runStep(fetchImage, { imageKey });
263
- * ```
264
- */
291
+ * Run a reusable {@link StepDefinition} as a durable, memoized, retried step:
292
+ * validates the args before the body runs and the result after (when the step
293
+ * declares `returns`), and forwards any rollback handler to Cloudflare.
294
+ *
295
+ * ```ts
296
+ * const data = await ctx.runStep(fetchImage, { imageKey });
297
+ * ```
298
+ */
265
299
  type WorkflowRunStepFunction = <A extends StepArgsValidator, Result>(step: StepDefinition<A, Result>, args: InferStepArgs<A>, options?: RunStepOptions) => Promise<Result>;
266
300
  /**
267
- * One branch of a {@link WorkflowParallelFunction} fan-out — a declared child
268
- * workflow (referenced by its `lunora/workflows.ts` export name) plus the params
269
- * it is created with. The phantom `Output` carries the child's result type into
270
- * the `ctx.parallel(...)` result tuple. Build one with the `branch(...)` helper.
271
- */
301
+ * A `defineWorkflowEvent` result — the single source of truth for one external
302
+ * event's wire `type` and payload shape. Both ends of the exchange import the same
303
+ * definition (`ctx.waitForEvent(orderApproved)` inside the workflow,
304
+ * `workflows.get(w).sendEvent(id, orderApproved, payload)` from the caller), so
305
+ * there is no string to typo and no second place to update on a rename, and the
306
+ * payload is parsed at both ends instead of crossing as `unknown`.
307
+ *
308
+ * It does NOT make every mismatch a compile error: two definitions with the same
309
+ * payload shape are mutually assignable, so sending `orderRejected` where the
310
+ * workflow awaits `orderApproved` still type-checks (and still hibernates until
311
+ * the timeout). What it removes is the hand-matched literal.
312
+ */
313
+ interface WorkflowEventDefinition<Payload = unknown> {
314
+ /** Runtime brand check (see `isWorkflowEventDefinition`). */
315
+ readonly isLunoraWorkflowEvent: true;
316
+ /** Validator for the event payload — parsed on send and again on receive. */
317
+ readonly payload: Validator<Payload>;
318
+ /** The wire event type Cloudflare matches `sendEvent` against `waitForEvent`. */
319
+ readonly type: string;
320
+ }
321
+ /** Per-call options for {@link WorkflowWaitForEventFunction}. */
322
+ interface WaitForEventOptions {
323
+ /**
324
+ * The durable step label, defaulting to `event:<type>`.
325
+ *
326
+ * Cloudflare identifies a memoized step by this name, so the default couples
327
+ * step identity to the wire type: renaming the event type also renames the
328
+ * step, and an instance that already recorded the wait replays into a *fresh*
329
+ * one that nothing will ever satisfy. Pass a stable `name` on any wait that can
330
+ * outlive a deploy (an approval held for days), and to tell two waits on the
331
+ * same event type apart in the timeline.
332
+ */
333
+ name?: string;
334
+ /** How long to wait before the wait rejects. Defaults to Cloudflare's 24h. */
335
+ timeout?: number | string;
336
+ }
337
+ /**
338
+ * Hibernate until a declared event is delivered to this instance, then resolve
339
+ * with its validated payload. The typed wrapper over
340
+ * {@link WorkflowStepLike.waitForEvent}: the event's `type` comes from the
341
+ * definition instead of a hand-written string, and the payload is parsed through
342
+ * the definition's validator before the workflow resumes on it.
343
+ *
344
+ * ```ts
345
+ * const { approvedBy } = await ctx.waitForEvent(orderApproved, { timeout: "1 hour" });
346
+ * ```
347
+ */
348
+ type WorkflowWaitForEventFunction = <Payload>(event: WorkflowEventDefinition<Payload>, options?: WaitForEventOptions) => Promise<Payload>;
349
+ /**
350
+ * One branch of a {@link WorkflowParallelFunction} fan-out — a declared child
351
+ * workflow (referenced by its `lunora/workflows.ts` export name) plus the params
352
+ * it is created with. The phantom `Output` carries the child's result type into
353
+ * the `ctx.parallel(...)` result tuple. Build one with the `branch(...)` helper.
354
+ */
272
355
  interface WorkflowBranch<Output = unknown> {
273
356
  /** Phantom marker for the branch output type — never present at runtime. */
274
357
  readonly __output?: Output;
275
358
  /**
276
- * Optional group-saga compensation (plan 075 Phase 3): the `lunora/workflows.ts`
277
- * export name of a workflow to run if a **sibling** branch in the same
278
- * `ctx.parallel(...)` group fails **after** this branch has already completed.
279
- * It is spawned fire-and-forget (a durable, replay-safe idempotent create) with
280
- * {@link BranchCompensationParams} as its `ctx.params`. Omit for no
281
- * compensation — a group where no branch sets this behaves exactly as a plain
282
- * fan-out (fail-fast, no rollback).
283
- */
359
+ * Optional group-saga compensation (plan 075 Phase 3): the `lunora/workflows.ts`
360
+ * export name of a workflow to run if a **sibling** branch in the same
361
+ * `ctx.parallel(...)` group fails **after** this branch has already completed.
362
+ * It is spawned fire-and-forget (a durable, replay-safe idempotent create) with
363
+ * {@link BranchCompensationParams} as its `ctx.params`. Omit for no
364
+ * compensation — a group where no branch sets this behaves exactly as a plain
365
+ * fan-out (fail-fast, no rollback).
366
+ */
284
367
  readonly compensateWith?: string;
285
368
  /** Optional explicit child instance id (defaults to a deterministic parent-derived id). */
286
369
  readonly id?: string;
@@ -292,13 +375,13 @@ interface WorkflowBranch<Output = unknown> {
292
375
  readonly workflow: string;
293
376
  }
294
377
  /**
295
- * The `ctx.params` a group-saga compensation workflow (a branch's
296
- * {@link WorkflowBranch.compensateWith}) receives when a sibling's failure rolls
297
- * back the group. Everything is plain-serialisable — the compensation is an
298
- * ordinary declared workflow, so it can `ctx.runStep(...)` its own undo logic.
299
- */
378
+ * The `ctx.params` a group-saga compensation workflow (a branch's
379
+ * {@link WorkflowBranch.compensateWith}) receives when a sibling's failure rolls
380
+ * back the group. Everything is plain-serialisable — the compensation is an
381
+ * ordinary declared workflow, so it can `ctx.runStep(...)` its own undo logic.
382
+ */
300
383
  interface BranchCompensationParams {
301
- /** Index signature: this is a workflow `params` bag, so it is a valid `Record&lt;string, unknown>` payload. */
384
+ /** Index signature: this is a workflow `params` bag, so it is a valid `Record<string, unknown>` payload. */
302
385
  [key: string]: unknown;
303
386
  /** The export name of the completed branch being compensated. */
304
387
  branch: string;
@@ -313,20 +396,20 @@ interface BranchCompensationParams {
313
396
  output?: unknown;
314
397
  }
315
398
  /** Map a tuple of {@link WorkflowBranch}es to the tuple of their output types, preserving order. */
316
- type WorkflowBranchOutputs<B extends ReadonlyArray<WorkflowBranch>> = { -readonly [K in keyof B]: B[K] extends WorkflowBranch<infer Output> ? Output : never };
399
+ type WorkflowBranchOutputs<B extends ReadonlyArray<WorkflowBranch>> = { -readonly [K in keyof B]: B[K] extends WorkflowBranch<infer Output> ? Output : never; };
317
400
  /**
318
- * Run branches as isolated child workflow instances and resolve with their
319
- * outputs in declaration order. Each branch gets its own Durable Object (own
320
- * memory / CPU / retry budget); the parent hibernates while they execute. Rejects
321
- * (non-retryable) on the first branch that fails.
322
- *
323
- * ```ts
324
- * const [tags, thumb] = await ctx.parallel([
325
- * branch("imageTag", { key }),
326
- * branch("thumbnail", { key }),
327
- * ]);
328
- * ```
329
- */
401
+ * Run branches as isolated child workflow instances and resolve with their
402
+ * outputs in declaration order. Each branch gets its own Durable Object (own
403
+ * memory / CPU / retry budget); the parent hibernates while they execute. Rejects
404
+ * (non-retryable) on the first branch that fails.
405
+ *
406
+ * ```ts
407
+ * const [tags, thumb] = await ctx.parallel([
408
+ * branch("imageTag", { key }),
409
+ * branch("thumbnail", { key }),
410
+ * ]);
411
+ * ```
412
+ */
330
413
  type WorkflowParallelFunction = <const B extends ReadonlyArray<WorkflowBranch>>(branches: B) => Promise<WorkflowBranchOutputs<B>>;
331
414
  /** Per-call options for {@link WorkflowSpawnFunction}. */
332
415
  interface WorkflowSpawnOptions {
@@ -334,17 +417,17 @@ interface WorkflowSpawnOptions {
334
417
  id?: string;
335
418
  }
336
419
  /**
337
- * Fire-and-forget start of a declared child workflow from inside a workflow body
338
- * — replay-safe (idempotent create), returns a live handle to the child. Use
339
- * {@link WorkflowParallelFunction} instead when you need to await results.
340
- */
420
+ * Fire-and-forget start of a declared child workflow from inside a workflow body
421
+ * — replay-safe (idempotent create), returns a live handle to the child. Use
422
+ * {@link WorkflowParallelFunction} instead when you need to await results.
423
+ */
341
424
  type WorkflowSpawnFunction = (workflow: string, params?: Record<string, unknown>, options?: WorkflowSpawnOptions) => Promise<WorkflowInstanceLike>;
342
425
  /**
343
- * The context object passed to a `defineWorkflow` handler. Bundles the native
344
- * Cloudflare durability primitives (`step`, `event`) with the Lunora runner
345
- * (`run`), the reusable-step runner (`runStep`), the fan-out primitives
346
- * (`parallel` / `spawn`), the Worker `env`, and a logger.
347
- */
426
+ * The context object passed to a `defineWorkflow` handler. Bundles the native
427
+ * Cloudflare durability primitives (`step`, `event`) with the Lunora runner
428
+ * (`run`), the reusable-step runner (`runStep`), the fan-out primitives
429
+ * (`parallel` / `spawn`), the Worker `env`, and a logger.
430
+ */
348
431
  interface WorkflowRunContext<Params = Record<string, unknown>> {
349
432
  /** The Worker environment bindings. */
350
433
  readonly env: Record<string, unknown>;
@@ -364,6 +447,8 @@ interface WorkflowRunContext<Params = Record<string, unknown>> {
364
447
  readonly spawn: WorkflowSpawnFunction;
365
448
  /** The native Cloudflare Workflows durable-step API. */
366
449
  readonly step: WorkflowStepLike;
450
+ /** Hibernate until a declared external event arrives; resolves with its validated payload. */
451
+ readonly waitForEvent: WorkflowWaitForEventFunction;
367
452
  }
368
453
  /** The workflow body. Receives a {@link WorkflowRunContext}, returns the output. */
369
454
  type WorkflowHandler<Params = Record<string, unknown>, Output = unknown> = (context: WorkflowRunContext<Params>) => Output | Promise<Output>;
@@ -372,19 +457,19 @@ interface WorkflowConfig<Params = Record<string, unknown>, Output = unknown> {
372
457
  /** The workflow body — the multi-step durable program. */
373
458
  handler: WorkflowHandler<Params, Output>;
374
459
  /**
375
- * Optional override for the deployed workflow name — the `workflows[].name`
376
- * written to `wrangler.jsonc`. Defaults to a kebab-cased form of the
377
- * `lunora/workflows.ts` export name (`orderPipeline` → `order-pipeline`).
378
- * This does NOT change the binding name, which is always derived from the
379
- * export name (`orderPipeline` → `WORKFLOW_ORDER_PIPELINE`).
380
- */
460
+ * Optional override for the deployed workflow name — the `workflows[].name`
461
+ * written to `wrangler.jsonc`. Defaults to a kebab-cased form of the
462
+ * `lunora/workflows.ts` export name (`orderPipeline` → `order-pipeline`).
463
+ * This does NOT change the binding name, which is always derived from the
464
+ * export name (`orderPipeline` → `WORKFLOW_ORDER_PIPELINE`).
465
+ */
381
466
  name?: string;
382
467
  }
383
468
  /**
384
- * A `defineWorkflow` result — the config plus the runtime brand codegen and the
385
- * config layer use to discover it. The phantom `__params` / `__output` carry
386
- * the inferred types to the generated `ctx.workflows` handle.
387
- */
469
+ * A `defineWorkflow` result — the config plus the runtime brand codegen and the
470
+ * config layer use to discover it. The phantom `__params` / `__output` carry
471
+ * the inferred types to the generated `ctx.workflows` handle.
472
+ */
388
473
  interface WorkflowDefinition<Params = Record<string, unknown>, Output = unknown> extends WorkflowConfig<Params, Output> {
389
474
  /** Phantom marker for the output type — never present at runtime. */
390
475
  readonly __output?: Output;
@@ -394,9 +479,10 @@ interface WorkflowDefinition<Params = Record<string, unknown>, Output = unknown>
394
479
  readonly isLunoraWorkflow: true;
395
480
  }
396
481
  /**
397
- * A typed handle to one declared workflow, addressable from `ctx.workflows`.
398
- * Thin pass-through over the Cloudflare `Workflow` binding.
399
- */
482
+ * A typed handle to one declared workflow, addressable from `ctx.workflows`.
483
+ * Thin pass-through over the Cloudflare `Workflow` binding, plus the declared-event
484
+ * send (which the raw binding cannot type).
485
+ */
400
486
  interface WorkflowHandle<Params = Record<string, unknown>> {
401
487
  /** Start a new instance (optionally with an id + params). */
402
488
  create: (options?: WorkflowCreateOptions<Params>) => Promise<WorkflowInstanceLike>;
@@ -404,11 +490,22 @@ interface WorkflowHandle<Params = Record<string, unknown>> {
404
490
  createBatch: (batch: ReadonlyArray<WorkflowCreateOptions<Params>>) => Promise<WorkflowInstanceLike[]>;
405
491
  /** Get a handle to an existing instance by id. */
406
492
  get: (id: string) => Promise<WorkflowInstanceLike>;
493
+ /**
494
+ * Deliver a declared event to one instance of this workflow — the typed
495
+ * counterpart of the workflow body's `ctx.waitForEvent`. The wire type comes
496
+ * from the definition (never a hand-written string) and the payload is parsed
497
+ * through the definition's validator **before** the send, so a bad value fails
498
+ * the caller's request instead of waking the workflow on garbage.
499
+ *
500
+ * Mirrors `ctx.agents.<name>.sendEvent(id, …)`: the instance is addressed by
501
+ * id rather than by holding an instance handle, so the common case is one call.
502
+ */
503
+ sendEvent: <Payload>(instanceId: string, event: WorkflowEventDefinition<Payload>, payload: Payload) => Promise<void>;
407
504
  }
408
505
  /**
409
- * The `ctx.workflows` surface available on `MutationCtx` and `ActionCtx`. Each
410
- * declared workflow is reachable by its `lunora/workflows.ts` export name.
411
- */
506
+ * The `ctx.workflows` surface available on `MutationCtx` and `ActionCtx`. Each
507
+ * declared workflow is reachable by its `lunora/workflows.ts` export name.
508
+ */
412
509
  interface Workflows {
413
510
  /** Resolve the handle for a declared workflow by export name. */
414
511
  get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
@@ -416,10 +513,10 @@ interface Workflows {
416
513
  /** Options for `createWorkflows`. */
417
514
  interface LunoraWorkflowsOptions {
418
515
  /**
419
- * Map of `lunora/workflows.ts` export name → its Cloudflare `Workflow`
420
- * binding. Codegen builds this from `env` (`{ orderPipeline:
421
- * env.WORKFLOW_ORDER_PIPELINE }`); for manual wiring construct it yourself.
422
- */
516
+ * Map of `lunora/workflows.ts` export name → its Cloudflare `Workflow`
517
+ * binding. Codegen builds this from `env` (`{ orderPipeline:
518
+ * env.WORKFLOW_ORDER_PIPELINE }`); for manual wiring construct it yourself.
519
+ */
423
520
  bindings: Record<string, WorkflowBindingLike>;
424
521
  }
425
- export { ArgsOf as A, BranchCompensationParams as B, WorkflowSpawnFunction as C, WorkflowSpawnOptions as D, WorkflowStatusResult as E, FunctionReference as F, WorkflowStepConfigLike as G, WorkflowStepContextLike as H, InferStepArgs as I, WorkflowStepRollbackOptionsLike as J, LunoraWorkflowsOptions as L, RunFunctionOptions as R, StepArgsValidator as S, WorkflowDefinition as W, Workflows as a, StepConfig as b, StepDefinition as c, WorkflowConfig as d, WorkflowBranch as e, WorkflowInstanceStatus as f, WorkflowEventLike as g, WorkflowStepLike as h, WorkflowRunContext as i, WorkflowLogger as j, WorkflowRunFunction as k, WorkflowRunStepFunction as l, RunStepOptions as m, StepHandler as n, StepRollbackContext as o, StepRollbackHandler as p, StepRunContext as q, WorkflowBindingLike as r, WorkflowBranchOutputs as s, WorkflowCreateOptions as t, WorkflowHandle as u, WorkflowHandler as v, WorkflowInstanceLike as w, WorkflowParallelFunction as x, WorkflowRollbackContextLike as y, WorkflowRollbackHandlerLike as z };
522
+ export { ArgsOf as A, BranchCompensationParams as B, WorkflowInstanceLike as C, WorkflowParallelFunction as D, WorkflowRollbackContextLike as E, FunctionKind as F, WorkflowRollbackHandlerLike as G, WorkflowSpawnFunction as H, InferStepArgs as I, WorkflowSpawnOptions as J, WorkflowStatusResult as K, LunoraWorkflowsOptions as L, WorkflowStepConfigLike as M, WorkflowStepContextLike as N, WorkflowStepRollbackOptionsLike as O, RunFunctionOptions as R, StepArgsValidator as S, WorkflowDefinition as W, Workflows as a, WorkflowEventDefinition as b, StepConfig as c, StepDefinition as d, WorkflowConfig as e, WorkflowBranch as f, WorkflowInstanceStatus as g, WorkflowEventLike as h, WorkflowStepLike as i, WorkflowRunContext as j, WorkflowLogger as k, WorkflowRunFunction as l, WorkflowRunStepFunction as m, WorkflowWaitForEventFunction as n, FunctionReference as o, RunStepOptions as p, StepHandler as q, StepRollbackContext as r, StepRollbackHandler as s, StepRunContext as t, WaitForEventOptions as u, WorkflowBindingLike as v, WorkflowBranchOutputs as w, WorkflowCreateOptions as x, WorkflowHandle as y, WorkflowHandler as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/workflow",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.50",
4
4
  "description": "Durable workflows for Lunora: defineWorkflow over Cloudflare Workflows, generated WorkflowEntrypoint classes, and the ctx.workflows surface",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -48,7 +48,8 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@lunora/values": "1.0.0-alpha.3"
51
+ "@lunora/errors": "1.0.0-alpha.35",
52
+ "@lunora/values": "1.0.0-alpha.44"
52
53
  },
53
54
  "engines": {
54
55
  "node": "^22.15.0 || >=24.11.0"
@@ -1,132 +0,0 @@
1
- import { NonRetryableError } from './NonRetryableError-Dn2dTyBS.mjs';
2
-
3
- const MAX_BRANCHES = 100;
4
- const BRANCH_MARKER_KEY = "__lunoraBranch";
5
- const SPAWN_STEP_PREFIX = "lunora:spawn:";
6
- const AWAIT_STEP_PREFIX = "lunora:await:";
7
- const SIGNAL_STEP_PREFIX = "lunora:signal:";
8
- const COMPENSATE_STEP_PREFIX = "lunora:compensate:";
9
- const BRANCH_EVENT_PREFIX = "lunora:branch:";
10
- const branch = (workflow, params, options) => {
11
- return { compensateWith: options?.compensateWith, id: options?.id, params, timeout: options?.timeout, workflow };
12
- };
13
- const serializeError = (error) => {
14
- if (error instanceof Error) {
15
- return { message: error.message, name: error.name };
16
- }
17
- return { message: String(error), name: "Error" };
18
- };
19
- const okOutcome = (value) => {
20
- return { status: "ok", value };
21
- };
22
- const errorOutcome = (error) => {
23
- return { error: serializeError(error), status: "error" };
24
- };
25
- const compensateCompleted = async (deps, completed, error) => {
26
- for (let cursor = completed.length - 1; cursor >= 0; cursor -= 1) {
27
- const done = completed[cursor];
28
- const compensateWith = done?.plan.item.compensateWith;
29
- if (done === void 0 || compensateWith === void 0) {
30
- continue;
31
- }
32
- await deps.step.do(`${COMPENSATE_STEP_PREFIX}${done.plan.childId}`, async () => {
33
- const compensateId = `${done.plan.childId}:compensate`;
34
- const compensationParams = {
35
- branch: done.plan.item.workflow,
36
- error,
37
- index: done.plan.index,
38
- output: done.output
39
- };
40
- await deps.resolveBinding(compensateWith).create({ id: compensateId, params: compensationParams });
41
- return compensateId;
42
- });
43
- }
44
- };
45
- const createParallel = (deps) => {
46
- const run = async (branches) => {
47
- if (branches.length === 0) {
48
- return [];
49
- }
50
- if (branches.length > MAX_BRANCHES) {
51
- throw new NonRetryableError(
52
- `ctx.parallel: ${String(branches.length)} branches exceeds the cap of ${String(MAX_BRANCHES)} — split the fan-out or raise the work into fewer child workflows`
53
- );
54
- }
55
- const planned = branches.map((item, index) => {
56
- const childId = deps.nextChildId(item.id);
57
- return { childId, eventType: `${BRANCH_EVENT_PREFIX}${childId}`, index, item };
58
- });
59
- await Promise.all(
60
- planned.map(
61
- (plan) => deps.step.do(`${SPAWN_STEP_PREFIX}${plan.childId}`, async () => {
62
- const binding = deps.resolveBinding(plan.item.workflow);
63
- const marker = { eventType: plan.eventType, index: plan.index, parentBinding: deps.parentBinding, parentId: deps.instanceId };
64
- await binding.create({ id: plan.childId, params: { ...plan.item.params, [BRANCH_MARKER_KEY]: marker } });
65
- return plan.childId;
66
- })
67
- )
68
- );
69
- const results = [];
70
- const completed = [];
71
- for (const plan of planned) {
72
- const event = await deps.step.waitForEvent(`${AWAIT_STEP_PREFIX}${plan.childId}`, {
73
- timeout: plan.item.timeout,
74
- type: plan.eventType
75
- });
76
- const outcome = event.payload;
77
- if (outcome.status === "error") {
78
- await compensateCompleted(deps, completed, outcome.error);
79
- throw new NonRetryableError(`ctx.parallel: branch "${plan.item.workflow}" (#${String(plan.index)}) failed: ${outcome.error.message}`);
80
- }
81
- completed.push({ output: outcome.value, plan });
82
- results.push(outcome.value);
83
- }
84
- return results;
85
- };
86
- return run;
87
- };
88
- const createSpawn = (deps) => async (workflow, params, options) => {
89
- const childId = deps.nextChildId(options?.id);
90
- await deps.step.do(`${SPAWN_STEP_PREFIX}${childId}`, async () => {
91
- const binding = deps.resolveBinding(workflow);
92
- await binding.create({ id: childId, params });
93
- return childId;
94
- });
95
- return deps.resolveBinding(workflow).get(childId);
96
- };
97
- const extractBranchMarker = (payload) => {
98
- if (typeof payload !== "object" || payload === null) {
99
- return void 0;
100
- }
101
- const marker = payload[BRANCH_MARKER_KEY];
102
- if (typeof marker !== "object" || marker === null) {
103
- return void 0;
104
- }
105
- const candidate = marker;
106
- if (typeof candidate.eventType !== "string" || typeof candidate.parentBinding !== "string" || typeof candidate.parentId !== "string" || typeof candidate.index !== "number") {
107
- return void 0;
108
- }
109
- return { eventType: candidate.eventType, index: candidate.index, parentBinding: candidate.parentBinding, parentId: candidate.parentId };
110
- };
111
- const stripBranchMarker = (payload) => {
112
- if (typeof payload !== "object" || payload === null) {
113
- return payload;
114
- }
115
- const rest = { ...payload };
116
- Reflect.deleteProperty(rest, BRANCH_MARKER_KEY);
117
- return rest;
118
- };
119
- const signalBranchParent = async (deps, marker, outcome) => {
120
- const binding = deps.env[marker.parentBinding];
121
- if (!binding || typeof binding.get !== "function") {
122
- return;
123
- }
124
- const getParent = binding.get.bind(binding);
125
- await deps.step.do(`${SIGNAL_STEP_PREFIX}${String(marker.index)}`, async () => {
126
- const parent = await getParent(marker.parentId);
127
- await parent.sendEvent({ payload: outcome, type: marker.eventType });
128
- return marker.eventType;
129
- });
130
- };
131
-
132
- export { BRANCH_MARKER_KEY, MAX_BRANCHES, branch, createParallel, createSpawn, errorOutcome, extractBranchMarker, okOutcome, signalBranchParent, stripBranchMarker };