@lunora/scheduler 1.0.0-alpha.7 → 1.0.0-alpha.71

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 (29) hide show
  1. package/README.md +4 -3
  2. package/dist/index.d.mts +870 -411
  3. package/dist/index.d.ts +870 -411
  4. package/dist/index.mjs +1 -8
  5. package/dist/packem_shared/CRON_SCHEDULE_KINDS-xyFk800s.mjs +1 -0
  6. package/dist/packem_shared/MAX_RETRY_ATTEMPTS-CoEnaemn.mjs +1 -0
  7. package/dist/packem_shared/assertScheduleDelay-BgA4K1WB.mjs +1 -0
  8. package/dist/packem_shared/assertScheduleInstant-BzESPqyw.mjs +1 -0
  9. package/dist/packem_shared/assertValidCronExpression-DnBtukpq.mjs +1 -0
  10. package/dist/packem_shared/base64-BFBvYeZM.mjs +1 -0
  11. package/dist/packem_shared/createCronTrigger-DSuDZtqj.mjs +1 -0
  12. package/dist/packem_shared/createQueueConsumer-DWWfGYKN.mjs +1 -0
  13. package/dist/packem_shared/createScheduler-BW6wVD57.mjs +1 -0
  14. package/dist/packem_shared/createSchedulerHost-CO6BlgVY.mjs +1 -0
  15. package/dist/packem_shared/createWorkpool-CQs6z3Y0.mjs +1 -0
  16. package/dist/packem_shared/do-client-XQayNStI.mjs +1 -0
  17. package/dist/packem_shared/isWorkflowReference-CT3tdefh.mjs +1 -0
  18. package/dist/packem_shared/resolveScheduleId-DgysKSrN.mjs +1 -0
  19. package/dist/packem_shared/wire-codec-TIVAE0Ds.mjs +1 -0
  20. package/package.json +4 -3
  21. package/dist/packem_shared/CRON_SCHEDULE_KINDS-BMVeCHOu.mjs +0 -130
  22. package/dist/packem_shared/SchedulerDO-DVeJrNbs.mjs +0 -675
  23. package/dist/packem_shared/assertValidCronExpression-B9m75qU0.mjs +0 -24
  24. package/dist/packem_shared/createCronTrigger-Dh4VLxD9.mjs +0 -28
  25. package/dist/packem_shared/createQueueConsumer-Cy-Mp-El.mjs +0 -61
  26. package/dist/packem_shared/createScheduler-CcD09oIm.mjs +0 -70
  27. package/dist/packem_shared/createWorkpool-C28trhTA.mjs +0 -63
  28. package/dist/packem_shared/isWorkflowReference-C9mQkMXt.mjs +0 -3
  29. package/dist/packem_shared/jurisdiction-CR2zC3Et.mjs +0 -13
package/dist/index.d.mts CHANGED
@@ -1,82 +1,162 @@
1
+ import { SchedulerHost } from '@lunora/platform';
1
2
  /**
2
- * Opaque reference to a Lunora function. Mirrors the `FunctionReference` shape
3
- * emitted by `@lunora/codegen` (and consumed by `@lunora/client`). We avoid a
4
- * direct dependency to keep this package usable from the codegen pipeline
5
- * itself.
6
- *
7
- * The runtime identifier lives in `__lunoraRef` — this MUST stay in lockstep
8
- * with the codegen emit + `@lunora/client`'s `FunctionReference`.
9
- */
10
- 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
+ };
11
40
  readonly __lunoraRef: string;
12
- /** Marker phantom type — discriminates queries / mutations / actions. */
13
- readonly _kind?: "query" | "mutation" | "action";
14
- }
15
- type ArgsOf<F extends FunctionReference> = F extends {
16
- _args?: infer A;
17
- } ? A : Record<string, unknown>;
18
- /**
19
- * Typed reference to a Lunora durable workflow — either the generated
20
- * `workflows.&lt;name>` reference object (`_generated/api.ts`, which carries the
21
- * `WORKFLOW_*` binding + export name) or, structurally, a `defineWorkflow()`
22
- * result imported directly. Both are matched by the `isLunoraWorkflow` brand and
23
- * carry the workflow's `params` in the phantom `__params`, so a `cronJobs()`
24
- * registration infers them.
25
- *
26
- * Declared structurally here so `@lunora/scheduler` can let a `cronJobs()`
27
- * builder target a workflow without depending on `@lunora/workflow` (and so the
28
- * generated `workflows.*` object needs no `@lunora/scheduler` import — it
29
- * matches structurally). A cron whose target is a {@link WorkflowReference}
30
- * starts a new workflow INSTANCE on each fire (the args become its `params`)
31
- * instead of dispatching a one-shot function. `@lunora/codegen` resolves the
32
- * concrete `lunora/workflows.ts` export statically; the runtime brand here is
33
- * the authoring-time guard.
34
- */
41
+ }
42
+ /** Extract the args type from a {@link FunctionReference}. */
43
+ type ArgsOf<F> = F extends FunctionReference<infer _K, infer A, infer _R> ? A : never;
44
+ /**
45
+ * Typed reference to a Lunora durable workflow — either the generated
46
+ * `workflows.<name>` reference object (`_generated/api.ts`, which carries the
47
+ * `WORKFLOW_*` binding + export name) or, structurally, a `defineWorkflow()`
48
+ * result imported directly. Both are matched by the `isLunoraWorkflow` brand and
49
+ * carry the workflow's `params` in the phantom `__params`, so a `cronJobs()`
50
+ * registration infers them.
51
+ *
52
+ * Declared structurally here so `@lunora/scheduler` can let a `cronJobs()`
53
+ * builder target a workflow without depending on `@lunora/workflow` (and so the
54
+ * generated `workflows.*` object needs no `@lunora/scheduler` import — it
55
+ * matches structurally). A cron whose target is a {@link WorkflowReference}
56
+ * starts a new workflow INSTANCE on each fire (the args become its `params`)
57
+ * instead of dispatching a one-shot function. `@lunora/codegen` resolves the
58
+ * concrete `lunora/workflows.ts` export statically; the runtime brand here is
59
+ * the authoring-time guard.
60
+ */
35
61
  interface WorkflowReference<Params = Record<string, unknown>> {
36
62
  /** Phantom carrier for the workflow's `params` type — drives `cronJobs()` arg inference. Never read at runtime. */
37
63
  readonly __params?: Params;
38
- /** The `WORKFLOW_*` binding name (present on a generated `workflows.&lt;name>` ref). */
64
+ /** The `WORKFLOW_*` binding name (present on a generated `workflows.<name>` ref). */
39
65
  readonly binding?: string;
40
66
  readonly isLunoraWorkflow: true;
41
67
  /** The workflow's export/stable name (present on a generated ref; a `defineWorkflow({ name })` override otherwise). */
42
68
  readonly name?: string;
43
69
  }
70
+ /**
71
+ * A function reference a scheduler or workpool may target.
72
+ *
73
+ * `stream` is excluded deliberately, and the exclusion is load-bearing rather
74
+ * than tidiness: a scheduled job is dispatched as an ordinary `/rpc` call, and
75
+ * the function runner cannot execute a stream function (see
76
+ * `create-worker.ts`'s registry note). Accepting one compiles a job that is
77
+ * guaranteed to fail when its alarm fires, long after the call site that
78
+ * scheduled it.
79
+ */
80
+ type SchedulableReference<Args = unknown, Return = unknown> = FunctionReference<Exclude<FunctionKind, "stream">, Args, Return>;
44
81
  /** A cron job's target: either a one-shot function dispatch or a durable workflow start. */
45
- type CronTarget = FunctionReference | WorkflowReference;
82
+ type CronTarget = SchedulableReference | WorkflowReference;
46
83
  /** The arguments a cron's target accepts: a workflow's inferred `params`, else an open record (function args aren't inferred). */
47
84
  type CronTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : Record<string, unknown>;
85
+ /**
86
+ * The arguments a one-shot schedule target ({@link Scheduler.runAfter} /
87
+ * {@link Scheduler.runAt}) accepts. Unlike {@link CronTargetArgs} it resolves a
88
+ * {@link FunctionReference}'s `args` through {@link ArgsOf} as well as a
89
+ * {@link WorkflowReference}'s `params`, so scheduling a generated function
90
+ * reference is arg-checked against that function's validator while scheduling a
91
+ * workflow/agent is checked against its `params`.
92
+ */
93
+ type ScheduleTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : T extends SchedulableReference ? ArgsOf<T> : Record<string, unknown>;
48
94
  /** Narrow a {@link CronTarget} to a {@link WorkflowReference} by its runtime brand. */
49
95
  declare const isWorkflowReference: (target: unknown) => target is WorkflowReference;
50
96
  /**
51
- * Per-job retry policy. Wired into the SchedulerDO's existing attempts/backoff
52
- * machinery. When omitted, the DO falls back to its built-in defaults
53
- * (`maxAttempts: 5`, `backoff: "exponential"`, `baseMs: 30_000`) so existing
54
- * `runAfter`/`runAt` callers keep today's behaviour unchanged.
55
- *
56
- * On exhaustion (attempts > `maxAttempts`) the record is parked under the
57
- * `dead:` dead-letter key for inspection — never silently dropped.
58
- */
97
+ * Per-job retry policy. Wired into the SchedulerDO's existing attempts/backoff
98
+ * machinery. When omitted, the DO falls back to its built-in defaults
99
+ * (`maxAttempts: 5`, `backoff: "exponential"`, `baseMs: 30_000`) so existing
100
+ * `runAfter`/`runAt` callers keep today's behaviour unchanged.
101
+ *
102
+ * On exhaustion (attempts > `maxAttempts`) the record is parked under the
103
+ * `dead:` dead-letter key for inspection — never silently dropped.
104
+ */
59
105
  interface RetryPolicy {
60
106
  /**
61
- * Backoff growth across attempts. `"exponential"` doubles the delay each
62
- * attempt (`baseMs * 2 ** (attempt - 1)`); `"linear"` grows it linearly
63
- * (`baseMs * attempt`). Default `"exponential"`.
64
- */
107
+ * Backoff growth across attempts. `"exponential"` doubles the delay each
108
+ * attempt (`baseMs * 2 ** (attempt - 1)`); `"linear"` grows it linearly
109
+ * (`baseMs * attempt`). Default `"exponential"`.
110
+ */
65
111
  backoff?: "exponential" | "linear";
66
112
  /** Base delay in milliseconds for the first retry. Default `30_000`. */
67
113
  baseMs?: number;
68
- /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
114
+ /**
115
+ * Maximum number of **retries** after the initial dispatch. Default `5`, so
116
+ * a job that keeps failing is dispatched 6 times in total before it is
117
+ * dead-lettered (the park happens once `attempts > maxAttempts`).
118
+ */
69
119
  maxAttempts?: number;
70
120
  /** Optional ceiling clamping the computed backoff delay. */
71
121
  maxMs?: number;
72
122
  }
