@lunora/scheduler 1.0.0-alpha.1 → 1.0.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
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
- */
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
10
  interface FunctionReference {
11
11
  readonly __lunoraRef: string;
12
12
  /** Marker phantom type — discriminates queries / mutations / actions. */
@@ -16,22 +16,22 @@ type ArgsOf<F extends FunctionReference> = F extends {
16
16
  _args?: infer A;
17
17
  } ? A : Record<string, unknown>;
18
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
- */
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
+ */
35
35
  interface WorkflowReference<Params = Record<string, unknown>> {
36
36
  /** Phantom carrier for the workflow's `params` type — drives `cronJobs()` arg inference. Never read at runtime. */
37
37
  readonly __params?: Params;
@@ -45,23 +45,32 @@ interface WorkflowReference<Params = Record<string, unknown>> {
45
45
  type CronTarget = FunctionReference | WorkflowReference;
46
46
  /** The arguments a cron's target accepts: a workflow's inferred `params`, else an open record (function args aren't inferred). */
47
47
  type CronTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : Record<string, unknown>;
48
+ /**
49
+ * The arguments a one-shot schedule target ({@link Scheduler.runAfter} /
50
+ * {@link Scheduler.runAt}) accepts. Unlike {@link CronTargetArgs} it preserves a
51
+ * {@link FunctionReference}'s inferred `args` (via {@link ArgsOf}) as well as a
52
+ * {@link WorkflowReference}'s inferred `params`, so scheduling a plain function
53
+ * keeps its today's arg checking while scheduling a workflow/agent infers its
54
+ * `params`.
55
+ */
56
+ type ScheduleTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : T extends FunctionReference ? ArgsOf<T> : Record<string, unknown>;
48
57
  /** Narrow a {@link CronTarget} to a {@link WorkflowReference} by its runtime brand. */
49
58
  declare const isWorkflowReference: (target: unknown) => target is WorkflowReference;
50
59
  /**
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
- */
60
+ * Per-job retry policy. Wired into the SchedulerDO's existing attempts/backoff
61
+ * machinery. When omitted, the DO falls back to its built-in defaults
62
+ * (`maxAttempts: 5`, `backoff: "exponential"`, `baseMs: 30_000`) so existing
63
+ * `runAfter`/`runAt` callers keep today's behaviour unchanged.
64
+ *
65
+ * On exhaustion (attempts > `maxAttempts`) the record is parked under the
66
+ * `dead:` dead-letter key for inspection — never silently dropped.
67
+ */
59
68
  interface RetryPolicy {
60
69
  /**
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
- */
70
+ * Backoff growth across attempts. `"exponential"` doubles the delay each
71
+ * attempt (`baseMs * 2 ** (attempt - 1)`); `"linear"` grows it linearly
72
+ * (`baseMs * attempt`). Default `"exponential"`.
73
+ */
65
74
  backoff?: "exponential" | "linear";
66
75
  /** Base delay in milliseconds for the first retry. Default `30_000`. */
67
76
  baseMs?: number;
@@ -72,11 +81,11 @@ interface RetryPolicy {
72
81
  }
73
82
  interface RunOptions {
74
83
  /**
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
- */
84
+ * Logical workpool this job belongs to. When set, the SchedulerDO gates the
85
+ * job behind the pool's `maxConcurrency` (see {@link WorkpoolOptions}).
86
+ * Usually populated by {@link Workpool.enqueue}; callers rarely set it on a
87
+ * bare `runAfter`/`runAt`.
88
+ */
80
89
  pool?: string;
81
90
  /** Per-job retry policy. Falls back to the DO's built-in defaults when omitted. */
82
91
  retry?: RetryPolicy;
@@ -86,33 +95,46 @@ interface RunOptions {
86
95
  interface ScheduleRecord {
87
96
  args: Record<string, unknown>;
88
97
  /**
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
- */
98
+ * Number of dispatch attempts already made. Absent (treated as 0) until the
99
+ * first failure, after which `recordRetry()` persists it on both the
100
+ * `retry:` row and the `id:` header. Surfaced here so `/list` consumers and
101
+ * the studio see the field the storage layer actually writes.
102
+ */
94
103
  attempts?: number;
95
104
  enqueuedAt: number;
96
- functionPath: string;
105
+ /**
106
+ * The `ns:fn` path of the function to dispatch on fire. Absent when the job
107
+ * targets a durable workflow/agent instead — see {@link ScheduleRecord.workflow}.
108
+ * Exactly one of `functionPath` / `workflow` is set.
109
+ */
110
+ functionPath?: string;
97
111
  id: string;
98
112
  /**
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
- */
113
+ * Scheduler/workpool instance name the job was enqueued through. Echoed in
114
+ * the dispatch payload so the runtime can call back the SAME DO instance's
115
+ * `/complete` to release a pooled slot. Absent for the default instance.
116
+ */
103
117
  instanceName?: string;
104
118
  /**
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
- */
119
+ * Logical workpool this job belongs to (set by {@link Workpool.enqueue}).
120
+ * When present, the SchedulerDO only dispatches the job while the pool's
121
+ * in-flight count is below its `maxConcurrency`; otherwise it stays queued
122
+ * and drains as slots free. Absent for plain `runAfter`/`runAt` jobs, which
123
+ * are never concurrency-gated.
124
+ */
111
125
  pool?: string;
112
126
  /** Per-job retry policy (see {@link RetryPolicy}); absent means DO defaults. */
113
127
  retry?: RetryPolicy;
114
128
  scheduledFor: number;
115
129
  shardKey?: string;
130
+ /**
131
+ * The `WORKFLOW_*`/`AGENT_*` binding name to start a fresh durable instance
132
+ * of on fire (the {@link ScheduleRecord.args} become its `params`). Set
133
+ * instead of {@link ScheduleRecord.functionPath} when the job targets a
134
+ * workflow/agent {@link WorkflowReference}. The runtime — not the DO — owns
135
+ * the binding, so the dispatch payload carries this through to the Worker.
136
+ */
137
+ workflow?: string;
116
138
  }
117
139
  interface Scheduler {
118
140
  cancel: (id: string) => Promise<{
@@ -122,19 +144,39 @@ interface Scheduler {
122
144
  get: (id: string) => Promise<ScheduleRecord | null>;
123
145
  /** All pending scheduled jobs (the DO's `/list` view). */
124
146
  list: () => Promise<ScheduleRecord[]>;
125
- runAfter: <F extends FunctionReference>(delayMs: number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
147
+ /**
148
+ * Schedule `target` to run once, `delayMs` from now. `target` is a function
149
+ * {@link FunctionReference} (dispatched as a one-shot) or a durable
150
+ * {@link WorkflowReference} — the generated `workflows.&lt;name>` /
151
+ * `agents.&lt;name>` ref — which starts a fresh instance on fire (args become
152
+ * its `params`). {@link ScheduleTargetArgs} infers the accepted args from
153
+ * whichever target was passed.
154
+ */
155
+ runAfter: <T extends CronTarget>(delayMs: number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
126
156
  id: string;
127
157
  scheduledFor: number;
128
158
  }>;
129
- runAt: <F extends FunctionReference>(date: Date | number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
159
+ /** Like {@link Scheduler.runAfter} but fires at an absolute `date`/timestamp. */
160
+ runAt: <T extends CronTarget>(date: Date | number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
130
161
  id: string;
131
162
  scheduledFor: number;
132
163
  }>;
133
164
  }
165
+ /**
166
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
167
+ * Cloudflare adds values over time.
168
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
169
+ */
170
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
134
171
  /** Subset of `DurableObjectNamespace` the package consumes. */
135
172
  interface DurableObjectNamespaceLike {
136
173
  get: (id: DurableObjectIdLike) => DurableObjectStubLike;
137
174
  idFromName: (name: string) => DurableObjectIdLike;
175
+ /**
176
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
177
+ * workers-types releases (and test doubles) may not expose it.
178
+ */
179
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => DurableObjectNamespaceLike;
138
180
  }
139
181
  interface DurableObjectIdLike {
140
182
  toString: () => string;
@@ -145,12 +187,19 @@ interface DurableObjectStubLike {
145
187
  interface LunoraSchedulerOptions {
146
188
  /** Optional named instance — useful for tenant isolation. Default `default`. */
147
189
  instanceName?: string;
190
+ /**
191
+ * Pin the SchedulerDO (durable timers + cron state) to a Cloudflare
192
+ * data-residency jurisdiction. Pass the same value as the worker's
193
+ * `jurisdiction` so scheduled state co-resides with app data. Omit for the
194
+ * un-pinned global namespace.
195
+ */
196
+ jurisdiction?: DurableObjectJurisdiction;
148
197
  /** Binding to the `SchedulerDO` durable object namespace. */
149
198
  namespace: DurableObjectNamespaceLike;
150
199
  /**
151
- * Origin where the Worker is mounted. SchedulerDO uses this base URL when
152
- * dispatching scheduled functions back to the Worker on alarm fire.
153
- */
200
+ * Origin where the Worker is mounted. SchedulerDO uses this base URL when
201
+ * dispatching scheduled functions back to the Worker on alarm fire.
202
+ */
154
203
  originUrl: string;
155
204
  }
156
205
  /** Per-enqueue options for a {@link Workpool}. Extends {@link RunOptions} minus the implicit `pool` (the pool sets that). */
@@ -163,41 +212,41 @@ interface EnqueueOptions {
163
212
  shardKey?: string;
164
213
  }
165
214
  /**
166
- * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
167
- * (same `namespace` / `originUrl` / `instanceName`) plus the bounded-concurrency
168
- * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
169
- * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
170
- * the scheduler already uses.
171
- */
215
+ * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
216
+ * (same `namespace` / `originUrl` / `instanceName`) plus the bounded-concurrency
217
+ * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
218
+ * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
219
+ * the scheduler already uses.
220
+ */
172
221
  interface WorkpoolOptions extends LunoraSchedulerOptions {
173
222
  /**
174
- * Maximum number of jobs from this pool that may be in flight at once.
175
- * Excess enqueues are persisted and drain as slots free. Must be a positive
176
- * integer.
177
- */
223
+ * Maximum number of jobs from this pool that may be in flight at once.
224
+ * Excess enqueues are persisted and drain as slots free. Must be a positive
225
+ * integer.
226
+ */
178
227
  maxConcurrency: number;
179
228
  /**
180
- * Pool name — the concurrency counter is keyed by this inside the
181
- * SchedulerDO storage (`pool:&lt;name>`). Default `default`.
182
- */
229
+ * Pool name — the concurrency counter is keyed by this inside the
230
+ * SchedulerDO storage (`pool:&lt;name>`). Default `default`.
231
+ */
183
232
  name?: string;
184
233
  }
185
234
  /**
186
- * Bounded-concurrency action queue (Lunora equivalent of `@convex-dev/workpool`).
187
- * Built on the existing SchedulerDO: `enqueue` schedules a job tagged with this
188
- * pool's name; the DO caps simultaneous dispatch at `maxConcurrency` and queues
189
- * the rest durably.
190
- */
235
+ * Bounded-concurrency action queue (Lunora equivalent of `@convex-dev/workpool`).
236
+ * Built on the existing SchedulerDO: `enqueue` schedules a job tagged with this
237
+ * pool's name; the DO caps simultaneous dispatch at `maxConcurrency` and queues
238
+ * the rest durably.
239
+ */
191
240
  interface Workpool {
192
241
  /** Cancel a queued/in-flight pool job by id. */
193
242
  cancel: (id: string) => Promise<{
194
243
  cancelled: boolean;
195
244
  }>;
196
245
  /**
197
- * Enqueue `function_(args)` into the pool. Resolves with the durable job id
198
- * and the time it was scheduled for (it may not run immediately if the pool
199
- * is at capacity).
200
- */
246
+ * Enqueue `function_(args)` into the pool. Resolves with the durable job id
247
+ * and the time it was scheduled for (it may not run immediately if the pool
248
+ * is at capacity).
249
+ */
201
250
  enqueue: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: EnqueueOptions) => Promise<{
202
251
  id: string;
203
252
  scheduledFor: number;
@@ -271,11 +320,11 @@ interface QueueWorkpoolOptions {
271
320
  queue: QueueLike<QueueJob>;
272
321
  }
273
322
  /**
274
- * Queues-backed producer: enqueue function dispatches onto a Cloudflare Queue.
275
- * Concurrency, retries, and dead-lettering are configured on the queue consumer
276
- * in `wrangler.jsonc` (`max_concurrency` / `max_retries` / `dead_letter_queue`),
277
- * not here — that's the whole point of using Queues over the DO workpool.
278
- */
323
+ * Queues-backed producer: enqueue function dispatches onto a Cloudflare Queue.
324
+ * Concurrency, retries, and dead-lettering are configured on the queue consumer
325
+ * in `wrangler.jsonc` (`max_concurrency` / `max_retries` / `dead_letter_queue`),
326
+ * not here — that's the whole point of using Queues over the DO workpool.
327
+ */
279
328
  interface QueueWorkpool {
280
329
  /** Enqueue a single `fn(args)` dispatch. */
281
330
  enqueue: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: QueueEnqueueOptions) => Promise<void>;
@@ -303,41 +352,41 @@ interface HttpDispatcherOptions {
303
352
  originUrl: string;
304
353
  }
305
354
  /**
306
- * Client-side scheduler — forwards `runAfter` / `runAt` / `cancel` calls to a
307
- * `SchedulerDO` over HTTP. The DO owns the alarm and the storage; this is a
308
- * thin RPC wrapper.
309
- */
355
+ * Client-side scheduler — forwards `runAfter` / `runAt` / `cancel` calls to a
356
+ * `SchedulerDO` over HTTP. The DO owns the alarm and the storage; this is a
357
+ * thin RPC wrapper.
358
+ */
310
359
  declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
311
360
  /**
312
- * Bounded-concurrency action queue — the Lunora equivalent of
313
- * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
314
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
315
- * a workpool is just a NAMED logical pool inside that DO (concurrency counter
316
- * keyed by {@link WorkpoolOptions.name} under the `pool:&lt;name>` storage key).
317
- * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
318
- * the scheduler already uses.
319
- *
320
- * `enqueue` schedules a job tagged with this pool; the DO dispatches at most
321
- * `maxConcurrency` of the pool's jobs at once and queues the rest durably,
322
- * draining them as the runtime reports completions (`POST /complete`).
323
- *
324
- * ```ts
325
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
326
- * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
327
- * ```
328
- *
329
- * Why not Cloudflare Queues? Queues natively cover concurrency-capped, retried,
330
- * dead-lettered, delayed dispatch (`max_concurrency`, `max_retries`,
331
- * `retry({ delaySeconds })`, `dead_letter_queue`), and are the right tool when
332
- * you just want to rate-limit fire-and-forget background work. This workpool
333
- * deliberately stays on `SchedulerDO` because it offers what a queue can't: a
334
- * hard concurrency cap (the DO is the single serialization point — no
335
- * cross-consumer overshoot), per-job cancellation, and per-job status
336
- * introspection, all keyed by a stable job id. Reach for Queues when you don't
337
- * need those; reach for this when you do. Either way, do NOT grow multi-step
338
- * orchestration on top of this — that's Cloudflare **Workflows** (`step.do` /
339
- * `step.sleep` / `step.waitForEvent`).
340
- */
361
+ * Bounded-concurrency action queue — the Lunora equivalent of
362
+ * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
363
+ * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
364
+ * a workpool is just a NAMED logical pool inside that DO (concurrency counter
365
+ * keyed by {@link WorkpoolOptions.name} under the `pool:&lt;name>` storage key).
366
+ * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
367
+ * the scheduler already uses.
368
+ *
369
+ * `enqueue` schedules a job tagged with this pool; the DO dispatches at most
370
+ * `maxConcurrency` of the pool's jobs at once and queues the rest durably,
371
+ * draining them as the runtime reports completions (`POST /complete`).
372
+ *
373
+ * ```ts
374
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
375
+ * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
376
+ * ```
377
+ *
378
+ * Why not Cloudflare Queues? Queues natively cover concurrency-capped, retried,
379
+ * dead-lettered, delayed dispatch (`max_concurrency`, `max_retries`,
380
+ * `retry({ delaySeconds })`, `dead_letter_queue`), and are the right tool when
381
+ * you just want to rate-limit fire-and-forget background work. This workpool
382
+ * deliberately stays on `SchedulerDO` because it offers what a queue can't: a
383
+ * hard concurrency cap (the DO is the single serialization point — no
384
+ * cross-consumer overshoot), per-job cancellation, and per-job status
385
+ * introspection, all keyed by a stable job id. Reach for Queues when you don't
386
+ * need those; reach for this when you do. Either way, do NOT grow multi-step
387
+ * orchestration on top of this — that's Cloudflare **Workflows** (`step.do` /
388
+ * `step.sleep` / `step.waitForEvent`).
389
+ */
341
390
  declare const createWorkpool: (options: WorkpoolOptions) => Workpool;
342
391
  interface CronTriggerOptions {
343
392
  /** Args passed to the function. */
@@ -359,10 +408,10 @@ interface CronTriggerSnippet {
359
408
  wranglerJsonc: string;
360
409
  }
361
410
  /**
362
- * Produces the wrangler.jsonc fragment + dispatcher metadata for a recurring
363
- * function. The actual cron handler is mounted by `@lunora/runtime` — we only
364
- * emit the configuration here.
365
- */
411
+ * Produces the wrangler.jsonc fragment + dispatcher metadata for a recurring
412
+ * function. The actual cron handler is mounted by `@lunora/runtime` — we only
413
+ * emit the configuration here.
414
+ */
366
415
  declare const createCronTrigger: (options: CronTriggerOptions) => CronTriggerSnippet;
367
416
  /** Sub-day recurrence. Exactly one unit must be provided. */
368
417
  interface IntervalSchedule {
@@ -388,29 +437,29 @@ interface MonthlySchedule extends DailySchedule {
388
437
  day: number;
389
438
  }
390
439
  /**
391
- * One registered cron job, normalized to a compiled cron expression. Shared
392
- * verbatim with `@lunora/codegen` (which lifts the same fields out of the AST)
393
- * and the runtime dispatcher — keep the shape stable across all three.
394
- */
440
+ * One registered cron job, normalized to a compiled cron expression. Shared
441
+ * verbatim with `@lunora/codegen` (which lifts the same fields out of the AST)
442
+ * and the runtime dispatcher — keep the shape stable across all three.
443
+ */
395
444
  interface CronJob {
396
445
  /** Args forwarded to the function (or, for a workflow target, used as its `params`) on each fire. */
397
446
  args: Record<string, unknown>;
398
447
  /** Compiled standard cron expression, e.g. `"0 9 * * *"`. */
399
448
  cron: string;
400
449
  /**
401
- * `__lunoraRef` of the target function. Present for a function target;
402
- * absent when the job targets a workflow ({@link CronJob.workflow} instead).
403
- */
450
+ * `__lunoraRef` of the target function. Present for a function target;
451
+ * absent when the job targets a workflow ({@link CronJob.workflow} instead).
452
+ */
404
453
  functionPath?: string;
405
454
  /** Human-readable identifier — must be unique within one `cronJobs()`. */
406
455
  name: string;
407
456
  /**
408
- * Set when the job targets a durable workflow rather than a function: the
409
- * workflow's stable name (`defineWorkflow({ name })`) when one was declared,
410
- * otherwise `""`. `@lunora/codegen` statically resolves the concrete
411
- * `lunora/workflows.ts` export + its `WORKFLOW_*` binding for the emitted
412
- * dispatch map, so this authoring-time value is informational only.
413
- */
457
+ * Set when the job targets a durable workflow rather than a function: the
458
+ * workflow's stable name (`defineWorkflow({ name })`) when one was declared,
459
+ * otherwise `""`. `@lunora/codegen` statically resolves the concrete
460
+ * `lunora/workflows.ts` export + its `WORKFLOW_*` binding for the emitted
461
+ * dispatch map, so this authoring-time value is informational only.
462
+ */
414
463
  workflow?: string;
415
464
  }
416
465
  /** The ergonomic builder methods, excluding the raw `.cron` escape hatch. */
@@ -418,23 +467,23 @@ type CronScheduleKind = "daily" | "interval" | "monthly" | "weekly";
418
467
  /** The ergonomic schedule kinds as a runtime set (codegen reads this to detect cron builder methods). */
419
468
  declare const CRON_SCHEDULE_KINDS: ReadonlySet<CronScheduleKind>;
420
469
  /**
421
- * Compile one of the ergonomic schedule forms into a standard cron expression.
422
- * Exposed as a pure function so `@lunora/codegen` can reuse the exact same
423
- * compilation when it statically lifts a `crons.{kind}(...)` call out of the
424
- * AST — codegen imports this directly (no duplicated mirror).
425
- */
470
+ * Compile one of the ergonomic schedule forms into a standard cron expression.
471
+ * Exposed as a pure function so `@lunora/codegen` can reuse the exact same
472
+ * compilation when it statically lifts a `crons.{kind}(...)` call out of the
473
+ * AST — codegen imports this directly (no duplicated mirror).
474
+ */
426
475
  declare const compileCronSchedule: (kind: CronScheduleKind, schedule: DailySchedule | IntervalSchedule | MonthlySchedule | WeeklySchedule) => string;
427
476
  /**
428
- * Builder returned by {@link cronJobs}. Each method registers one recurring
429
- * job; the compiled expression is validated immediately so authoring mistakes
430
- * surface at definition time rather than at codegen.
431
- */
477
+ * Builder returned by {@link cronJobs}. Each method registers one recurring
478
+ * job; the compiled expression is validated immediately so authoring mistakes
479
+ * surface at definition time rather than at codegen.
480
+ */
432
481
  interface CronJobsBuilder {
433
482
  /**
434
- * Raw cron expression escape hatch (5- or 6-field, full cron-parser grammar).
435
- * The target may be a function (`internal.file.fn`) or a durable workflow
436
- * (`workflows.&lt;name>`); a workflow's `args` are inferred from its `params`.
437
- */
483
+ * Raw cron expression escape hatch (5- or 6-field, full cron-parser grammar).
484
+ * The target may be a function (`internal.file.fn`) or a durable workflow
485
+ * (`workflows.&lt;name>`); a workflow's `args` are inferred from its `params`.
486
+ */
438
487
  cron: <T extends CronTarget>(name: string, cronExpr: string, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
439
488
  /** Daily at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
440
489
  daily: <T extends CronTarget>(name: string, schedule: DailySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
@@ -448,41 +497,41 @@ interface CronJobsBuilder {
448
497
  weekly: <T extends CronTarget>(name: string, schedule: WeeklySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
449
498
  }
450
499
  /**
451
- * Create a code-first cron registry. The returned builder is chainable;
452
- * codegen discovers a `lunora/crons.ts` default export by AST, not a runtime
453
- * brand.
454
- */
500
+ * Create a code-first cron registry. The returned builder is chainable;
501
+ * codegen discovers a `lunora/crons.ts` default export by AST, not a runtime
502
+ * brand.
503
+ */
455
504
  declare const cronJobs: () => CronJobsBuilder;
456
505
  /**
457
- * Build a Queues producer that enqueues Lunora function dispatches. Concurrency
458
- * and retry policy live on the consumer's `wrangler.jsonc` config, not here.
459
- */
506
+ * Build a Queues producer that enqueues Lunora function dispatches. Concurrency
507
+ * and retry policy live on the consumer's `wrangler.jsonc` config, not here.
508
+ */
460
509
  declare const createQueueWorkpool: (options: QueueWorkpoolOptions) => QueueWorkpool;
461
510
  /**
462
- * Wrap a {@link QueueDispatch} into a Cloudflare `queue()` consumer handler.
463
- *
464
- * Each message is dispatched independently (concurrently across the batch). On
465
- * success the message is `ack()`-ed; on any failure — a thrown dispatcher or a
466
- * structurally-invalid body — it is `retry()`-ed, so Queues' own `max_retries`
467
- * + `dead_letter_queue` settings decide when to give up. Nothing is silently
468
- * dropped: a permanently-bad message rides retries into the dead-letter queue
469
- * where you can inspect it.
470
- */
511
+ * Wrap a {@link QueueDispatch} into a Cloudflare `queue()` consumer handler.
512
+ *
513
+ * Each message is dispatched independently (concurrently across the batch). On
514
+ * success the message is `ack()`-ed; on any failure — a thrown dispatcher or a
515
+ * structurally-invalid body — it is `retry()`-ed, so Queues' own `max_retries`
516
+ * + `dead_letter_queue` settings decide when to give up. Nothing is silently
517
+ * dropped: a permanently-bad message rides retries into the dead-letter queue
518
+ * where you can inspect it.
519
+ */
471
520
  declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: MessageBatchLike) => Promise<void>);
472
521
  /**
473
- * Default {@link QueueDispatch}: POST each job to the Worker's
474
- * `/_lunora/scheduler/dispatch` endpoint (the same path SchedulerDO dispatches
475
- * through), authenticated with the admin bearer. A non-2xx response throws so
476
- * the consumer retries the message.
477
- */
522
+ * Default {@link QueueDispatch}: POST each job to the Worker's
523
+ * `/_lunora/scheduler/dispatch` endpoint (the same path SchedulerDO dispatches
524
+ * through), authenticated with the admin bearer. A non-2xx response throws so
525
+ * the consumer retries the message.
526
+ */
478
527
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
479
528
  /**
480
- * Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
481
- * structurally so unit tests can pass a fake state without booting the
482
- * workers runtime. The WebSocket methods are optional: they back the live
483
- * `/ws` subscription (push the job list on every change) and are absent in the
484
- * storage-only fakes, in which case the DO simply serves no live sockets.
485
- */
529
+ * Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
530
+ * structurally so unit tests can pass a fake state without booting the
531
+ * workers runtime. The WebSocket methods are optional: they back the live
532
+ * `/ws` subscription (push the job list on every change) and are absent in the
533
+ * storage-only fakes, in which case the DO simply serves no live sockets.
534
+ */
486
535
  interface SchedulerDOState {
487
536
  /** Accept a hibernatable server WebSocket (workers `state.acceptWebSocket`). */
488
537
  acceptWebSocket?: (ws: WebSocket) => void;
@@ -505,30 +554,30 @@ interface SchedulerDOState {
505
554
  interface SchedulerEnv {
506
555
  [key: string]: unknown;
507
556
  /**
508
- * Fallback bearer token attached to the dispatch when
509
- * {@link SchedulerEnv.LUNORA_SCHEDULER_SECRET} is not configured. Sent as
510
- * `authorization: Bearer &lt;token>`.
511
- */
557
+ * Fallback bearer token attached to the dispatch when
558
+ * {@link SchedulerEnv.LUNORA_SCHEDULER_SECRET} is not configured. Sent as
559
+ * `authorization: Bearer &lt;token>`.
560
+ */
512
561
  LUNORA_ADMIN_TOKEN?: string;
513
562
  /**
514
- * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
515
- * time to call back into the Worker. Read at fire time (NOT taken from the
516
- * request body) to prevent SSRF via a forged `originUrl` field.
517
- */
563
+ * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
564
+ * time to call back into the Worker. Read at fire time (NOT taken from the
565
+ * request body) to prevent SSRF via a forged `originUrl` field.
566
+ */
518
567
  LUNORA_ORIGIN_URL?: string;
519
568
  /**
520
- * Shared secret used to HMAC-sign the dispatch body so the runtime receiver
521
- * can authenticate the call (header `x-lunora-scheduler-signature`). Without
522
- * it the dispatch is sent unsigned (optionally bearer-authenticated via
523
- * {@link SchedulerEnv.LUNORA_ADMIN_TOKEN}).
524
- */
569
+ * Shared secret used to HMAC-sign the dispatch body so the runtime receiver
570
+ * can authenticate the call (header `x-lunora-scheduler-signature`). Without
571
+ * it the dispatch is sent unsigned (optionally bearer-authenticated via
572
+ * {@link SchedulerEnv.LUNORA_ADMIN_TOKEN}).
573
+ */
525
574
  LUNORA_SCHEDULER_SECRET?: string;
526
575
  }
527
576
  /**
528
- * One pool's live backlog, as surfaced by `GET /status`. `inFlight`/
529
- * `maxConcurrency` mirror the durable {@link PoolState} semaphore; `queued`
530
- * is the number of pending (not-yet-dispatched) jobs routed to this pool.
531
- */
577
+ * One pool's live backlog, as surfaced by `GET /status`. `inFlight`/
578
+ * `maxConcurrency` mirror the durable {@link PoolState} semaphore; `queued`
579
+ * is the number of pending (not-yet-dispatched) jobs routed to this pool.
580
+ */
532
581
  interface SchedulerPoolStatus {
533
582
  /** Jobs currently dispatched-but-not-yet-completed (the held slots). */
534
583
  inFlight: number;
@@ -540,10 +589,10 @@ interface SchedulerPoolStatus {
540
589
  queued: number;
541
590
  }
542
591
  /**
543
- * App-level scheduler backlog, as returned by `GET /status`. `pools` carries
544
- * the per-pool breakdown; `backlog` and `inFlight` are the app-wide sums of
545
- * `queued` and `inFlight` across every pool — the SLO view's headline numbers.
546
- */
592
+ * App-level scheduler backlog, as returned by `GET /status`. `pools` carries
593
+ * the per-pool breakdown; `backlog` and `inFlight` are the app-wide sums of
594
+ * `queued` and `inFlight` across every pool — the SLO view's headline numbers.
595
+ */
547
596
  interface SchedulerStatus {
548
597
  /** Sum of every pool's `queued` count — the total pending backlog. */
549
598
  backlog: number;
@@ -553,52 +602,59 @@ interface SchedulerStatus {
553
602
  pools: SchedulerPoolStatus[];
554
603
  }
555
604
  /**
556
- * Durable Object that stores pending scheduled invocations sorted by their
557
- * `scheduledFor` time and fires them via HTTP on alarm. Storage layout:
558
- * `id:&lt;id>` maps to {@link ScheduleRecord}; `t:&lt;paddedTime>:&lt;id>` maps to the
559
- * id (used as a sorted index).
560
- *
561
- * On every mutation the DO recomputes the earliest pending task and updates
562
- * the alarm via `state.storage.setAlarm(time)`.
563
- */
605
+ * Durable Object that stores pending scheduled invocations sorted by their
606
+ * `scheduledFor` time and fires them via HTTP on alarm. Storage layout:
607
+ * `id:&lt;id>` maps to {@link ScheduleRecord}; `t:&lt;paddedTime>:&lt;id>` maps to the
608
+ * id (used as a sorted index).
609
+ *
610
+ * On every mutation the DO recomputes the earliest pending task and updates
611
+ * the alarm via `state.storage.setAlarm(time)`.
612
+ */
564
613
  declare class SchedulerDO {
565
614
  private static indexKey;
566
615
  private static json;
567
616
  private static error;
568
617
  /**
569
- * Resolve the effective retry parameters for a record: its per-job
570
- * {@link RetryPolicy} merged over the DO's built-in defaults. Callers that
571
- * never set `record.retry` get today's behaviour verbatim
572
- * (`maxAttempts: 5`, exponential, `baseMs: 30_000`, no ceiling).
573
- */
618
+ * Resolve the effective retry parameters for a record: its per-job
619
+ * {@link RetryPolicy} merged over the DO's built-in defaults. Callers that
620
+ * never set `record.retry` get today's behaviour verbatim
621
+ * (`maxAttempts: 5`, exponential, `baseMs: 30_000`, no ceiling).
622
+ */
574
623
  private static resolveRetry;
575
624
  /** Clamp an untrusted `maxConcurrency` to a positive integer, else fall back. */
576
625
  private static normalizeConcurrency;
577
626
  /**
578
- * Sanitize an untrusted retry policy from the wire into a `RetryPolicy` (or
579
- * `undefined` when nothing valid was provided). Keeps obviously-bad values
580
- * out of storage so {@link SchedulerDO.resolveRetry} never has to re-guard.
581
- * @returns The normalized policy, or `undefined` if no valid policy was found.
582
- */
627
+ * Sanitize an untrusted retry policy from the wire into a `RetryPolicy` (or
628
+ * `undefined` when nothing valid was provided). Keeps obviously-bad values
629
+ * out of storage so {@link SchedulerDO.resolveRetry} never has to re-guard.
630
+ * @returns The normalized policy, or `undefined` if no valid policy was found.
631
+ */
583
632
  private static normalizeRetry;
584
633
  /**
585
- * Idempotently release the slot held by `jobId`, returning the updated
586
- * {@link PoolState} (pure — the caller persists it). A duplicate release for
587
- * an id that no longer holds a slot is a no-op, so an at-least-once
588
- * `/complete` (or a complete racing a failed-kick release) can never push
589
- * `inFlight` below the true number of running jobs and oversubscribe the
590
- * pool. Pools persisted before `inFlightIds` existed fall back to a clamped
591
- * counter decrement.
592
- */
634
+ * Idempotently release the slot held by `jobId`, returning the updated
635
+ * {@link PoolState} (pure — the caller persists it). A duplicate release for
636
+ * an id that no longer holds a slot is a no-op, so an at-least-once
637
+ * `/complete` (or a complete racing a failed-kick release) can never push
638
+ * `inFlight` below the true number of running jobs and oversubscribe the
639
+ * pool. Pools persisted before `inFlightIds` existed fall back to a clamped
640
+ * counter decrement.
641
+ */
593
642
  private static releaseSlot;
594
643
  /**
595
- * Best-effort release with no job id (legacy `/complete` payloads). Drops one
596
- * tracked id if the set exists, else clamps the counter. Less precise than
597
- * {@link SchedulerDO.releaseSlot} — a duplicate id-less complete CAN
598
- * over-release — but every current client sends the id, so this is the
599
- * compatibility shim, not the hot path.
600
- */
644
+ * Best-effort release with no job id (legacy `/complete` payloads). Drops one
645
+ * tracked id if the set exists, else clamps the counter. Less precise than
646
+ * {@link SchedulerDO.releaseSlot} — a duplicate id-less complete CAN
647
+ * over-release — but every current client sends the id, so this is the
648
+ * compatibility shim, not the hot path.
649
+ */
601
650
  private static releaseFirstSlot;
651
+ /**
652
+ * Normalize the mutually-exclusive dispatch target off an untrusted body: a
653
+ * one-shot function path (`functionPath`) or a durable workflow/agent
654
+ * instance (`workflow`, a `WORKFLOW_*`/`AGENT_*` binding). Returns `undefined`
655
+ * when neither is present so the caller can reject the schedule.
656
+ */
657
+ private static resolveScheduleTarget;
602
658
  protected readonly state: SchedulerDOState;
603
659
  protected readonly env: SchedulerEnv;
604
660
  constructor(state: SchedulerDOState, env: SchedulerEnv);
@@ -606,180 +662,192 @@ declare class SchedulerDO {
606
662
  /** Called by the Workers runtime when the alarm previously set by `_rescheduleAlarm()` fires. */
607
663
  alarm(): Promise<void>;
608
664
  /**
609
- * Internal dispatch hook; overridden in unit tests to capture the outgoing
610
- * request. Returns `true` ONLY on an explicit 2xx response (`response.ok`).
611
- * Anything else — a network failure, a 5xx, OR a non-2xx such as 404
612
- * (receiver route not mounted) / 401 / 403 / 4xx — returns `false` and
613
- * enters the retry pipeline via {@link recordRetry}. Treating 4xx as
614
- * success used to permanently delete the job; since the receiver may simply
615
- * be missing (404) or transiently failing, we retry rather than silently
616
- * drop. After {@link MAX_RETRY_ATTEMPTS} the record is parked under a
617
- * `dead:` key for inspection — never silently deleted.
618
- *
619
- * The dispatch target is taken from `env.LUNORA_ORIGIN_URL` (NOT from the
620
- * stored record) to prevent SSRF via a forged `originUrl` on the schedule
621
- * request. If that env var is missing at fire time (a deploy/binding
622
- * regression — schedule time already enforced its presence) we return
623
- * `false` so the record is retried rather than silently dropped.
624
- */
665
+ * Internal dispatch hook; overridden in unit tests to capture the outgoing
666
+ * request. Returns `true` ONLY on an explicit 2xx response (`response.ok`).
667
+ * Anything else — a network failure, a 5xx, OR a non-2xx such as 404
668
+ * (receiver route not mounted) / 401 / 403 / 4xx — returns `false` and
669
+ * enters the retry pipeline via {@link recordRetry}. Treating 4xx as
670
+ * success used to permanently delete the job; since the receiver may simply
671
+ * be missing (404) or transiently failing, we retry rather than silently
672
+ * drop. After {@link MAX_RETRY_ATTEMPTS} the record is parked under a
673
+ * `dead:` key for inspection — never silently deleted.
674
+ *
675
+ * The dispatch target is taken from `env.LUNORA_ORIGIN_URL` (NOT from the
676
+ * stored record) to prevent SSRF via a forged `originUrl` on the schedule
677
+ * request. If that env var is missing at fire time (a deploy/binding
678
+ * regression — schedule time already enforced its presence) we return
679
+ * `false` so the record is retried rather than silently dropped.
680
+ */
625
681
  protected dispatch(record: ScheduleRecord): Promise<boolean>;
626
682
  /**
627
- * Claim + drain one due record with per-record fault isolation, so a storage
628
- * throw can never abort the whole alarm pass (which would skip the remaining
629
- * due records and the `rescheduleAlarm()` that re-arms the clock).
630
- *
631
- * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
632
- * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
633
- * thrown storage op decides whether the job stays re-fireable.
634
- *
635
- * When the record was NOT successfully dispatched, re-assert the time-index
636
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
637
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
638
- * before throwing, and re-inserting the same key is idempotent, so a
639
- * surviving claim is simply rewritten to its prior value. When the record WAS
640
- * dispatched (the throw came from post-dispatch cleanup), leave the index
641
- * deleted so the already-kicked, idempotent job is not re-fired.
642
- */
683
+ * Claim + drain one due record with per-record fault isolation, so a storage
684
+ * throw can never abort the whole alarm pass (which would skip the remaining
685
+ * due records and the `rescheduleAlarm()` that re-arms the clock).
686
+ *
687
+ * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
688
+ * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
689
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
690
+ *
691
+ * A throw reaching here always means the job was NOT dispatched:
692
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
693
+ * returns instead of throwing once a kick succeeds, so every escaping throw
694
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore always
695
+ * re-assert the time-index claim so a later alarm re-attempts it
696
+ * (at-least-once): the claim delete may have removed it and
697
+ * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
698
+ * re-inserting the same key is idempotent, so a surviving claim is simply
699
+ * rewritten to its prior value.
700
+ */
643
701
  private drainRecordGuarded;
644
702
  /**
645
- * Process one due (already index-claimed) record within an alarm drain:
646
- * apply the workpool concurrency gate, dispatch, and settle the result.
647
- * A saturated pool re-arms the job (backpressure, no attempt charged); a
648
- * free slot is reserved durably before dispatch and released immediately if
649
- * the kick fails (success holds it until the runtime reports completion).
650
- * Success clears the `id:`/`retry:` rows; failure routes to
651
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
652
- * lifetime of the drain so the budget decrements without re-reading storage.
653
- * @returns `true` only when the record was successfully dispatched (a 2xx
654
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
655
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
656
- * later throws) must NOT have its time-index claim restored, since re-firing
657
- * an already-kicked job would break idempotency. A `false` return (pool
658
- * backpressure or a failed dispatch) means the job is still re-fireable —
659
- * either already re-armed here, or, if a throw escapes, re-claimed by the
660
- * guard's catch.
661
- */
703
+ * Process one due (already index-claimed) record within an alarm drain:
704
+ * apply the workpool concurrency gate, dispatch, and settle the result.
705
+ * A saturated pool re-arms the job (backpressure, no attempt charged); a
706
+ * free slot is reserved durably before dispatch and released immediately if
707
+ * the kick fails (success holds it until the runtime reports completion).
708
+ * Success clears the `id:`/`retry:` rows; failure routes to
709
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
710
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
711
+ * concurrent /complete landing mid-dispatch can't be clobbered.
712
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
713
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
714
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
715
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
716
+ * is still re-fireable and the guard safely re-claims the time index.
717
+ * @returns `true` only when the record was successfully dispatched (a 2xx
718
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
719
+ * re-fireable — already re-armed here). The value is informational (the guard
720
+ * branches on throw/no-throw, not on this boolean).
721
+ */
662
722
  private drainRecord;
663
723
  /**
664
- * Concurrency gate for a pooled record. Returns `false` (and re-arms the
665
- * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
666
- * otherwise reserves a slot durably and returns `true`. Non-pooled records
667
- * always return `true` without touching any pool state.
668
- */
724
+ * Concurrency gate for a pooled record. Returns `false` (and re-arms the
725
+ * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
726
+ * otherwise reserves a slot durably and returns `true`. Non-pooled records
727
+ * always return `true` without touching any pool state.
728
+ *
729
+ * The pool row is read FRESH from storage on every call — never cached
730
+ * across the drain. Each reservation durably `savePool()`s before the next
731
+ * record runs, so a same-pass reservation is still visible to the next
732
+ * record's fresh read (the budget carries forward); and because dispatch()
733
+ * awaits an outbound fetch between records, a concurrent /complete that
734
+ * decrements the row mid-drain IS reflected here instead of being clobbered
735
+ * by a stale in-memory copy (which would leak a slot permanently).
736
+ */
669
737
  private reservePoolSlot;
670
738
  /**
671
- * Accept a hibernatable live subscription to the job list. The scheduler has
672
- * exactly one subscription shape (the whole list), so there's no per-socket
673
- * registry or dependency tracking — every accepted socket gets the full list
674
- * on connect and on every change. The worker is responsible for gating the
675
- * upgrade behind the admin token before it reaches here.
676
- */
739
+ * Accept a hibernatable live subscription to the job list. The scheduler has
740
+ * exactly one subscription shape (the whole list), so there's no per-socket
741
+ * registry or dependency tracking — every accepted socket gets the full list
742
+ * on connect and on every change. The worker is responsible for gating the
743
+ * upgrade behind the admin token before it reaches here.
744
+ */
677
745
  private handleWebSocketUpgrade;
678
746
  /**
679
- * Re-list the jobs and push them to every connected subscriber. Called after
680
- * any change (schedule / cancel / alarm-fire) so live studios reflect it
681
- * immediately. A no-op when the runtime doesn't support hibernated sockets.
682
- */
747
+ * Re-list the jobs and push them to every connected subscriber. Called after
748
+ * any change (schedule / cancel / alarm-fire) so live studios reflect it
749
+ * immediately. A no-op when the runtime doesn't support hibernated sockets.
750
+ */
683
751
  private broadcastChange;
684
752
  /** The current pending job records (shared by `/list` and the live channel). */
685
753
  private listRecords;
686
754
  /**
687
- * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
688
- * returning a base64url signature, or `undefined` when no secret is
689
- * configured. Mirrors `@lunora/storage`'s signed-URL HMAC pattern (WebCrypto
690
- * `crypto.subtle`, available in workerd).
691
- */
755
+ * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
756
+ * returning a base64url signature, or `undefined` when no secret is
757
+ * configured. Mirrors `@lunora/storage`'s signed-URL HMAC pattern (WebCrypto
758
+ * `crypto.subtle`, available in workerd).
759
+ */
692
760
  private signDispatch;
693
761
  /**
694
- * Move a failed record into the retry pipeline with configurable backoff.
695
- * The retry budget/backoff comes from the record's {@link RetryPolicy}
696
- * (falling back to the DO defaults); on exhaustion the record is parked
697
- * under a `dead:` key for manual inspection.
698
- */
762
+ * Move a failed record into the retry pipeline with configurable backoff.
763
+ * The retry budget/backoff comes from the record's {@link RetryPolicy}
764
+ * (falling back to the DO defaults); on exhaustion the record is parked
765
+ * under a `dead:` key for manual inspection.
766
+ */
699
767
  private recordRetry;
700
768
  /** Read the durable `pool:&lt;name>` row, defaulting to a fresh `inFlight: 0` pool. */
701
769
  private loadPool;
702
770
  private savePool;
703
771
  /**
704
- * Re-arm a pooled job that couldn't run because its pool was at capacity.
705
- * No attempt is charged (this is backpressure, not a failure): the job is
706
- * pushed `POOL_BACKPRESSURE_DELAY_MS` into the future so a later alarm
707
- * drains it once a slot frees, keeping its `id:` header and retry policy.
708
- */
772
+ * Re-arm a pooled job that couldn't run because its pool was at capacity.
773
+ * No attempt is charged (this is backpressure, not a failure): the job is
774
+ * pushed `POOL_BACKPRESSURE_DELAY_MS` into the future so a later alarm
775
+ * drains it once a slot frees, keeping its `id:` header and retry policy.
776
+ */
709
777
  private requeuePooled;
710
778
  /**
711
- * Release a pool slot when the runtime reports an action finished. This is
712
- * the durable-semaphore decrement: dispatch() only KICKS the action and
713
- * holds the slot; the runtime calls back here (`POST /complete { id }`) once
714
- * the action settles, freeing the slot for the next queued job. Idempotent
715
- * and safe if the job/pool is already gone.
716
- */
779
+ * Release a pool slot when the runtime reports an action finished. This is
780
+ * the durable-semaphore decrement: dispatch() only KICKS the action and
781
+ * holds the slot; the runtime calls back here (`POST /complete { id }`) once
782
+ * the action settles, freeing the slot for the next queued job. Idempotent
783
+ * and safe if the job/pool is already gone.
784
+ */
717
785
  private handleComplete;
718
786
  /** `GET /pool?name=` — inspect a pool's slot usage + queued count. */
719
787
  private handlePoolStatus;
720
788
  /**
721
- * `GET /status` — the app-level backlog signal that powers the studio's
722
- * SLO view. Enumerates every durable `pool:&lt;name>` row for its `inFlight`/
723
- * `maxConcurrency` semaphore, counts the pending (not-yet-dispatched) jobs
724
- * routed to each pool with the same single-pass scan {@link handlePoolStatus}
725
- * uses, and rolls those up into app-wide `backlog` (sum of `queued`) and
726
- * `inFlight` (sum of held slots) totals.
727
- *
728
- * Pools that have rows but no queued jobs still appear (with `queued: 0`) so
729
- * a saturated-but-idle pool stays visible; a pool that only ever existed as
730
- * queued jobs without a persisted row is unreachable here (the schedule path
731
- * always writes a `pool:&lt;name>` row before the job's header), so a single
732
- * scan over `pool:`/`id:` is sufficient.
733
- */
789
+ * `GET /status` — the app-level backlog signal that powers the studio's
790
+ * SLO view. Enumerates every durable `pool:&lt;name>` row for its `inFlight`/
791
+ * `maxConcurrency` semaphore, counts the pending (not-yet-dispatched) jobs
792
+ * routed to each pool with the same single-pass scan {@link handlePoolStatus}
793
+ * uses, and rolls those up into app-wide `backlog` (sum of `queued`) and
794
+ * `inFlight` (sum of held slots) totals.
795
+ *
796
+ * Pools that have rows but no queued jobs still appear (with `queued: 0`) so
797
+ * a saturated-but-idle pool stays visible; a pool that only ever existed as
798
+ * queued jobs without a persisted row is unreachable here (the schedule path
799
+ * always writes a `pool:&lt;name>` row before the job's header), so a single
800
+ * scan over `pool:`/`id:` is sufficient.
801
+ */
734
802
  private handleStatus;
735
803
  private handleSchedule;
736
804
  private handleCancel;
737
805
  private handleList;
738
806
  /**
739
- * `GET /dead` — list the dead-letter records: jobs that exhausted their
740
- * retry budget ({@link recordRetry}) and were parked under `dead:&lt;id>`
741
- * instead of being silently dropped. These never appear in `/list` (their
742
- * `id:` header is deleted on park), so this is the ONLY way the studio can
743
- * surface — and recover — a permanently-failed job.
744
- */
807
+ * `GET /dead` — list the dead-letter records: jobs that exhausted their
808
+ * retry budget ({@link recordRetry}) and were parked under `dead:&lt;id>`
809
+ * instead of being silently dropped. These never appear in `/list` (their
810
+ * `id:` header is deleted on park), so this is the ONLY way the studio can
811
+ * surface — and recover — a permanently-failed job.
812
+ */
745
813
  private handleDeadList;
746
814
  /**
747
- * `POST /dead/retry { id }` — resurrect a dead-letter record: reset its
748
- * exhausted attempt count to 0 (a fresh retry budget), re-arm it for
749
- * immediate dispatch via the standard time index, and drop the `dead:` row.
750
- * The new `id:` header makes it visible to `/list` and the live `/ws`
751
- * subscription again. A miss is a no-op (`{ retried: false }`).
752
- */
815
+ * `POST /dead/retry { id }` — resurrect a dead-letter record: reset its
816
+ * exhausted attempt count to 0 (a fresh retry budget), re-arm it for
817
+ * immediate dispatch via the standard time index, and drop the `dead:` row.
818
+ * The new `id:` header makes it visible to `/list` and the live `/ws`
819
+ * subscription again. A miss is a no-op (`{ retried: false }`).
820
+ */
753
821
  private handleDeadRetry;
754
822
  /**
755
- * `POST /dead/cancel { id }` — permanently drop a dead-letter record the
756
- * operator has decided not to recover. Returns `{ removed }` (false when
757
- * nothing matched). Idempotent: a repeated purge is a harmless no-op.
758
- */
823
+ * `POST /dead/cancel { id }` — permanently drop a dead-letter record the
824
+ * operator has decided not to recover. Returns `{ removed }` (false when
825
+ * nothing matched). Idempotent: a repeated purge is a harmless no-op.
826
+ */
759
827
  private handleDeadCancel;
760
828
  /**
761
- * Resolve a single pending job by id via a direct `id:&lt;id>` storage read —
762
- * O(1), versus scanning the whole `/list` view. Responds `{ record }` on a
763
- * hit and `{}` on a miss (an absent `record` field — JSON has no `undefined`
764
- * — which the client reads back as `null`).
765
- */
829
+ * Resolve a single pending job by id via a direct `id:&lt;id>` storage read —
830
+ * O(1), versus scanning the whole `/list` view. Responds `{ record }` on a
831
+ * hit and `{}` on a miss (an absent `record` field — JSON has no `undefined`
832
+ * — which the client reads back as `null`).
833
+ */
766
834
  private handleGet;
767
835
  private removeRecord;
768
836
  /**
769
- * Arm the alarm for `scheduledFor` only if it is sooner than the currently
770
- * set alarm (or none is set). Used on the schedule path: inserting a job
771
- * can only ever pull the earliest-pending time *earlier*, never later, so a
772
- * full `t:` rescan is unnecessary unless the new job is the new earliest.
773
- */
837
+ * Arm the alarm for `scheduledFor` only if it is sooner than the currently
838
+ * set alarm (or none is set). Used on the schedule path: inserting a job
839
+ * can only ever pull the earliest-pending time *earlier*, never later, so a
840
+ * full `t:` rescan is unnecessary unless the new job is the new earliest.
841
+ */
774
842
  private armAlarmIfEarlier;
775
843
  private rescheduleAlarm;
776
844
  }
777
845
  /** Standard cron expression (5- or 6-field) or a supported `@macro`, per `cron-parser`. */
778
846
  declare const isValidCronExpression: (schedule: string) => boolean;
779
847
  /**
780
- * Assert a raw cron expression is well-formed, throwing the same shaped error
781
- * both cron surfaces use. The `context` prefix lets callers name the offending
782
- * job (`cron job "send digest"`) vs. the bare trigger.
783
- */
848
+ * Assert a raw cron expression is well-formed, throwing the same shaped error
849
+ * both cron surfaces use. The `context` prefix lets callers name the offending
850
+ * job (`cron job "send digest"`) vs. the bare trigger.
851
+ */
784
852
  declare const assertValidCronExpression: (schedule: string, context?: string) => void;
785
- export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, 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 };
853
+ 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 };