73
123
  interface RunOptions {
74
124
  /**
75
- * Logical workpool this job belongs to. When set, the SchedulerDO gates the
76
- * job behind the pool's `maxConcurrency` (see {@link WorkpoolOptions}).
77
- * Usually populated by {@link Workpool.enqueue}; callers rarely set it on a
78
- * bare `runAfter`/`runAt`.
79
- */
125
+ * Job id to store the record under, instead of one the SchedulerDO mints.
126
+ *
127
+ * Exists for `@lunora/server`'s deferred-schedule facade: inside a mutation a
128
+ * `runAfter`/`runAt` is buffered until the transaction commits, but the
129
+ * handler is handed the id synchronously, so the id has to be decided before
130
+ * the call is made. Callers that are not deferring should leave it unset and
131
+ * take the minted id from the return value. Anything that is not a plain
132
+ * `[A-Za-z0-9_-]` id of at most 64 characters, or that LEADS with `-`, is
133
+ * REFUSED (`400 INVALID_SCHEDULE_ID`) rather than replaced: the id is handed
134
+ * to `WorkflowBinding.create({ id })` verbatim for a workflow target, and the
135
+ * engine's instance-id grammar (`^[a-zA-Z0-9_][a-zA-Z0-9-_]*$`) refuses that
136
+ * first character. Minting over it would mean two calls naming the same bad
137
+ * id ran the job twice instead of the second answering `409`.
138
+ *
139
+ * **Not an idempotency key.** An id that is already scheduled is REFUSED
140
+ * (`409 DUPLICATE_SCHEDULE_ID`), not replaced or de-duplicated: the time
141
+ * index is keyed by time as well as id, so an overwrite would fire the new
142
+ * job at the old job's instant and drop the slot it was actually scheduled
143
+ * for. Cancel the existing job first if you mean to reschedule it. The id
144
+ * is free again once the job has fired or been cancelled.
145
+ */
146
+ id?: string;
147
+ /**
148
+ * Cap for the {@link RunOptions.pool} this job joins, applied when the pool
149
+ * is first created and refreshed on every enqueue that carries one. Ignored
150
+ * without `pool`. A pool created by a `runAfter`/`runAt` that omits it caps
151
+ * at 1 — {@link Workpool} is the usual way to set it.
152
+ */
153
+ maxConcurrency?: number;
154
+ /**
155
+ * Logical workpool this job belongs to. When set, the SchedulerDO gates the
156
+ * job behind the pool's `maxConcurrency` (see {@link WorkpoolOptions}).
157
+ * Usually populated by {@link Workpool.enqueue}; callers rarely set it on a
158
+ * bare `runAfter`/`runAt`.
159
+ */
80
160
  pool?: string;
81
161
  /** Per-job retry policy. Falls back to the DO's built-in defaults when omitted. */
82
162
  retry?: RetryPolicy;
@@ -86,65 +166,108 @@ interface RunOptions {
86
166
  interface ScheduleRecord {
87
167
  args: Record<string, unknown>;
88
168
  /**
89
- * Number of dispatch attempts already made. Absent (treated as 0) until the
90
- * first failure, after which `recordRetry()` persists it on both the
91
- * `retry:` row and the `id:` header. Surfaced here so `/list` consumers and
92
- * the studio see the field the storage layer actually writes.
93
- */
169
+ * Number of dispatch attempts already made. Absent (treated as 0) until the
170
+ * first failure, after which `recordRetry()` persists it on both the
171
+ * `retry:` row and the `id:` header. Surfaced here so `/list` consumers and
172
+ * the studio see the field the storage layer actually writes.
173
+ */
94
174
  attempts?: number;
95
175
  enqueuedAt: number;
96
- functionPath: string;
176
+ /**
177
+ * The `ns:fn` path of the function to dispatch on fire. Absent when the job
178
+ * targets a durable workflow/agent instead — see {@link ScheduleRecord.workflow}.
179
+ * Exactly one of `functionPath` / `workflow` is set.
180
+ */
181
+ functionPath?: string;
97
182
  id: string;
98
183
  /**
99
- * Scheduler/workpool instance name the job was enqueued through. Echoed in
100
- * the dispatch payload so the runtime can call back the SAME DO instance's
101
- * `/complete` to release a pooled slot. Absent for the default instance.
102
- */
184
+ * Scheduler/workpool instance name the job was enqueued through. Echoed in
185
+ * the dispatch payload so the runtime can call back the SAME DO instance's
186
+ * `/complete` to release a pooled slot. Absent for the default instance.
187
+ */
103
188
  instanceName?: string;
104
189
  /**
105
- * Logical workpool this job belongs to (set by {@link Workpool.enqueue}).
106
- * When present, the SchedulerDO only dispatches the job while the pool's
107
- * in-flight count is below its `maxConcurrency`; otherwise it stays queued
108
- * and drains as slots free. Absent for plain `runAfter`/`runAt` jobs, which
109
- * are never concurrency-gated.
110
- */
190
+ * Logical workpool this job belongs to (set by {@link Workpool.enqueue}).
191
+ * When present, the SchedulerDO only dispatches the job while the pool's
192
+ * in-flight count is below its `maxConcurrency`; otherwise it stays queued
193
+ * and drains as slots free. Absent for plain `runAfter`/`runAt` jobs, which
194
+ * are never concurrency-gated.
195
+ */
111
196
  pool?: string;
112
197
  /** Per-job retry policy (see {@link RetryPolicy}); absent means DO defaults. */
113
198
  retry?: RetryPolicy;
114
199
  scheduledFor: number;
115
200
  shardKey?: string;
201
+ /**
202
+ * The `WORKFLOW_*`/`AGENT_*` binding name to start a fresh durable instance
203
+ * of on fire (the {@link ScheduleRecord.args} become its `params`). Set
204
+ * instead of {@link ScheduleRecord.functionPath} when the job targets a
205
+ * workflow/agent {@link WorkflowReference}. The runtime — not the DO — owns
206
+ * the binding, so the dispatch payload carries this through to the Worker.
207
+ */
208
+ workflow?: string;
116
209
  }
117
210
  interface Scheduler {
118
211
  cancel: (id: string) => Promise<{
119
212
  cancelled: boolean;
120
213
  }>;
214
+ /**
215
+ * Jobs that exhausted their retry budget and were parked under `dead:`
216
+ * (the DO's `/dead` view). Deliberately absent from {@link Scheduler.list} —
217
+ * the park deletes the `id:` header — so this is the only view of a job
218
+ * that failed permanently rather than being silently dropped.
219
+ */
220
+ dead: () => Promise<ScheduleRecord[]>;
221
+ /**
222
+ * Resurrect a parked job with a fresh attempt budget (the DO's
223
+ * `POST /dead/retry`). `false` when the id is not parked; a racing double
224
+ * recover is a no-op rather than an error.
225
+ */
226
+ deadRetry: (id: string) => Promise<boolean>;
121
227
  /** Resolve a single pending job by id, or `null` when absent (derived from {@link Scheduler.list}). */
122
228
  get: (id: string) => Promise<ScheduleRecord | null>;
123
229
  /** All pending scheduled jobs (the DO's `/list` view). */
124
230
  list: () => Promise<ScheduleRecord[]>;
125
- runAfter: <F extends FunctionReference>(delayMs: number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
126
- id: string;
127
- scheduledFor: number;
128
- }>;
129
- runAt: <F extends FunctionReference>(date: Date | number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
130
- id: string;
131
- scheduledFor: number;
132
- }>;
231
+ /**
232
+ * Schedule `target` to run once, `delayMs` from now. `target` is a function
233
+ * {@link FunctionReference} (dispatched as a one-shot) or a durable
234
+ * {@link WorkflowReference} — the generated `workflows.<name>` /
235
+ * `agents.<name>` ref — which starts a fresh instance on fire (args become
236
+ * its `params`). {@link ScheduleTargetArgs} infers the accepted args from
237
+ * whichever target was passed.
238
+ *
239
+ * **Resolves the job id, a bare string** — the same value `cancel`/`get`
240
+ * take, and the same value the `ctx.scheduler` surface promises. This object
241
+ * IS `ctx.scheduler` on the shard side (codegen installs it behind
242
+ * `SchedulerLike`, whose `runAfter`/`runAt` are declared `Promise<string>`),
243
+ * so resolving a `{ id, scheduledFor }` record here handed mutations an
244
+ * object where every other gate — `@lunora/server`'s `Scheduler`,
245
+ * `@lunora/shard-engine`'s `SchedulerLike`, `@lunora/runtime`'s httpAction
246
+ * ctx, and the docs — said string. Nothing caught it, because the install is
247
+ * a cast: apps wrote the object into a string column and `cancel(id)`
248
+ * answered `{ cancelled: false }` with no error anywhere.
249
+ *
250
+ * The fire instant is not lost: `runAt` was handed it, and a caller that
251
+ * needs it back reads `scheduledFor` off {@link Scheduler.get}.
252
+ */
253
+ runAfter: <T extends CronTarget>(delayMs: number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<string>;
254
+ /** Like {@link Scheduler.runAfter} but fires at an absolute `date`/timestamp. Resolves the job id. */
255
+ runAt: <T extends CronTarget>(date: Date | number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<string>;
133
256
  }
134
257
  /**
135
- * Cloudflare Durable Object data-residency jurisdiction. Widening union —
136
- * Cloudflare adds values over time.
137
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
138
- */
258
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
259
+ * Cloudflare adds values over time.
260
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
261
+ */
139
262
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
140
263
  /** Subset of `DurableObjectNamespace` the package consumes. */
141
264
  interface DurableObjectNamespaceLike {
142
265
  get: (id: DurableObjectIdLike) => DurableObjectStubLike;
143
266
  idFromName: (name: string) => DurableObjectIdLike;
144
267
  /**
145
- * Derive a jurisdiction-restricted subnamespace. Optional because older
146
- * workers-types releases (and test doubles) may not expose it.
147
- */
268
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
269
+ * workers-types releases (and test doubles) may not expose it.
270
+ */
148
271
  jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => DurableObjectNamespaceLike;
149
272
  }
150
273
  interface DurableObjectIdLike {
@@ -157,19 +280,20 @@ interface LunoraSchedulerOptions {
157
280
  /** Optional named instance — useful for tenant isolation. Default `default`. */
158
281
  instanceName?: string;
159
282
  /**
160
- * Pin the SchedulerDO (durable timers + cron state) to a Cloudflare
161
- * data-residency jurisdiction. Pass the same value as the worker's
162
- * `jurisdiction` so scheduled state co-resides with app data. Omit for the
163
- * un-pinned global namespace.
164
- */
283
+ * Pin the SchedulerDO (durable timers + cron state) to a Cloudflare
284
+ * data-residency jurisdiction. Pass the same value as the worker's
285
+ * `jurisdiction` so scheduled state co-resides with app data. Omit for the
286
+ * un-pinned global namespace.
287
+ */
165
288
  jurisdiction?: DurableObjectJurisdiction;
166
- /** Binding to the `SchedulerDO` durable object namespace. */
167
- namespace: DurableObjectNamespaceLike;
168
289
  /**
169
- * Origin where the Worker is mounted. SchedulerDO uses this base URL when
170
- * dispatching scheduled functions back to the Worker on alarm fire.
171
- */
172
- originUrl: string;
290
+ * Binding to the `SchedulerDO` durable object namespace.
291
+ *
292
+ * The origin the DO dispatches back to is NOT passed here: it reads
293
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, because a
294
+ * caller-supplied dispatch target would be an SSRF vector.
295
+ */
296
+ namespace: DurableObjectNamespaceLike;
173
297
  }
174
298
  /** Per-enqueue options for a {@link Workpool}. Extends {@link RunOptions} minus the implicit `pool` (the pool sets that). */
175
299
  interface EnqueueOptions {
@@ -181,46 +305,46 @@ interface EnqueueOptions {
181
305
  shardKey?: string;
182
306
  }
183
307
  /**
184
- * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
185
- * (same `namespace` / `originUrl` / `instanceName`) plus the bounded-concurrency
186
- * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
187
- * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
188
- * the scheduler already uses.
189
- */
308
+ * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
309
+ * (same `namespace` / `instanceName`) plus the bounded-concurrency
310
+ * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
311
+ * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
312
+ * the scheduler already uses.
313
+ */
190
314
  interface WorkpoolOptions extends LunoraSchedulerOptions {
191
315
  /**
192
- * Maximum number of jobs from this pool that may be in flight at once.
193
- * Excess enqueues are persisted and drain as slots free. Must be a positive
194
- * integer.
195
- */
316
+ * Maximum number of jobs from this pool that may be in flight at once.
317
+ * Excess enqueues are persisted and drain as slots free. Must be a positive
318
+ * integer.
319
+ */
196
320
  maxConcurrency: number;
197
321
  /**
198
- * Pool name — the concurrency counter is keyed by this inside the
199
- * SchedulerDO storage (`pool:&lt;name>`). Default `default`.
200
- */
322
+ * Pool name — the concurrency counter is keyed by this inside the
323
+ * SchedulerDO storage (`pool:<name>`). Default `default`.
324
+ */
201
325
  name?: string;
202
326
  }
203
327
  /**
204
- * Bounded-concurrency action queue (Lunora equivalent of `@convex-dev/workpool`).
205
- * Built on the existing SchedulerDO: `enqueue` schedules a job tagged with this
206
- * pool's name; the DO caps simultaneous dispatch at `maxConcurrency` and queues
207
- * the rest durably.
208
- */
328
+ * Bounded-concurrency action queue (Lunora equivalent of `@convex-dev/workpool`).
329
+ * Built on the existing SchedulerDO: `enqueue` schedules a job tagged with this
330
+ * pool's name; the DO caps simultaneous dispatch at `maxConcurrency` and queues
331
+ * the rest durably.
332
+ */
209
333
  interface Workpool {
210
334
  /** Cancel a queued/in-flight pool job by id. */
211
335
  cancel: (id: string) => Promise<{
212
336
  cancelled: boolean;
213
337
  }>;
214
338
  /**
215
- * Enqueue `function_(args)` into the pool. Resolves with the durable job id
216
- * and the time it was scheduled for (it may not run immediately if the pool
217
- * is at capacity).
218
- */
219
- enqueue: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: EnqueueOptions) => Promise<{
339
+ * Enqueue `function_(args)` into the pool. Resolves with the durable job id
340
+ * and the time it was scheduled for (it may not run immediately if the pool
341
+ * is at capacity).
342
+ */
343
+ enqueue: <F extends SchedulableReference>(function_: F, args: ArgsOf<F>, options?: EnqueueOptions) => Promise<{
220
344
  id: string;
221
345
  scheduledFor: number;
222
346
  }>;
223
- /** The pool's name (the `pool:&lt;name>` storage key suffix). */
347
+ /** The pool's name (the `pool:<name>` storage key suffix). */
224
348
  readonly name: string;
225
349
  /** Inspect the pool's current state — `inFlight` slots used and the configured `maxConcurrency`. */
226
350
  status: () => Promise<{
@@ -271,6 +395,13 @@ interface MessageBatchLike<Body = unknown> {
271
395
  }
272
396
  /** The wire payload Lunora puts on the queue: a function dispatch. */
273
397
  interface QueueJob {
398
+ /**
399
+ * The call's arguments in WIRE form (`shared/wire-codec`), so a `bigint`,
400
+ * `Date` or bytes survives the queue's own JSON serialisation. The producers
401
+ * encode; the shard's dispatch loop is the single decoder. A custom
402
+ * {@link QueueDispatch} must forward this untouched — decoding it here and
403
+ * letting the shard decode again flattens a `Date` to `{}`.
404
+ */
274
405
  args?: Record<string, unknown>;
275
406
  functionPath: string;
276
407
  /** Routing hint forwarded to the Worker so the call lands on the right shard. */
@@ -289,14 +420,14 @@ interface QueueWorkpoolOptions {
289
420
  queue: QueueLike<QueueJob>;
290
421
  }
291
422
  /**
292
- * Queues-backed producer: enqueue function dispatches onto a Cloudflare Queue.
293
- * Concurrency, retries, and dead-lettering are configured on the queue consumer
294
- * in `wrangler.jsonc` (`max_concurrency` / `max_retries` / `dead_letter_queue`),
295
- * not here — that's the whole point of using Queues over the DO workpool.
296
- */
423
+ * Queues-backed producer: enqueue function dispatches onto a Cloudflare Queue.
424
+ * Concurrency, retries, and dead-lettering are configured on the queue consumer
425
+ * in `wrangler.jsonc` (`max_concurrency` / `max_retries` / `dead_letter_queue`),
426
+ * not here — that's the whole point of using Queues over the DO workpool.
427
+ */
297
428
  interface QueueWorkpool {
298
429
  /** Enqueue a single `fn(args)` dispatch. */
299
- enqueue: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: QueueEnqueueOptions) => Promise<void>;
430
+ enqueue: <F extends SchedulableReference>(function_: F, args: ArgsOf<F>, options?: QueueEnqueueOptions) => Promise<void>;
300
431
  /** Enqueue many dispatches in one `sendBatch`. Each job names its function `ref`. */
301
432
  enqueueBatch: (jobs: ReadonlyArray<{
302
433
  args?: Record<string, unknown>;
@@ -304,8 +435,12 @@ interface QueueWorkpool {
304
435
  shardKey?: string;
305
436
  }>, options?: QueueSendOptionsLike) => Promise<void>;
306
437
  }
307
- /** Dispatches a single {@link QueueJob} — the consumer's per-message worker. */
308
- type QueueDispatch = (job: QueueJob) => Promise<void>;
438
+ /**
439
+ * Dispatches a single {@link QueueJob} — the consumer's per-message worker.
440
+ * `messageId` is the queue message's native id, threaded through so the
441
+ * dispatcher can attribute a failure to the exact message that caused it.
442
+ */
443
+ type QueueDispatch = (job: QueueJob, messageId?: string) => Promise<void>;
309
444
  /** Options for `createQueueConsumer`. */
310
445
  interface QueueConsumerOptions {
311
446
  /** How each job is executed; e.g. the `httpDispatcher`. */
@@ -319,43 +454,50 @@ interface HttpDispatcherOptions {
319
454
  fetchImpl?: typeof fetch;
320
455
  /** Origin where the Worker is mounted (the `/_lunora/scheduler/dispatch` endpoint). */
321
456
  originUrl: string;
457
+ /**
458
+ * Abort a job's dispatch after this many ms; the abort is retryable, so the
459
+ * consumer retries the message. Defaults to 5 minutes — raise it for a
460
+ * workpool running jobs that legitimately run longer, lower it to fail a
461
+ * stuck origin faster.
462
+ */
463
+ timeoutMs?: number;
322
464
  }
323
465
  /**
324
- * Client-side scheduler — forwards `runAfter` / `runAt` / `cancel` calls to a
325
- * `SchedulerDO` over HTTP. The DO owns the alarm and the storage; this is a
326
- * thin RPC wrapper.
327
- */
466
+ * Client-side scheduler — forwards `runAfter` / `runAt` / `cancel` calls to a
467
+ * `SchedulerDO` over HTTP. The DO owns the alarm and the storage; this is a
468
+ * thin RPC wrapper.
469
+ */
328
470
  declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
329
471
  /**
330
- * Bounded-concurrency action queue — the Lunora equivalent of
331
- * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
332
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
333
- * a workpool is just a NAMED logical pool inside that DO (concurrency counter
334
- * keyed by {@link WorkpoolOptions.name} under the `pool:&lt;name>` storage key).
335
- * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
336
- * the scheduler already uses.
337
- *
338
- * `enqueue` schedules a job tagged with this pool; the DO dispatches at most
339
- * `maxConcurrency` of the pool's jobs at once and queues the rest durably,
340
- * draining them as the runtime reports completions (`POST /complete`).
341
- *
342
- * ```ts
343
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
344
- * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
345
- * ```
346
- *
347
- * Why not Cloudflare Queues? Queues natively cover concurrency-capped, retried,
348
- * dead-lettered, delayed dispatch (`max_concurrency`, `max_retries`,
349
- * `retry({ delaySeconds })`, `dead_letter_queue`), and are the right tool when
350
- * you just want to rate-limit fire-and-forget background work. This workpool
351
- * deliberately stays on `SchedulerDO` because it offers what a queue can't: a
352
- * hard concurrency cap (the DO is the single serialization point — no
353
- * cross-consumer overshoot), per-job cancellation, and per-job status
354
- * introspection, all keyed by a stable job id. Reach for Queues when you don't
355
- * need those; reach for this when you do. Either way, do NOT grow multi-step
356
- * orchestration on top of this — that's Cloudflare **Workflows** (`step.do` /
357
- * `step.sleep` / `step.waitForEvent`).
358
- */
472
+ * Bounded-concurrency action queue — the Lunora equivalent of
473
+ * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
474
+ * `instanceName` options and is built on the SAME `SchedulerDO`:
475
+ * a workpool is just a NAMED logical pool inside that DO (concurrency counter
476
+ * keyed by {@link WorkpoolOptions.name} under the `pool:<name>` storage key).
477
+ * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
478
+ * the scheduler already uses.
479
+ *
480
+ * `enqueue` schedules a job tagged with this pool; the DO dispatches at most
481
+ * `maxConcurrency` of the pool's jobs at once and queues the rest durably,
482
+ * draining them as the runtime reports completions (`POST /complete`).
483
+ *
484
+ * ```ts
485
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, maxConcurrency: 5 });
486
+ * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
487
+ * ```
488
+ *
489
+ * Why not Cloudflare Queues? Queues natively cover concurrency-capped, retried,
490
+ * dead-lettered, delayed dispatch (`max_concurrency`, `max_retries`,
491
+ * `retry({ delaySeconds })`, `dead_letter_queue`), and are the right tool when
492
+ * you just want to rate-limit fire-and-forget background work. This workpool
493
+ * deliberately stays on `SchedulerDO` because it offers what a queue can't: a
494
+ * hard concurrency cap (the DO is the single serialization point — no
495
+ * cross-consumer overshoot), per-job cancellation, and per-job status
496
+ * introspection, all keyed by a stable job id. Reach for Queues when you don't
497
+ * need those; reach for this when you do. Either way, do NOT grow multi-step
498
+ * orchestration on top of this — that's Cloudflare **Workflows** (`step.do` /
499
+ * `step.sleep` / `step.waitForEvent`).
500
+ */
359
501
  declare const createWorkpool: (options: WorkpoolOptions) => Workpool;
360
502
  interface CronTriggerOptions {
361
503
  /** Args passed to the function. */
@@ -377,15 +519,25 @@ interface CronTriggerSnippet {
377
519
  wranglerJsonc: string;
378
520
  }
379
521
  /**
380
- * Produces the wrangler.jsonc fragment + dispatcher metadata for a recurring
381
- * function. The actual cron handler is mounted by `@lunora/runtime` — we only
382
- * emit the configuration here.
383
- */
522
+ * Produces the wrangler.jsonc fragment + dispatcher metadata for a recurring
523
+ * function. The actual cron handler is mounted by `@lunora/runtime` — we only
524
+ * emit the configuration here.
525
+ */
384
526
  declare const createCronTrigger: (options: CronTriggerOptions) => CronTriggerSnippet;
385
527
  /** Sub-day recurrence. Exactly one unit must be provided. */
386
528
  interface IntervalSchedule {
529
+ /** 1–23, and must divide 24 (an interval repeats *within* a day). */
387
530
  hours?: number;
531
+ /** 1–59, and must divide 60. */
388
532
  minutes?: number;
533
+ /**
534
+ * Accepted by the type, **rejected at definition time**: Cloudflare Cron
535
+ * Triggers have a one-minute floor, so the 6-field expression this compiles
536
+ * to would survive codegen and land in the committed `wrangler.jsonc`, then
537
+ * fail at `wrangler deploy` naming neither the job nor the file. Declared so
538
+ * the rejection can name the job instead. Use `ctx.scheduler.runAfter`/
539
+ * `runAt` for sub-minute recurrence, or `{ minutes: 1 }`.
540
+ */
389
541
  seconds?: number;
390
542
  }
391
543
  /** Daily recurrence at a fixed UTC wall-clock time. */
@@ -395,6 +547,19 @@ interface DailySchedule {
395
547
  /** 0–59. */
396
548
  minuteUTC: number;
397
549
  }
550
+ /**
551
+ * Hourly recurrence at a fixed minute past the hour.
552
+ *
553
+ * `crons.interval({ hours: 1 })` compiles to the same expression, but the
554
+ * asymmetry of having `daily`/`weekly`/`monthly` and no `hourly` is its own
555
+ * papercut — and unlike the interval form this one lets
556
+ * the caller place the job off the hour boundary, which is how you stop a
557
+ * dozen hourly jobs from stampeding at `:00`.
558
+ */
559
+ interface HourlySchedule {
560
+ /** 0–59. */
561
+ minuteUTC: number;
562
+ }
398
563
  /** Weekly recurrence at a fixed UTC time on a given weekday. */
399
564
  interface WeeklySchedule extends DailySchedule {
400
565
  /** Long weekday name, case-insensitive (e.g. `"monday"`). */
@@ -406,106 +571,156 @@ interface MonthlySchedule extends DailySchedule {
406
571
  day: number;
407
572
  }
408
573
  /**
409
- * One registered cron job, normalized to a compiled cron expression. Shared
410
- * verbatim with `@lunora/codegen` (which lifts the same fields out of the AST)
411
- * and the runtime dispatcher — keep the shape stable across all three.
412
- */
574
+ * One registered cron job, normalized to a compiled cron expression. Shared
575
+ * verbatim with `@lunora/codegen` (which lifts the same fields out of the AST)
576
+ * and the runtime dispatcher — keep the shape stable across all three.
577
+ */
413
578
  interface CronJob {
414
579
  /** Args forwarded to the function (or, for a workflow target, used as its `params`) on each fire. */
415
580
  args: Record<string, unknown>;
416
581
  /** Compiled standard cron expression, e.g. `"0 9 * * *"`. */
417
582
  cron: string;
418
583
  /**
419
- * `__lunoraRef` of the target function. Present for a function target;
420
- * absent when the job targets a workflow ({@link CronJob.workflow} instead).
421
- */
584
+ * `__lunoraRef` of the target function. Present for a function target;
585
+ * absent when the job targets a workflow ({@link CronJob.workflow} instead).
586
+ */
422
587
  functionPath?: string;
423
588
  /** Human-readable identifier — must be unique within one `cronJobs()`. */
424
589
  name: string;
425
590
  /**
426
- * Set when the job targets a durable workflow rather than a function: the
427
- * workflow's stable name (`defineWorkflow({ name })`) when one was declared,
428
- * otherwise `""`. `@lunora/codegen` statically resolves the concrete
429
- * `lunora/workflows.ts` export + its `WORKFLOW_*` binding for the emitted
430
- * dispatch map, so this authoring-time value is informational only.
431
- */
591
+ * Set when the job targets a durable workflow rather than a function: the
592
+ * workflow's stable name (`defineWorkflow({ name })`) when one was declared,
593
+ * otherwise `""`. `@lunora/codegen` statically resolves the concrete
594
+ * `lunora/workflows.ts` export + its `WORKFLOW_*` binding for the emitted
595
+ * dispatch map, so this authoring-time value is informational only.
596
+ */
432
597
  workflow?: string;
433
598
  }
434
599
  /** The ergonomic builder methods, excluding the raw `.cron` escape hatch. */
435
- type CronScheduleKind = "daily" | "interval" | "monthly" | "weekly";
600
+ type CronScheduleKind = "daily" | "hourly" | "interval" | "monthly" | "weekly";
436
601
  /** The ergonomic schedule kinds as a runtime set (codegen reads this to detect cron builder methods). */
437
602
  declare const CRON_SCHEDULE_KINDS: ReadonlySet<CronScheduleKind>;
438
603
  /**
439
- * Compile one of the ergonomic schedule forms into a standard cron expression.
440
- * Exposed as a pure function so `@lunora/codegen` can reuse the exact same
441
- * compilation when it statically lifts a `crons.{kind}(...)` call out of the
442
- * AST — codegen imports this directly (no duplicated mirror).
443
- */
444
- declare const compileCronSchedule: (kind: CronScheduleKind, schedule: DailySchedule | IntervalSchedule | MonthlySchedule | WeeklySchedule) => string;
445
- /**
446
- * Builder returned by {@link cronJobs}. Each method registers one recurring
447
- * job; the compiled expression is validated immediately so authoring mistakes
448
- * surface at definition time rather than at codegen.
449
- */
604
+ * Compile one of the ergonomic schedule forms into a standard cron expression.
605
+ * Exposed as a pure function so `@lunora/codegen` can reuse the exact same
606
+ * compilation when it statically lifts a `crons.{kind}(...)` call out of the
607
+ * AST — codegen imports this directly (no duplicated mirror). `jobName` is
608
+ * optional (see {@link compileInterval}) so codegen's existing 2-arg call site
609
+ * keeps working unchanged.
610
+ */
611
+ declare const compileCronSchedule: (kind: CronScheduleKind, schedule: DailySchedule | HourlySchedule | IntervalSchedule | MonthlySchedule | WeeklySchedule, jobName?: string) => string;
612
+ /**
613
+ * Builder returned by {@link cronJobs}. Each method registers one recurring
614
+ * job; the compiled expression is validated immediately so authoring mistakes
615
+ * surface at definition time rather than at codegen.
616
+ */
450
617
  interface CronJobsBuilder {
451
618
  /**
452
- * Raw cron expression escape hatch (5- or 6-field, full cron-parser grammar).
453
- * The target may be a function (`internal.file.fn`) or a durable workflow
454
- * (`workflows.&lt;name>`); a workflow's `args` are inferred from its `params`.
455
- */
619
+ * Raw cron expression escape hatch (5- or 6-field, full cron-parser grammar).
620
+ * The target may be a function (`internal.file.fn`) or a durable workflow
621
+ * (`workflows.<name>`); a workflow's `args` are inferred from its `params`.
622
+ */
456
623
  cron: <T extends CronTarget>(name: string, cronExpr: string, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
457
- /** Daily at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
624
+ /** Daily at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.<name>`). */
458
625
  daily: <T extends CronTarget>(name: string, schedule: DailySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
459
- /** Every `{ seconds | minutes | hours }`. The target may be a function or a durable workflow (`workflows.&lt;name>`). */
626
+ /** Hourly at `minuteUTC` past the hour. The target may be a function or a durable workflow (`workflows.<name>`). */
627
+ hourly: <T extends CronTarget>(name: string, schedule: HourlySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
628
+ /** Every `{ minutes | hours }` — `{ seconds }` throws (Cron Triggers have a one-minute floor). The target may be a function or a durable workflow (`workflows.<name>`). */
460
629
  interval: <T extends CronTarget>(name: string, schedule: IntervalSchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
461
630
  /** Snapshot of the registered jobs, in declaration order. */
462
631
  jobs: () => ReadonlyArray<CronJob>;
463
- /** Monthly on `day` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
632
+ /** Monthly on `day` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.<name>`). */
464
633
  monthly: <T extends CronTarget>(name: string, schedule: MonthlySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
465
- /** Weekly on `dayOfWeek` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
634
+ /** Weekly on `dayOfWeek` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.<name>`). */
466
635
  weekly: <T extends CronTarget>(name: string, schedule: WeeklySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
467
636
  }
468
637
  /**
469
- * Create a code-first cron registry. The returned builder is chainable;
470
- * codegen discovers a `lunora/crons.ts` default export by AST, not a runtime
471
- * brand.
472
- */
638
+ * Create a code-first cron registry. The returned builder is chainable;
639
+ * codegen discovers a `lunora/crons.ts` default export by AST, not a runtime
640
+ * brand.
641
+ */
473
642
  declare const cronJobs: () => CronJobsBuilder;
474
643
  /**
475
- * Build a Queues producer that enqueues Lunora function dispatches. Concurrency
476
- * and retry policy live on the consumer's `wrangler.jsonc` config, not here.
477
- */
644
+ * Build a Queues producer that enqueues Lunora function dispatches. Concurrency
645
+ * and retry policy live on the consumer's `wrangler.jsonc` config, not here.
646
+ */
478
647
  declare const createQueueWorkpool: (options: QueueWorkpoolOptions) => QueueWorkpool;
479
648
  /**
480
- * Wrap a {@link QueueDispatch} into a Cloudflare `queue()` consumer handler.
481
- *
482
- * Each message is dispatched independently (concurrently across the batch). On
483
- * success the message is `ack()`-ed; on any failure — a thrown dispatcher or a
484
- * structurally-invalid body — it is `retry()`-ed, so Queues' own `max_retries`
485
- * + `dead_letter_queue` settings decide when to give up. Nothing is silently
486
- * dropped: a permanently-bad message rides retries into the dead-letter queue
487
- * where you can inspect it.
488
- */
649
+ * Wrap a {@link QueueDispatch} into a Cloudflare `queue()` consumer handler.
650
+ *
651
+ * Each message is dispatched independently (concurrently across the batch). On
652
+ * success the message is `ack()`-ed; on any failure — a thrown dispatcher or a
653
+ * structurally-invalid body — it is `retry()`-ed, so Queues' own `max_retries`
654
+ * + `dead_letter_queue` settings decide when to give up. Nothing is silently
655
+ * dropped: a permanently-bad message rides retries into the dead-letter queue
656
+ * where you can inspect it.
657
+ */
489
658
  declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: MessageBatchLike) => Promise<void>);
490
659
  /**
491
- * Default {@link QueueDispatch}: POST each job to the Worker's
492
- * `/_lunora/scheduler/dispatch` endpoint (the same path SchedulerDO dispatches
493
- * through), authenticated with the admin bearer. A non-2xx response throws so
494
- * the consumer retries the message.
495
- */
660
+ * Default {@link QueueDispatch}: dispatch each job to the Worker's
661
+ * `/_lunora/scheduler/dispatch` endpoint (the same path SchedulerDO dispatches
662
+ * through) via `@lunora/dispatch`'s `createDispatchRunner` — which bounds each
663
+ * job with {@link HttpDispatcherOptions.timeoutMs} (default
664
+ * {@link DEFAULT_JOB_TIMEOUT_MS}), so a hung origin no longer holds the whole
665
+ * `queue()` invocation open, and threads the queue message id through for
666
+ * failure attribution. Any dispatch failure throws so the consumer retries the
667
+ * message — including a 2xx carrying a non-empty non-JSON body, which is an
668
+ * intermediary's page rather than a function's return value and therefore no
669
+ * evidence the job ran. An empty 2xx is a normal success (a `void` function).
670
+ *
671
+ * `job.args` is forwarded VERBATIM: {@link encodeJobArgs} already put it in wire
672
+ * form at the producer, and the shard's dispatch loop is the single decoder.
673
+ * Encoding again here would leave the handler a tagged array.
674
+ */
496
675
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
497
676
  /**
498
- * Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
499
- * structurally so unit tests can pass a fake state without booting the
500
- * workers runtime. The WebSocket methods are optional: they back the live
501
- * `/ws` subscription (push the job list on every change) and are absent in the
502
- * storage-only fakes, in which case the DO simply serves no live sockets.
503
- */
677
+ * The id a new record is stored under: the caller's, or a freshly minted one when
678
+ * they did not name it.
679
+ *
680
+ * A caller id that is not a safe key segment is REFUSED, not replaced.
681
+ * `RunOptions.id` is not an idempotency key — an id already scheduled answers
682
+ * `409 DUPLICATE_SCHEDULE_ID` — so quietly swapping an invalid one for a random
683
+ * id made `runAt(ts, ref, args, { id: "-daily-2026-09-06" })` mint a fresh id on
684
+ * every call and run the job once per call, where naming it was the caller's way
685
+ * of saying "at most once".
686
+ *
687
+ * Minting swaps a leading `-` for `_` rather than re-rolling: 1 in 64 minted ids
688
+ * led with one and was refused by the workflow engine on dispatch. `_` is in the
689
+ * engine's leading class, the swap is a single pass, and 96 random bits stay 96
690
+ * random bits everywhere but that first character.
691
+ *
692
+ * Exported because two surfaces have to agree on it. The SchedulerDO applies it
693
+ * when the record is written (turning the refusal into a `400`);
694
+ * `@lunora/server`'s deferred-schedule facade applies it when the call is
695
+ * BUFFERED, because it answers the handler with the id synchronously — long
696
+ * before the DO sees the request. Restating the rule in the facade is how the two
697
+ * drift: an id the facade accepted and the DO replaced leaves the handler holding
698
+ * an id no job was ever stored under, so its later `cancel` silently misses.
699
+ * @param requested the caller's `RunOptions.id`, if any
700
+ * @throws LunoraError `INVALID_SCHEDULE_ID` when `requested` is supplied and is not a safe key segment
701
+ */
702
+ declare const resolveScheduleId: (requested: unknown) => string;
703
+ /**
704
+ * Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
705
+ * structurally so unit tests can pass a fake state without booting the
706
+ * workers runtime. The WebSocket methods are optional: they back the live
707
+ * `/ws` subscription (push the job list on every change) and are absent in the
708
+ * storage-only fakes, in which case the DO simply serves no live sockets.
709
+ */
504
710
  interface SchedulerDOState {
505
711
  /** Accept a hibernatable server WebSocket (workers `state.acceptWebSocket`). */
506
712
  acceptWebSocket?: (ws: WebSocket) => void;
507
713
  /** Every accepted server WebSocket (workers `state.getWebSockets`). */
508
714
  getWebSockets?: () => WebSocket[];
715
+ /**
716
+ * Register a constant ping/pong auto-response so the runtime answers a
717
+ * known keepalive frame on a hibernated socket WITHOUT waking this DO (no
718
+ * billable request, no dispatch). Optional: absent in the unit harness and
719
+ * older runtimes, present on the real `DurableObjectState`. Mirrors
720
+ * `@lunora/do`'s `ShardDOState.setWebSocketAutoResponse` — see
721
+ * {@link SchedulerDO.armWebSocketKeepalive}.
722
+ */
723
+ setWebSocketAutoResponse?: (pair: WebSocketRequestResponsePair) => void;
509
724
  storage: {
510
725
  delete: (key: string | string[]) => Promise<number | boolean>;
511
726
  deleteAlarm: () => Promise<void> | void;
@@ -515,6 +730,7 @@ interface SchedulerDOState {
515
730
  end?: string;
516
731
  limit?: number;
517
732
  prefix?: string;
733
+ startAfter?: string;
518
734
  }) => Promise<Map<string, T>>;
519
735
  put: <T = unknown>(entries: Record<string, T> | string, value?: T) => Promise<void>;
520
736
  setAlarm: (scheduledTime: number | Date) => Promise<void> | void;
@@ -523,281 +739,524 @@ interface SchedulerDOState {
523
739
  interface SchedulerEnv {
524
740
  [key: string]: unknown;
525
741
  /**
526
- * Fallback bearer token attached to the dispatch when
527
- * {@link SchedulerEnv.LUNORA_SCHEDULER_SECRET} is not configured. Sent as
528
- * `authorization: Bearer &lt;token>`.
529
- */
742
+ * Fallback bearer token attached to the dispatch when
743
+ * {@link SchedulerEnv.LUNORA_SCHEDULER_SECRET} is not configured. Sent as
744
+ * `authorization: Bearer <token>`.
745
+ */
530
746
  LUNORA_ADMIN_TOKEN?: string;
531
747
  /**
532
- * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
533
- * time to call back into the Worker. Read at fire time (NOT taken from the
534
- * request body) to prevent SSRF via a forged `originUrl` field.
535
- */
748
+ * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
749
+ * time to call back into the Worker. Read at fire time (NOT taken from the
750
+ * request body, which carries no dispatch target at all) to prevent SSRF.
751
+ */
536
752
  LUNORA_ORIGIN_URL?: string;
537
753
  /**
538
- * Shared secret used to HMAC-sign the dispatch body so the runtime receiver
539
- * can authenticate the call (header `x-lunora-scheduler-signature`). Without
540
- * it the dispatch is sent unsigned (optionally bearer-authenticated via
541
- * {@link SchedulerEnv.LUNORA_ADMIN_TOKEN}).
542
- */
754
+ * Shared secret used to HMAC-sign the dispatch body so the runtime receiver
755
+ * can authenticate the call (header `x-lunora-scheduler-signature`). Without
756
+ * it the dispatch is sent unsigned (optionally bearer-authenticated via
757
+ * {@link SchedulerEnv.LUNORA_ADMIN_TOKEN}).
758
+ */
543
759
  LUNORA_SCHEDULER_SECRET?: string;
544
760
  }
545
761
  /**
546
- * One pool's live backlog, as surfaced by `GET /status`. `inFlight`/
547
- * `maxConcurrency` mirror the durable {@link PoolState} semaphore; `queued`
548
- * is the number of pending (not-yet-dispatched) jobs routed to this pool.
549
- */
762
+ * Retries allowed after the original attempt before {@link SchedulerDO.recordRetry}
763
+ * parks a record in the dead-letter (`dead:`) prefix. Overridable per job via
764
+ * {@link RetryPolicy.maxAttempts}. Exported so test doubles of the scheduler
765
+ * (`@lunora/testing`'s fake scheduler) model the same budget instead of
766
+ * duplicating the number.
767
+ */
768
+ declare const MAX_RETRY_ATTEMPTS = 5;
769
+ /**
770
+ * Backoff before the first retry, in milliseconds; doubles on each subsequent
771
+ * retry under the default `"exponential"` backoff (`baseMs * 2 ** (attempts - 1)`).
772
+ * Overridable per job via {@link RetryPolicy.baseMs}. Exported alongside
773
+ * {@link MAX_RETRY_ATTEMPTS} for the same reason.
774
+ */
775
+ declare const RETRY_BASE_DELAY_MS = 3e4;
776
+ /**
777
+ * One pool's live backlog, as surfaced by `GET /status`. `inFlight`/
778
+ * `maxConcurrency` mirror the durable {@link PoolState} semaphore; `queued`
779
+ * is the number of pending (not-yet-dispatched) jobs routed to this pool.
780
+ */
550
781
  interface SchedulerPoolStatus {
551
782
  /** Jobs currently dispatched-but-not-yet-completed (the held slots). */
552
783
  inFlight: number;
553
784
  /** The pool's concurrency cap. */
554
785
  maxConcurrency: number;
555
- /** The logical workpool name (the `pool:&lt;name>` suffix). */
786
+ /** The logical workpool name (the `pool:<name>` suffix). */
556
787
  name: string;
557
788
  /** Pending jobs routed to this pool but not yet dispatched. */
558
789
  queued: number;
559
790
  }
560
791
  /**
561
- * App-level scheduler backlog, as returned by `GET /status`. `pools` carries
562
- * the per-pool breakdown; `backlog` and `inFlight` are the app-wide sums of
563
- * `queued` and `inFlight` across every pool — the SLO view's headline numbers.
564
- */
792
+ * App-level scheduler backlog, as returned by `GET /status`. `pools` carries
793
+ * the per-pool breakdown; `backlog` and `inFlight` are the app-wide sums of
794
+ * `queued` and `inFlight` across every pool — the SLO view's headline numbers.
795
+ */
565
796
  interface SchedulerStatus {
566
797
  /** Sum of every pool's `queued` count — the total pending backlog. */
567
798
  backlog: number;
568
799
  /** Sum of every pool's `inFlight` count — the total held concurrency slots. */
569
800
  inFlight: number;
570
- /** Per-pool backlog breakdown, one entry per `pool:&lt;name>` record. */
801
+ /** Per-pool backlog breakdown, one entry per `pool:<name>` record. */
571
802
  pools: SchedulerPoolStatus[];
572
803
  }
573
804
  /**
574
- * Durable Object that stores pending scheduled invocations sorted by their
575
- * `scheduledFor` time and fires them via HTTP on alarm. Storage layout:
576
- * `id:&lt;id>` maps to {@link ScheduleRecord}; `t:&lt;paddedTime>:&lt;id>` maps to the
577
- * id (used as a sorted index).
578
- *
579
- * On every mutation the DO recomputes the earliest pending task and updates
580
- * the alarm via `state.storage.setAlarm(time)`.
581
- */
805
+ * Durable Object that stores pending scheduled invocations sorted by their
806
+ * `scheduledFor` time and fires them via HTTP on alarm. Storage layout:
807
+ * `id:<id>` maps to {@link ScheduleRecord}; `t:<paddedTime>:<id>` maps to the
808
+ * id (used as a sorted index).
809
+ *
810
+ * On every mutation the DO recomputes the earliest pending task and updates
811
+ * the alarm via `state.storage.setAlarm(time)`.
812
+ */
582
813
  declare class SchedulerDO {
583
814
  private static indexKey;
584
815
  private static json;
585
816
  private static error;
586
817
  /**
587
- * Resolve the effective retry parameters for a record: its per-job
588
- * {@link RetryPolicy} merged over the DO's built-in defaults. Callers that
589
- * never set `record.retry` get today's behaviour verbatim
590
- * (`maxAttempts: 5`, exponential, `baseMs: 30_000`, no ceiling).
591
- */
818
+ * Resolve the effective retry parameters for a record: its per-job
819
+ * {@link RetryPolicy} merged over the DO's built-in defaults. Callers that
820
+ * never set `record.retry` get today's behaviour verbatim
821
+ * (`maxAttempts: 5`, exponential, `baseMs: 30_000`, no ceiling).
822
+ */
592
823
  private static resolveRetry;
593
824
  /** Clamp an untrusted `maxConcurrency` to a positive integer, else fall back. */
594
825
  private static normalizeConcurrency;
595
826
  /**
596
- * Sanitize an untrusted retry policy from the wire into a `RetryPolicy` (or
597
- * `undefined` when nothing valid was provided). Keeps obviously-bad values
598
- * out of storage so {@link SchedulerDO.resolveRetry} never has to re-guard.
599
- * @returns The normalized policy, or `undefined` if no valid policy was found.
600
- */
827
+ * Sanitize an untrusted retry policy from the wire into a `RetryPolicy` (or
828
+ * `undefined` when nothing valid was provided). Keeps obviously-bad values
829
+ * out of storage so {@link SchedulerDO.resolveRetry} never has to re-guard.
830
+ * @returns The normalized policy, or `undefined` if no valid policy was found.
831
+ */
601
832
  private static normalizeRetry;
602
833
  /**
603
- * Idempotently release the slot held by `jobId`, returning the updated
604
- * {@link PoolState} (pure — the caller persists it). A duplicate release for
605
- * an id that no longer holds a slot is a no-op, so an at-least-once
606
- * `/complete` (or a complete racing a failed-kick release) can never push
607
- * `inFlight` below the true number of running jobs and oversubscribe the
608
- * pool. Pools persisted before `inFlightIds` existed fall back to a clamped
609
- * counter decrement.
610
- */
834
+ * Idempotently release the slot held by `jobId`, returning the updated
835
+ * {@link PoolState} (pure — the caller persists it). A duplicate release for
836
+ * an id that no longer holds a slot is a no-op, so an at-least-once
837
+ * `/complete` (or a complete racing a failed-kick release) can never push
838
+ * `inFlight` below the true number of running jobs and oversubscribe the
839
+ * pool. Pools persisted before `inFlightIds` existed fall back to a clamped
840
+ * counter decrement.
841
+ */
611
842
  private static releaseSlot;
612
843
  /**
613
- * Best-effort release with no job id (legacy `/complete` payloads). Drops one
614
- * tracked id if the set exists, else clamps the counter. Less precise than
615
- * {@link SchedulerDO.releaseSlot} — a duplicate id-less complete CAN
616
- * over-release — but every current client sends the id, so this is the
617
- * compatibility shim, not the hot path.
618
- */
844
+ * Best-effort release with no job id (legacy `/complete` payloads). Drops one
845
+ * tracked id if the set exists, else clamps the counter. Less precise than
846
+ * {@link SchedulerDO.releaseSlot} — a duplicate id-less complete CAN
847
+ * over-release — but every current client sends the id, so this is the
848
+ * compatibility shim, not the hot path.
849
+ */
619
850
  private static releaseFirstSlot;
851
+ /**
852
+ * Normalize the mutually-exclusive dispatch target off an untrusted body: a
853
+ * one-shot function path (`functionPath`) or a durable workflow/agent
854
+ * instance (`workflow`, a `WORKFLOW_*`/`AGENT_*` binding). Returns `undefined`
855
+ * when neither is present so the caller can reject the schedule.
856
+ */
857
+ private static resolveScheduleTarget;
620
858
  protected readonly state: SchedulerDOState;
621
859
  protected readonly env: SchedulerEnv;
860
+ /**
861
+ * Whether {@link SchedulerDO.reindexOrphanedRecords} has already run in THIS
862
+ * instance. Once is enough: an orphan can only be minted by an eviction, and
863
+ * an eviction ends the instance that minted it.
864
+ */
865
+ private reindexed;
622
866
  constructor(state: SchedulerDOState, env: SchedulerEnv);
623
867
  fetch(request: Request): Promise<Response>;
624
868
  /** Called by the Workers runtime when the alarm previously set by `_rescheduleAlarm()` fires. */
625
869
  alarm(): Promise<void>;
626
870
  /**
627
- * Internal dispatch hook; overridden in unit tests to capture the outgoing
628
- * request. Returns `true` ONLY on an explicit 2xx response (`response.ok`).
629
- * Anything else — a network failure, a 5xx, OR a non-2xx such as 404
630
- * (receiver route not mounted) / 401 / 403 / 4xx — returns `false` and
631
- * enters the retry pipeline via {@link recordRetry}. Treating 4xx as
632
- * success used to permanently delete the job; since the receiver may simply
633
- * be missing (404) or transiently failing, we retry rather than silently
634
- * drop. After {@link MAX_RETRY_ATTEMPTS} the record is parked under a
635
- * `dead:` key for inspection — never silently deleted.
636
- *
637
- * The dispatch target is taken from `env.LUNORA_ORIGIN_URL` (NOT from the
638
- * stored record) to prevent SSRF via a forged `originUrl` on the schedule
639
- * request. If that env var is missing at fire time (a deploy/binding
640
- * regression — schedule time already enforced its presence) we return
641
- * `false` so the record is retried rather than silently dropped.
642
- */
871
+ * Internal dispatch hook; overridden in unit tests to capture the outgoing
872
+ * request. Returns `true` ONLY on an explicit 2xx response (`response.ok`).
873
+ * Anything else — a network failure, a 5xx, OR a non-2xx such as 404
874
+ * (receiver route not mounted) / 401 / 403 / 4xx — returns `false` and
875
+ * enters the retry pipeline via {@link recordRetry}. Treating 4xx as
876
+ * success used to permanently delete the job; since the receiver may simply
877
+ * be missing (404) or transiently failing, we retry rather than silently
878
+ * drop. After {@link MAX_RETRY_ATTEMPTS} the record is parked under a
879
+ * `dead:` key for inspection — never silently deleted.
880
+ *
881
+ * The dispatch target is taken from `env.LUNORA_ORIGIN_URL` (NOT from the
882
+ * stored record) so a schedule request can never name where the DO calls
883
+ * back — that would be SSRF. If that env var is missing at fire time (a deploy/binding
884
+ * regression — schedule time already enforced its presence) we return
885
+ * `false` so the record is retried rather than silently dropped.
886
+ */
643
887
  protected dispatch(record: ScheduleRecord): Promise<boolean>;
644
888
  /**
645
- * Claim + drain one due record with per-record fault isolation, so a storage
646
- * throw can never abort the whole alarm pass (which would skip the remaining
647
- * due records and the `rescheduleAlarm()` that re-arms the clock).
648
- *
649
- * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
650
- * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
651
- * thrown storage op decides whether the job stays re-fireable.
652
- *
653
- * When the record was NOT successfully dispatched, re-assert the time-index
654
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
655
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
656
- * before throwing, and re-inserting the same key is idempotent, so a
657
- * surviving claim is simply rewritten to its prior value. When the record WAS
658
- * dispatched (the throw came from post-dispatch cleanup), leave the index
659
- * deleted so the already-kicked, idempotent job is not re-fired.
660
- */
889
+ * Register the hibernation-safe ping/pong keepalive. The runtime answers a
890
+ * {@link WS_KEEPALIVE_PING} text frame with {@link WS_KEEPALIVE_PONG}
891
+ * WITHOUT waking this Durable Object, keeping an idle `/ws` subscription
892
+ * alive across hibernation with no billable wakeup and no dispatch. Without
893
+ * this, a client's heartbeat ping goes unanswered and its watchdog force-
894
+ * closes the socket every ~90s, defeating hibernation (each unanswered ping
895
+ * wakes the DO to reconnect) — mirrors `@lunora/do`'s
896
+ * `ShardDO.armWebSocketKeepalive`. The auto-response is per-instance, so
897
+ * this re-runs on every construction (including a post-hibernation wake).
898
+ * Guarded: the API and the `WebSocketRequestResponsePair` global are absent
899
+ * in the unit harness and on older runtimes, where it degrades to a no-op.
900
+ */
901
+ private armWebSocketKeepalive;
902
+ /**
903
+ * Claim + drain one due record with per-record fault isolation, so a storage
904
+ * throw can never abort the whole alarm pass (which would skip the remaining
905
+ * due records and the `rescheduleAlarm()` that re-arms the clock).
906
+ *
907
+ * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
908
+ * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
909
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
910
+ *
911
+ * A throw reaching here always means the job was NOT dispatched:
912
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
913
+ * returns instead of throwing once a kick succeeds, so every escaping throw
914
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore re-assert
915
+ * the time-index claim so a later alarm re-attempts it (at-least-once): the
916
+ * claim delete may have removed it and recordRetry()/requeuePooled() may not
917
+ * have re-armed it before throwing, and re-inserting the same key is
918
+ * idempotent, so a surviving claim is simply rewritten to its prior value.
919
+ *
920
+ * With one exception, checked first: a record that already has a durable
921
+ * `dead:` row is TERMINAL, and re-claiming it would re-dispatch a job the
922
+ * dead-letter says is finished. See the comment on that branch.
923
+ */
661
924
  private drainRecordGuarded;
662
925
  /**
663
- * Process one due (already index-claimed) record within an alarm drain:
664
- * apply the workpool concurrency gate, dispatch, and settle the result.
665
- * A saturated pool re-arms the job (backpressure, no attempt charged); a
666
- * free slot is reserved durably before dispatch and released immediately if
667
- * the kick fails (success holds it until the runtime reports completion).
668
- * Success clears the `id:`/`retry:` rows; failure routes to
669
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
670
- * lifetime of the drain so the budget decrements without re-reading storage.
671
- * @returns `true` only when the record was successfully dispatched (a 2xx
672
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
673
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
674
- * later throws) must NOT have its time-index claim restored, since re-firing
675
- * an already-kicked job would break idempotency. A `false` return (pool
676
- * backpressure or a failed dispatch) means the job is still re-fireable —
677
- * either already re-armed here, or, if a throw escapes, re-claimed by the
678
- * guard's catch.
679
- */
926
+ * Process one due (already index-claimed) record within an alarm drain:
927
+ * apply the workpool concurrency gate, dispatch, and settle the result.
928
+ * A saturated pool re-arms the job (backpressure, no attempt charged); a
929
+ * free slot is reserved durably before dispatch and released immediately if
930
+ * the kick fails (success holds it until the runtime reports completion).
931
+ * Success clears the `id:`/`retry:` rows; failure routes to
932
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
933
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
934
+ * concurrent /complete landing mid-dispatch can't be clobbered.
935
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
936
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
937
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
938
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
939
+ * is still re-fireable and the guard safely re-claims the time index.
940
+ * @returns `true` only when the record was successfully dispatched (a 2xx
941
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
942
+ * re-fireable — already re-armed here). The value is informational (the guard
943
+ * branches on throw/no-throw, not on this boolean).
944
+ */
680
945
  private drainRecord;
681
946
  /**
682
- * Concurrency gate for a pooled record. Returns `false` (and re-arms the
683
- * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
684
- * otherwise reserves a slot durably and returns `true`. Non-pooled records
685
- * always return `true` without touching any pool state.
686
- */
947
+ * Concurrency gate for a pooled record. Returns `false` (and re-arms the
948
+ * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
949
+ * otherwise reserves a slot durably and returns `true`. Non-pooled records
950
+ * always return `true` without touching any pool state.
951
+ *
952
+ * The pool row is read FRESH from storage on every call — never cached
953
+ * across the drain. Each reservation durably `savePool()`s before the next
954
+ * record runs, so a same-pass reservation is still visible to the next
955
+ * record's fresh read (the budget carries forward); and because dispatch()
956
+ * awaits an outbound fetch between records, a concurrent /complete that
957
+ * decrements the row mid-drain IS reflected here instead of being clobbered
958
+ * by a stale in-memory copy (which would leak a slot permanently).
959
+ */
687
960
  private reservePoolSlot;
688
961
  /**
689
- * Accept a hibernatable live subscription to the job list. The scheduler has
690
- * exactly one subscription shape (the whole list), so there's no per-socket
691
- * registry or dependency tracking — every accepted socket gets the full list
692
- * on connect and on every change. The worker is responsible for gating the
693
- * upgrade behind the admin token before it reaches here.
694
- */
962
+ * Accept a hibernatable live subscription to the job list. The scheduler has
963
+ * exactly one subscription shape (the whole list), so there's no per-socket
964
+ * registry or dependency tracking — every accepted socket gets the full list
965
+ * on connect and on every change. The worker is responsible for gating the
966
+ * upgrade behind the admin token before it reaches here.
967
+ */
695
968
  private handleWebSocketUpgrade;
696
969
  /**
697
- * Re-list the jobs and push them to every connected subscriber. Called after
698
- * any change (schedule / cancel / alarm-fire) so live studios reflect it
699
- * immediately. A no-op when the runtime doesn't support hibernated sockets.
700
- */
970
+ * Re-list the jobs (bounded — see {@link listPage}) and push them to
971
+ * every connected subscriber. Called after any change (schedule / cancel /
972
+ * alarm-fire) so live studios reflect it immediately. A no-op when the
973
+ * runtime doesn't support hibernated sockets.
974
+ */
701
975
  private broadcastChange;
702
- /** The current pending job records (shared by `/list` and the live channel). */
703
- private listRecords;
704
- /**
705
- * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
706
- * returning a base64url signature, or `undefined` when no secret is
707
- * configured. Mirrors `@lunora/storage`'s signed-URL HMAC pattern (WebCrypto
708
- * `crypto.subtle`, available in workerd).
709
- */
976
+ /**
977
+ * One bounded page of the rows under `prefix`, in key order, plus the
978
+ * `cursor` a caller resumes from (the last key of the page) when `truncated`.
979
+ * Lists `limit + 1` and slices back down so both facts are known without a
980
+ * second round-trip.
981
+ *
982
+ * Shared by `/list` (pending headers) and `/dead` (dead-letter records) so
983
+ * NEITHER can materialize an unbounded set into one JSON response: nothing
984
+ * prunes `dead:`, so a workpool with a broken origin parks thousands of rows
985
+ * and the studio's only view of them — and only way to requeue them — would
986
+ * fail exactly when it is needed.
987
+ */
988
+ private listPage;
989
+ /**
990
+ * Page through every row under `prefix` exactly once with bounded per-page
991
+ * memory (a `limit`+`startAfter` cursor loop), invoking `visit` for each.
992
+ * Unlike {@link listPage}, which intentionally truncates for the studio's
993
+ * live view, `/status` and `/pool` need EXACT counts — this walks the full
994
+ * set, but never materializes more than one page at a time.
995
+ */
996
+ private forEachPage;
997
+ /**
998
+ * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
999
+ * returning a base64url signature, or `undefined` when no secret is
1000
+ * configured. Mirrors `@lunora/storage`'s signed-URL HMAC pattern (WebCrypto
1001
+ * `crypto.subtle`, available in workerd).
1002
+ */
710
1003
  private signDispatch;
711
1004
  /**
712
- * Move a failed record into the retry pipeline with configurable backoff.
713
- * The retry budget/backoff comes from the record's {@link RetryPolicy}
714
- * (falling back to the DO defaults); on exhaustion the record is parked
715
- * under a `dead:` key for manual inspection.
716
- */
1005
+ * Move a failed record into the retry pipeline with configurable backoff.
1006
+ * The retry budget/backoff comes from the record's {@link RetryPolicy}
1007
+ * (falling back to the DO defaults); on exhaustion the record is parked
1008
+ * under a `dead:` key for manual inspection.
1009
+ */
717
1010
  private recordRetry;
718
- /** Read the durable `pool:&lt;name>` row, defaulting to a fresh `inFlight: 0` pool. */
1011
+ /**
1012
+ * Terminal park into the dead-letter (`dead:`) prefix, with `reason` naming
1013
+ * why in the emitted warning. Shared by the two ways a retry ends for good:
1014
+ * an exhausted attempt budget, and a backoff that ran past the largest
1015
+ * schedulable time (see {@link isIndexableTime}).
1016
+ */
1017
+ private parkDead;
1018
+ /** Read the durable `pool:<name>` row, defaulting to a fresh `inFlight: 0` pool. */
719
1019
  private loadPool;
720
1020
  private savePool;
721
1021
  /**
722
- * Re-arm a pooled job that couldn't run because its pool was at capacity.
723
- * No attempt is charged (this is backpressure, not a failure): the job is
724
- * pushed `POOL_BACKPRESSURE_DELAY_MS` into the future so a later alarm
725
- * drains it once a slot frees, keeping its `id:` header and retry policy.
726
- */
1022
+ * Re-arm a pooled job that couldn't run because its pool was at capacity.
1023
+ * No attempt is charged (this is backpressure, not a failure): the job is
1024
+ * pushed `POOL_BACKPRESSURE_DELAY_MS` into the future so a later alarm
1025
+ * drains it once a slot frees, keeping its `id:` header and retry policy.
1026
+ */
727
1027
  private requeuePooled;
728
1028
  /**
729
- * Release a pool slot when the runtime reports an action finished. This is
730
- * the durable-semaphore decrement: dispatch() only KICKS the action and
731
- * holds the slot; the runtime calls back here (`POST /complete { id }`) once
732
- * the action settles, freeing the slot for the next queued job. Idempotent
733
- * and safe if the job/pool is already gone.
734
- */
1029
+ * Release a pool slot when the runtime reports an action finished. This is
1030
+ * the durable-semaphore decrement: dispatch() only KICKS the action and
1031
+ * holds the slot; the runtime calls back here (`POST /complete { id }`) once
1032
+ * the action settles, freeing the slot for the next queued job. Idempotent
1033
+ * and safe if the job/pool is already gone.
1034
+ */
735
1035
  private handleComplete;
736
1036
  /** `GET /pool?name=` — inspect a pool's slot usage + queued count. */
737
1037
  private handlePoolStatus;
738
1038
  /**
739
- * `GET /status` — the app-level backlog signal that powers the studio's
740
- * SLO view. Enumerates every durable `pool:&lt;name>` row for its `inFlight`/
741
- * `maxConcurrency` semaphore, counts the pending (not-yet-dispatched) jobs
742
- * routed to each pool with the same single-pass scan {@link handlePoolStatus}
743
- * uses, and rolls those up into app-wide `backlog` (sum of `queued`) and
744
- * `inFlight` (sum of held slots) totals.
745
- *
746
- * Pools that have rows but no queued jobs still appear (with `queued: 0`) so
747
- * a saturated-but-idle pool stays visible; a pool that only ever existed as
748
- * queued jobs without a persisted row is unreachable here (the schedule path
749
- * always writes a `pool:&lt;name>` row before the job's header), so a single
750
- * scan over `pool:`/`id:` is sufficient.
751
- */
1039
+ * `GET /status` — the app-level backlog signal that powers the studio's
1040
+ * SLO view. Enumerates every durable `pool:<name>` row for its `inFlight`/
1041
+ * `maxConcurrency` semaphore, counts the pending (not-yet-dispatched) jobs
1042
+ * routed to each pool with the same single-pass scan {@link handlePoolStatus}
1043
+ * uses, and rolls those up into app-wide `backlog` (sum of `queued`) and
1044
+ * `inFlight` (sum of held slots) totals.
1045
+ *
1046
+ * Pools that have rows but no queued jobs still appear (with `queued: 0`) so
1047
+ * a saturated-but-idle pool stays visible; a pool that only ever existed as
1048
+ * queued jobs without a persisted row is unreachable here (the schedule path
1049
+ * always writes a `pool:<name>` row before the job's header), so a single
1050
+ * scan over `pool:` plus a cursor loop over `id:` is sufficient.
1051
+ */
752
1052
  private handleStatus;
1053
+ /**
1054
+ * Persist (or refresh) a pool's concurrency cap, so the alarm-time gate has
1055
+ * a durable `maxConcurrency` even after the enqueuing client is gone.
1056
+ */
1057
+ private persistPoolCap;
1058
+ /**
1059
+ * The `409` a caller-supplied id earns when something durable already holds
1060
+ * it, or `undefined` when the id is free.
1061
+ *
1062
+ * A pending header is the obvious half: `put` on `id:<id>` overwrites, but
1063
+ * the `t:` index is keyed by TIME as well as id, so the OLD entry survives.
1064
+ * The drain then dispatches the NEW record at the OLD time and deletes the
1065
+ * entry it should have fired at — the job runs early and never runs again.
1066
+ * Refused rather than made a replace: `RunOptions.id` exists so a deferred
1067
+ * schedule can name its own job, and silently retiming someone else's is the
1068
+ * worse failure.
1069
+ *
1070
+ * The `dead:` row holds the id too, and for a worse reason. A dead record
1071
+ * keeps NO `id:` header, so a pending-only check leaves the id apparently
1072
+ * free — and a later `/dead/retry` writes the revived corpse straight over
1073
+ * the new job's header and adds a SECOND time index under the same id. The
1074
+ * new job is gone and the dead one fires in its place. Recovering a dead job
1075
+ * is an operator action taken minutes or days after the schedule, so nothing
1076
+ * at schedule time would ever have surfaced the collision.
1077
+ */
1078
+ private idConflict;
1079
+ /**
1080
+ * The id the record is stored under, or the `Response` refusing it.
1081
+ *
1082
+ * A caller id that is not a safe key segment is refused with a `400` rather
1083
+ * than minted over: `RunOptions.id` is not an idempotency key, so swapping an
1084
+ * invalid one for a random id made two calls naming it schedule two jobs
1085
+ * where the second should have answered `409`.
1086
+ *
1087
+ * Only an id the CALLER chose can collide — a minted one is 96 random bits —
1088
+ * so {@link idConflict} costs two `get`s on the deferred path and nothing on
1089
+ * the ordinary one.
1090
+ */
1091
+ private resolveId;
753
1092
  private handleSchedule;
754
1093
  private handleCancel;
1094
+ /**
1095
+ * `GET /list[?cursor=]` — one bounded page of pending jobs. `truncated` says
1096
+ * whether more rows follow and `cursor` is what a caller passes back to get
1097
+ * them (`createScheduler.list()` walks every page; the studio shows one).
1098
+ */
755
1099
  private handleList;
756
1100
  /**
757
- * `GET /dead` — list the dead-letter records: jobs that exhausted their
758
- * retry budget ({@link recordRetry}) and were parked under `dead:&lt;id>`
759
- * instead of being silently dropped. These never appear in `/list` (their
760
- * `id:` header is deleted on park), so this is the ONLY way the studio can
761
- * surface — and recover — a permanently-failed job.
762
- */
1101
+ * `GET /dead` — list the dead-letter records: jobs that exhausted their
1102
+ * retry budget ({@link recordRetry}) and were parked under `dead:<id>`
1103
+ * instead of being silently dropped. These never appear in `/list` (their
1104
+ * `id:` header is deleted on park), so this is the ONLY way the studio can
1105
+ * surface — and recover — a permanently-failed job. Bounded and cursored
1106
+ * like `/list`: nothing prunes `dead:`, so this set grows without limit.
1107
+ */
763
1108
  private handleDeadList;
764
1109
  /**
765
- * `POST /dead/retry { id }` — resurrect a dead-letter record: reset its
766
- * exhausted attempt count to 0 (a fresh retry budget), re-arm it for
767
- * immediate dispatch via the standard time index, and drop the `dead:` row.
768
- * The new `id:` header makes it visible to `/list` and the live `/ws`
769
- * subscription again. A miss is a no-op (`{ retried: false }`).
770
- */
1110
+ * `POST /dead/retry { id }` — resurrect a dead-letter record: reset its
1111
+ * exhausted attempt count to 0 (a fresh retry budget), re-arm it for
1112
+ * immediate dispatch via the standard time index, and drop the `dead:` row.
1113
+ * The new `id:` header makes it visible to `/list` and the live `/ws`
1114
+ * subscription again. A miss is a no-op (`{ retried: false }`).
1115
+ */
771
1116
  private handleDeadRetry;
772
1117
  /**
773
- * `POST /dead/cancel { id }` — permanently drop a dead-letter record the
774
- * operator has decided not to recover. Returns `{ removed }` (false when
775
- * nothing matched). Idempotent: a repeated purge is a harmless no-op.
776
- */
1118
+ * `POST /dead/cancel { id }` — permanently drop a dead-letter record the
1119
+ * operator has decided not to recover. Returns `{ removed }` (false when
1120
+ * nothing matched). Idempotent: a repeated purge is a harmless no-op.
1121
+ */
777
1122
  private handleDeadCancel;
778
1123
  /**
779
- * Resolve a single pending job by id via a direct `id:&lt;id>` storage read —
780
- * O(1), versus scanning the whole `/list` view. Responds `{ record }` on a
781
- * hit and `{}` on a miss (an absent `record` field — JSON has no `undefined`
782
- * — which the client reads back as `null`).
783
- */
1124
+ * Resolve a single pending job by id via a direct `id:<id>` storage read —
1125
+ * O(1), versus scanning the whole `/list` view. Responds `{ record }` on a
1126
+ * hit and `{}` on a miss (an absent `record` field — JSON has no `undefined`
1127
+ * — which the client reads back as `null`).
1128
+ */
784
1129
  private handleGet;
785
1130
  private removeRecord;
786
1131
  /**
787
- * Arm the alarm for `scheduledFor` only if it is sooner than the currently
788
- * set alarm (or none is set). Used on the schedule path: inserting a job
789
- * can only ever pull the earliest-pending time *earlier*, never later, so a
790
- * full `t:` rescan is unnecessary unless the new job is the new earliest.
791
- */
1132
+ * Arm the alarm for `scheduledFor` only if it is sooner than the currently
1133
+ * set alarm (or none is set). Used on the schedule path: inserting a job
1134
+ * can only ever pull the earliest-pending time *earlier*, never later, so a
1135
+ * full `t:` rescan is unnecessary unless the new job is the new earliest.
1136
+ */
792
1137
  private armAlarmIfEarlier;
1138
+ /**
1139
+ * Re-index every pending job whose time-index entry is gone.
1140
+ *
1141
+ * {@link SchedulerDO.drainRecordGuarded} claims a job by DELETING its `t:`
1142
+ * entry, awaited (so durable) BEFORE {@link SchedulerDO.dispatch}'s outbound
1143
+ * fetch. If the Durable Object is evicted or crashes during that fetch, the
1144
+ * `id:` header (and any `retry:` row) survives with no `t:` entry — and
1145
+ * nothing puts one back: {@link SchedulerDO.rescheduleAlarm} derives the
1146
+ * clock from `t:` alone, and `alarm()`'s inline reconciliation only handles
1147
+ * the INVERSE orphan (a `t:` entry whose header is gone). The job then sits
1148
+ * in `/list` and `/status.backlog` forever, never fires, never reaches
1149
+ * `/dead`. The at-least-once contract `drainRecordGuarded` documents covers
1150
+ * a thrown storage op, not a lost instance.
1151
+ *
1152
+ * Re-firing is safe: the dispatch carries the record id, which the receiver
1153
+ * spends as `x-lunora-mutation-id` for a function target and as the workflow
1154
+ * INSTANCE id for a `workflow` target, so a job that DID reach the origin
1155
+ * before the crash is not run twice either way.
1156
+ *
1157
+ * Two bounded walks (all `t:` values, then all `id:` headers) rather than a
1158
+ * per-header `get`, so the cost is one pass over each prefix.
1159
+ */
1160
+ private reindexOrphanedRecords;
793
1161
  private rescheduleAlarm;
794
1162
  }
1163
+ /** What the Cloudflare scheduler host needs from the Worker's environment. */
1164
+ interface SchedulerHostOptions {
1165
+ /**
1166
+ * Named scheduler instance — one `SchedulerDO` per name, useful for tenant
1167
+ * isolation. Defaults to `"default"`.
1168
+ */
1169
+ instanceName?: string;
1170
+ /**
1171
+ * Data-residency jurisdiction for the `SchedulerDO`. Pass the same value as
1172
+ * the worker's own jurisdiction so scheduled state co-resides with app data.
1173
+ */
1174
+ jurisdiction?: "eu" | "fedramp" | "us";
1175
+ /**
1176
+ * The `SchedulerDO` namespace binding.
1177
+ *
1178
+ * The origin the DO dispatches back to is not configured here — it reads
1179
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, so a wrong value
1180
+ * there (or none) is what makes jobs fire into nothing.
1181
+ */
1182
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
1183
+ }
1184
+ /**
1185
+ * Build the Cloudflare {@link SchedulerHost}.
1186
+ *
1187
+ * The returned host has no `cron` member — see the module docstring.
1188
+ */
1189
+ declare const createSchedulerHost: (options: SchedulerHostOptions) => SchedulerHost;
795
1190
  /** Standard cron expression (5- or 6-field) or a supported `@macro`, per `cron-parser`. */
796
1191
  declare const isValidCronExpression: (schedule: string) => boolean;
797
1192
  /**
798
- * Assert a raw cron expression is well-formed, throwing the same shaped error
799
- * both cron surfaces use. The `context` prefix lets callers name the offending
800
- * job (`cron job "send digest"`) vs. the bare trigger.
801
- */
1193
+ * A 6-field (seconds-leading) cron expression is legal generic cron grammar
1194
+ * but not a Cloudflare Cron Trigger — the platform only understands the
1195
+ * 5-field, minute-granularity form and rejects the rest at `wrangler deploy`
1196
+ * with a message naming neither the job nor the file. Warn here instead of
1197
+ * staying silent, without throwing: unlike the ergonomic `.interval()` form,
1198
+ * the raw `.cron()` escape hatch is meant to accept cron grammar this module
1199
+ * doesn't otherwise second-guess.
1200
+ *
1201
+ * Exported (not module-private) because it is the ONE place this advisory is
1202
+ * written: the runtime `cronJobs()` builder reaches it via
1203
+ * {@link assertValidCronExpression}, and `@lunora/codegen`'s static
1204
+ * `discover/crons.ts` calls it directly after its own `isValidCronExpression`
1205
+ * check — so a hand-authored 6-field `.cron()` warns whether it's discovered
1206
+ * from source at build time or registered at runtime, with one shared message.
1207
+ */
1208
+ declare const warnIfSecondsLeading: (schedule: string, context: string) => void;
1209
+ /**
1210
+ * Assert a raw cron expression is well-formed, throwing the same shaped error
1211
+ * both cron surfaces use. The `context` prefix lets callers name the offending
1212
+ * job (`cron job "send digest"`) vs. the bare trigger. Well-formed but
1213
+ * Cloudflare-incompatible (6-field) expressions pass but log a warning — see
1214
+ * {@link warnIfSecondsLeading}.
1215
+ */
802
1216
  declare const assertValidCronExpression: (schedule: string, context?: string) => void;
803
- export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, type DurableObjectJurisdiction, type DurableObjectNamespaceLike, type DurableObjectStubLike, type EnqueueOptions, type FunctionReference, type HttpDispatcherOptions, type IntervalSchedule, type LunoraSchedulerOptions, type MessageBatchLike, type MonthlySchedule, type QueueConsumerOptions, type QueueDispatch, type QueueEnqueueOptions, type QueueJob, type QueueLike, type QueueMessageLike, type QueueSendOptionsLike, type QueueSendRequestLike, type QueueWorkpool, type QueueWorkpoolOptions, type RetryPolicy, type RunOptions, type ScheduleRecord, type Scheduler, SchedulerDO, type SchedulerDOState, type SchedulerEnv, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
1217
+ /**
1218
+ * Reject a `delayMs` a scheduler cannot act on, before it reaches the
1219
+ * SchedulerDO.
1220
+ *
1221
+ * A `NaN`/`Infinity` delay serializes to `null` through JSON and lands as a
1222
+ * malformed `scheduledFor`; a negative one schedules into the past. Both are the
1223
+ * caller's argument, so the answer has to name the argument — which is why the
1224
+ * code is `INVALID_INPUT` (400) and not `INTERNAL`: `toErrorBody` replaces an
1225
+ * internal-coded message with "Internal error", redacting the one sentence that
1226
+ * says what to fix.
1227
+ *
1228
+ * Exported because four surfaces enforce it — `createScheduler().runAfter`,
1229
+ * `createWorkpool().enqueue`, `@lunora/server`'s deferred-schedule facade (which
1230
+ * must reject BEFORE the transaction commits, not at flush time) and
1231
+ * `@lunora/testing`'s fake scheduler. They used to restate it and threw three
1232
+ * different codes between them, so a test written against the harness caught one
1233
+ * code while production threw another.
1234
+ * @param delayMs the delay to validate
1235
+ * @param surface what to name in the message — the call the delay was passed to (e.g. `"ctx.scheduler.runAfter"`)
1236
+ * @param argument the caller's argument to name in the message; `runAt` converts its absolute `date` to a delay and passes that name through so the answer points at what was actually written
1237
+ */
1238
+ declare const assertScheduleDelay: (delayMs: number, surface: string, argument?: string) => void;
1239
+ /**
1240
+ * Reject a `runAt` instant a scheduler cannot act on — {@link assertScheduleDelay}'s
1241
+ * bound, restated for the absolute form by converting the instant to the delay it
1242
+ * implies.
1243
+ *
1244
+ * `runAfter` has refused a `NaN`/`Infinity` argument since the guard was written;
1245
+ * `runAt` took the same value through a different door and let it reach the DO,
1246
+ * where it serializes to `null` through JSON and lands as a `scheduledFor` no
1247
+ * alarm can ever fire. `new Date("2026-13-01")`, `runAt(row.dueAt + delay)` on a
1248
+ * row whose `dueAt` is absent — both arrive here as a number that is not one.
1249
+ *
1250
+ * An instant already in the PAST is not refused. It is an overdue job
1251
+ * (`runAt(row.dueAt)` on a row that came due while the request was in flight),
1252
+ * and `runAfter` itself reaches `runAt` a fraction of a millisecond after
1253
+ * capturing its own clock reading — so a strict sign check would fail the
1254
+ * documented `runAfter(0, …)` call at random. The delay is therefore clamped at
1255
+ * zero while it is still a finite number, which leaves the half that matters:
1256
+ * a value that is not a number at all.
1257
+ * @param timestampMs the absolute instant (epoch ms) the caller passed
1258
+ * @param nowMs the clock to measure it against — the wall clock in production, the harness's virtual clock in a test
1259
+ * @param surface what to name in the message — the call the instant was passed to (e.g. `"ctx.scheduler.runAt"`)
1260
+ */
1261
+ declare const assertScheduleInstant: (timestampMs: number, nowMs: number, surface: string) => void;
1262
+ export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, type DurableObjectJurisdiction, type DurableObjectNamespaceLike, type DurableObjectStubLike, type EnqueueOptions, type FunctionKind, type FunctionReference, type HourlySchedule, type HttpDispatcherOptions, type IntervalSchedule, type LunoraSchedulerOptions, MAX_RETRY_ATTEMPTS, type MessageBatchLike, type MonthlySchedule, type QueueConsumerOptions, type QueueDispatch, type QueueEnqueueOptions, type QueueJob, type QueueLike, type QueueMessageLike, type QueueSendOptionsLike, type QueueSendRequestLike, type QueueWorkpool, type QueueWorkpoolOptions, RETRY_BASE_DELAY_MS, type RetryPolicy, type RunOptions, type ScheduleRecord, type Scheduler, SchedulerDO, type SchedulerDOState, type SchedulerEnv, type SchedulerHostOptions, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertScheduleDelay, assertScheduleInstant, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createSchedulerHost, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference, resolveScheduleId, warnIfSecondsLeading };