@lunora/scheduler 1.0.0-alpha.13 → 1.0.0-alpha.130

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 (31) hide show
  1. package/README.md +5 -4
  2. package/dist/index.d.mts +684 -108
  3. package/dist/index.d.ts +684 -108
  4. package/dist/index.mjs +1 -1
  5. package/dist/packem_shared/CRON_SCHEDULE_KINDS-v0tEYoxJ.mjs +1 -0
  6. package/dist/packem_shared/MAX_RETRY_ATTEMPTS-C7-WenJ5.mjs +1 -0
  7. package/dist/packem_shared/assertScheduleDelay-CVFZanKn.mjs +1 -0
  8. package/dist/packem_shared/assertScheduleInstant-DcEKtg0c.mjs +1 -0
  9. package/dist/packem_shared/assertValidCronExpression-fiNfNiWf.mjs +1 -0
  10. package/dist/packem_shared/createCronTrigger-cg32QNgj.mjs +1 -0
  11. package/dist/packem_shared/createQueueConsumer-B2s3Xod5.mjs +4 -0
  12. package/dist/packem_shared/createScheduler-3p6oGhSi.mjs +1 -0
  13. package/dist/packem_shared/createSchedulerHost-Cjqd9wDF.mjs +1 -0
  14. package/dist/packem_shared/createWorkpool-Bo9avoNF.mjs +1 -0
  15. package/dist/packem_shared/do-client-XQayNStI.mjs +1 -0
  16. package/dist/packem_shared/resolve-schedule-id-B2ghxgFm.mjs +1 -0
  17. package/dist/packem_shared/resolveScheduleId-BEV7mby4.mjs +1 -0
  18. package/dist/packem_shared/validate-cron-full.d-Bemx45E8.d.mts +28 -0
  19. package/dist/packem_shared/validate-cron-full.d-Bemx45E8.d.ts +28 -0
  20. package/dist/validate-cron-full.d.mts +1 -0
  21. package/dist/validate-cron-full.d.ts +1 -0
  22. package/dist/validate-cron-full.mjs +1 -0
  23. package/package.json +9 -3
  24. package/dist/packem_shared/CRON_SCHEDULE_KINDS-CZFw97XW.mjs +0 -1
  25. package/dist/packem_shared/SchedulerDO-DGgeZ-uI.mjs +0 -1
  26. package/dist/packem_shared/assertValidCronExpression-DHvO7Vm9.mjs +0 -1
  27. package/dist/packem_shared/createCronTrigger-CyPdVKNF.mjs +0 -1
  28. package/dist/packem_shared/createQueueConsumer-B0qNXRUJ.mjs +0 -1
  29. package/dist/packem_shared/createScheduler-B2YOy_ph.mjs +0 -1
  30. package/dist/packem_shared/createWorkpool-DyVdhB6o.mjs +0 -1
  31. package/dist/packem_shared/do-client-BFps7NIj.mjs +0 -1
package/dist/index.d.ts CHANGED
@@ -1,24 +1,51 @@
1
+ import { SchedulerHost } from '@lunora/platform';
2
+ export { a as assertValidCronExpression, i as isValidCronExpression, w as warnIfSecondsLeading } from "./packem_shared/validate-cron-full.d-Bemx45E8.js";
1
3
  /**
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.
4
+ * The generated function-reference type, shared by every package that needs to
5
+ * infer a call's args or return from `api.<file>.<fn>`.
6
6
  *
7
- * The runtime identifier lives in `__lunoraRef` — this MUST stay in lockstep
8
- * with the codegen emit + `@lunora/client`'s `FunctionReference`.
7
+ * This lives in `shared/` rather than in one package because the consumers span
8
+ * a dependency boundary they must not cross: `@lunora/scheduler` and
9
+ * `@lunora/workflow` accept a reference in `runAfter`/`runAt`/`step.run*` but
10
+ * cannot depend on `@lunora/client`, which is a browser package. Each of them
11
+ * previously hand-copied this declaration, and both copies silently rotted when
12
+ * the phantom carrier was renamed — the conditional matched an OPTIONAL property
13
+ * that no longer existed, so `ArgsOf<F>` quietly resolved to `unknown` and every
14
+ * `step.run(ref, args)` in the repo lost its arg checking without a single error.
15
+ *
16
+ * `shared/` is the repo's answer to exactly that shape: bundler-inlined,
17
+ * zero-dependency source imported by relative path, so it creates no runtime
18
+ * edge between the packages that inline it. Being ONE declaration, there is
19
+ * nothing to keep in lockstep and no drift test to write.
9
20
  */
10
- interface FunctionReference {
21
+ /** The registered function kinds a {@link FunctionReference} can describe. `stream` is a query that yields multiple frames over the WS. */
22
+ type FunctionKind = "action" | "mutation" | "query" | "stream";
23
+ /**
24
+ * Opaque reference to a registered function emitted by `@lunora/codegen`.
25
+ *
26
+ * At runtime it carries the `<file>:<function>` identifier in `__lunoraRef`.
27
+ * Generated declarations decorate this with phantom type parameters so callers
28
+ * can infer args / return values per call site.
29
+ */
30
+ interface FunctionReference<Kind extends FunctionKind = FunctionKind, Args = unknown, Return = unknown> {
31
+ /**
32
+ * Phantom marker carrying the `Kind`/`Args`/`Return` type parameters for
33
+ * inference. Never present at runtime; declared as a covariant (output)
34
+ * position so a concrete reference stays assignable to a widened one.
35
+ */
36
+ readonly __lunoraPhantom?: {
37
+ args: Args;
38
+ kind: Kind;
39
+ returns: Return;
40
+ };
11
41
  readonly __lunoraRef: string;
12
- /** Marker phantom type — discriminates queries / mutations / actions. */
13
- readonly _kind?: "query" | "mutation" | "action";
14
42
  }
15
- type ArgsOf<F extends FunctionReference> = F extends {
16
- _args?: infer A;
17
- } ? A : Record<string, unknown>;
43
+ /** Extract the args type from a {@link FunctionReference}. */
44
+ type ArgsOf<F> = F extends FunctionReference<infer _K, infer A, infer _R> ? A : never;
18
45
  /**
19
46
  * 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()`
47
+ * `workflows.<name>` reference object (`_generated/api.ts`, which carries the
48
+ * workflow's class name + export name) or, structurally, a `defineWorkflow()`
22
49
  * result imported directly. Both are matched by the `isLunoraWorkflow` brand and
23
50
  * carry the workflow's `params` in the phantom `__params`, so a `cronJobs()`
24
51
  * registration infers them.
@@ -35,25 +62,36 @@ type ArgsOf<F extends FunctionReference> = F extends {
35
62
  interface WorkflowReference<Params = Record<string, unknown>> {
36
63
  /** Phantom carrier for the workflow's `params` type — drives `cronJobs()` arg inference. Never read at runtime. */
37
64
  readonly __params?: Params;
38
- /** The `WORKFLOW_*` binding name (present on a generated `workflows.&lt;name>` ref). */
39
- readonly binding?: string;
65
+ /** The workflow's export key — its generated class name, reached as `ctx.exports.<className>` (present on a generated ref). */
66
+ readonly className?: string;
40
67
  readonly isLunoraWorkflow: true;
41
68
  /** The workflow's export/stable name (present on a generated ref; a `defineWorkflow({ name })` override otherwise). */
42
69
  readonly name?: string;
43
70
  }
71
+ /**
72
+ * A function reference a scheduler or workpool may target.
73
+ *
74
+ * `stream` is excluded deliberately, and the exclusion is load-bearing rather
75
+ * than tidiness: a scheduled job is dispatched as an ordinary `/rpc` call, and
76
+ * the function runner cannot execute a stream function (see
77
+ * `create-worker.ts`'s registry note). Accepting one compiles a job that is
78
+ * guaranteed to fail when its alarm fires, long after the call site that
79
+ * scheduled it.
80
+ */
81
+ type SchedulableReference<Args = unknown, Return = unknown> = FunctionReference<Exclude<FunctionKind, "stream">, Args, Return>;
44
82
  /** A cron job's target: either a one-shot function dispatch or a durable workflow start. */
45
- type CronTarget = FunctionReference | WorkflowReference;
83
+ type CronTarget = SchedulableReference | WorkflowReference;
46
84
  /** The arguments a cron's target accepts: a workflow's inferred `params`, else an open record (function args aren't inferred). */
47
85
  type CronTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : Record<string, unknown>;
48
86
  /**
49
87
  * 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`.
88
+ * {@link Scheduler.runAt}) accepts. Unlike {@link CronTargetArgs} it resolves a
89
+ * {@link FunctionReference}'s `args` through {@link ArgsOf} as well as a
90
+ * {@link WorkflowReference}'s `params`, so scheduling a generated function
91
+ * reference is arg-checked against that function's validator while scheduling a
92
+ * workflow/agent is checked against its `params`.
55
93
  */
56
- type ScheduleTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : T extends FunctionReference ? ArgsOf<T> : Record<string, unknown>;
94
+ type ScheduleTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : T extends SchedulableReference ? ArgsOf<T> : Record<string, unknown>;
57
95
  /** Narrow a {@link CronTarget} to a {@link WorkflowReference} by its runtime brand. */
58
96
  declare const isWorkflowReference: (target: unknown) => target is WorkflowReference;
59
97
  /**
@@ -74,12 +112,46 @@ interface RetryPolicy {
74
112
  backoff?: "exponential" | "linear";
75
113
  /** Base delay in milliseconds for the first retry. Default `30_000`. */
76
114
  baseMs?: number;
77
- /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
115
+ /**
116
+ * Maximum number of **retries** after the initial dispatch. Default `5`, so
117
+ * a job that keeps failing is dispatched 6 times in total before it is
118
+ * dead-lettered (the park happens once `attempts > maxAttempts`).
119
+ */
78
120
  maxAttempts?: number;
79
121
  /** Optional ceiling clamping the computed backoff delay. */
80
122
  maxMs?: number;
81
123
  }
82
124
  interface RunOptions {
125
+ /**
126
+ * Job id to store the record under, instead of one the SchedulerDO mints.
127
+ *
128
+ * Exists for `@lunora/server`'s deferred-schedule facade: inside a mutation a
129
+ * `runAfter`/`runAt` is buffered until the transaction commits, but the
130
+ * handler is handed the id synchronously, so the id has to be decided before
131
+ * the call is made. Callers that are not deferring should leave it unset and
132
+ * take the minted id from the return value. Anything that is not a plain
133
+ * `[A-Za-z0-9_-]` id of at most 64 characters, or that LEADS with `-`, is
134
+ * REFUSED (`400 INVALID_SCHEDULE_ID`) rather than replaced: the id is handed
135
+ * to `WorkflowBinding.create({ id })` verbatim for a workflow target, and the
136
+ * engine's instance-id grammar (`^[a-zA-Z0-9_][a-zA-Z0-9-_]*$`) refuses that
137
+ * first character. Minting over it would mean two calls naming the same bad
138
+ * id ran the job twice instead of the second answering `409`.
139
+ *
140
+ * **Not an idempotency key.** An id that is already scheduled is REFUSED
141
+ * (`409 DUPLICATE_SCHEDULE_ID`), not replaced or de-duplicated: the time
142
+ * index is keyed by time as well as id, so an overwrite would fire the new
143
+ * job at the old job's instant and drop the slot it was actually scheduled
144
+ * for. Cancel the existing job first if you mean to reschedule it. The id
145
+ * is free again once the job has fired or been cancelled.
146
+ */
147
+ id?: string;
148
+ /**
149
+ * Cap for the {@link RunOptions.pool} this job joins, applied when the pool
150
+ * is first created and refreshed on every enqueue that carries one. Ignored
151
+ * without `pool`. A pool created by a `runAfter`/`runAt` that omits it caps
152
+ * at 1 — {@link Workpool} is the usual way to set it.
153
+ */
154
+ maxConcurrency?: number;
83
155
  /**
84
156
  * Logical workpool this job belongs to. When set, the SchedulerDO gates the
85
157
  * job behind the pool's `maxConcurrency` (see {@link WorkpoolOptions}).
@@ -128,7 +200,7 @@ interface ScheduleRecord {
128
200
  scheduledFor: number;
129
201
  shardKey?: string;
130
202
  /**
131
- * The `WORKFLOW_*`/`AGENT_*` binding name to start a fresh durable instance
203
+ * The workflow/agent export key (its generated class name) to start a fresh durable instance
132
204
  * of on fire (the {@link ScheduleRecord.args} become its `params`). Set
133
205
  * instead of {@link ScheduleRecord.functionPath} when the job targets a
134
206
  * workflow/agent {@link WorkflowReference}. The runtime — not the DO — owns
@@ -140,6 +212,19 @@ interface Scheduler {
140
212
  cancel: (id: string) => Promise<{
141
213
  cancelled: boolean;
142
214
  }>;
215
+ /**
216
+ * Jobs that exhausted their retry budget and were parked under `dead:`
217
+ * (the DO's `/dead` view). Deliberately absent from {@link Scheduler.list} —
218
+ * the park deletes the `id:` header — so this is the only view of a job
219
+ * that failed permanently rather than being silently dropped.
220
+ */
221
+ dead: () => Promise<ScheduleRecord[]>;
222
+ /**
223
+ * Resurrect a parked job with a fresh attempt budget (the DO's
224
+ * `POST /dead/retry`). `false` when the id is not parked; a racing double
225
+ * recover is a no-op rather than an error.
226
+ */
227
+ deadRetry: (id: string) => Promise<boolean>;
143
228
  /** Resolve a single pending job by id, or `null` when absent (derived from {@link Scheduler.list}). */
144
229
  get: (id: string) => Promise<ScheduleRecord | null>;
145
230
  /** All pending scheduled jobs (the DO's `/list` view). */
@@ -147,20 +232,28 @@ interface Scheduler {
147
232
  /**
148
233
  * Schedule `target` to run once, `delayMs` from now. `target` is a function
149
234
  * {@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
235
+ * {@link WorkflowReference} — the generated `workflows.<name>` /
236
+ * `agents.<name>` ref — which starts a fresh instance on fire (args become
152
237
  * its `params`). {@link ScheduleTargetArgs} infers the accepted args from
153
238
  * whichever target was passed.
239
+ *
240
+ * **Resolves the job id, a bare string** — the same value `cancel`/`get`
241
+ * take, and the same value the `ctx.scheduler` surface promises. This object
242
+ * IS `ctx.scheduler` on the shard side (codegen installs it behind
243
+ * `SchedulerLike`, whose `runAfter`/`runAt` are declared `Promise<string>`),
244
+ * so resolving a `{ id, scheduledFor }` record here handed mutations an
245
+ * object where every other gate — `@lunora/server`'s `Scheduler`,
246
+ * `@lunora/shard-engine`'s `SchedulerLike`, `@lunora/runtime`'s httpAction
247
+ * ctx, and the docs — said string. Nothing caught it, because the install is
248
+ * a cast: apps wrote the object into a string column and `cancel(id)`
249
+ * answered `{ cancelled: false }` with no error anywhere.
250
+ *
251
+ * The fire instant is not lost: `runAt` was handed it, and a caller that
252
+ * needs it back reads `scheduledFor` off {@link Scheduler.get}.
154
253
  */
155
- runAfter: <T extends CronTarget>(delayMs: number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
156
- id: string;
157
- scheduledFor: number;
158
- }>;
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<{
161
- id: string;
162
- scheduledFor: number;
163
- }>;
254
+ runAfter: <T extends CronTarget>(delayMs: number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<string>;
255
+ /** Like {@link Scheduler.runAfter} but fires at an absolute `date`/timestamp. Resolves the job id. */
256
+ runAt: <T extends CronTarget>(date: Date | number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<string>;
164
257
  }
165
258
  /**
166
259
  * Cloudflare Durable Object data-residency jurisdiction. Widening union —
@@ -194,13 +287,14 @@ interface LunoraSchedulerOptions {
194
287
  * un-pinned global namespace.
195
288
  */
196
289
  jurisdiction?: DurableObjectJurisdiction;
197
- /** Binding to the `SchedulerDO` durable object namespace. */
198
- namespace: DurableObjectNamespaceLike;
199
290
  /**
200
- * Origin where the Worker is mounted. SchedulerDO uses this base URL when
201
- * dispatching scheduled functions back to the Worker on alarm fire.
291
+ * Binding to the `SchedulerDO` durable object namespace.
292
+ *
293
+ * The origin the DO dispatches back to is NOT passed here: it reads
294
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, because a
295
+ * caller-supplied dispatch target would be an SSRF vector.
202
296
  */
203
- originUrl: string;
297
+ namespace: DurableObjectNamespaceLike;
204
298
  }
205
299
  /** Per-enqueue options for a {@link Workpool}. Extends {@link RunOptions} minus the implicit `pool` (the pool sets that). */
206
300
  interface EnqueueOptions {
@@ -213,7 +307,7 @@ interface EnqueueOptions {
213
307
  }
214
308
  /**
215
309
  * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
216
- * (same `namespace` / `originUrl` / `instanceName`) plus the bounded-concurrency
310
+ * (same `namespace` / `instanceName`) plus the bounded-concurrency
217
311
  * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
218
312
  * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
219
313
  * the scheduler already uses.
@@ -227,7 +321,7 @@ interface WorkpoolOptions extends LunoraSchedulerOptions {
227
321
  maxConcurrency: number;
228
322
  /**
229
323
  * Pool name — the concurrency counter is keyed by this inside the
230
- * SchedulerDO storage (`pool:&lt;name>`). Default `default`.
324
+ * SchedulerDO storage (`pool:<name>`). Default `default`.
231
325
  */
232
326
  name?: string;
233
327
  }
@@ -247,11 +341,11 @@ interface Workpool {
247
341
  * and the time it was scheduled for (it may not run immediately if the pool
248
342
  * is at capacity).
249
343
  */
250
- enqueue: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: EnqueueOptions) => Promise<{
344
+ enqueue: <F extends SchedulableReference>(function_: F, args: ArgsOf<F>, options?: EnqueueOptions) => Promise<{
251
345
  id: string;
252
346
  scheduledFor: number;
253
347
  }>;
254
- /** The pool's name (the `pool:&lt;name>` storage key suffix). */
348
+ /** The pool's name (the `pool:<name>` storage key suffix). */
255
349
  readonly name: string;
256
350
  /** Inspect the pool's current state — `inFlight` slots used and the configured `maxConcurrency`. */
257
351
  status: () => Promise<{
@@ -302,6 +396,13 @@ interface MessageBatchLike<Body = unknown> {
302
396
  }
303
397
  /** The wire payload Lunora puts on the queue: a function dispatch. */
304
398
  interface QueueJob {
399
+ /**
400
+ * The call's arguments in WIRE form (`packages/wire/src/wire-codec`), so a `bigint`,
401
+ * `Date` or bytes survives the queue's own JSON serialisation. The producers
402
+ * encode; the shard's dispatch loop is the single decoder. A custom
403
+ * {@link QueueDispatch} must forward this untouched — decoding it here and
404
+ * letting the shard decode again flattens a `Date` to `{}`.
405
+ */
305
406
  args?: Record<string, unknown>;
306
407
  functionPath: string;
307
408
  /** Routing hint forwarded to the Worker so the call lands on the right shard. */
@@ -327,7 +428,7 @@ interface QueueWorkpoolOptions {
327
428
  */
328
429
  interface QueueWorkpool {
329
430
  /** Enqueue a single `fn(args)` dispatch. */
330
- enqueue: <F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: QueueEnqueueOptions) => Promise<void>;
431
+ enqueue: <F extends SchedulableReference>(function_: F, args: ArgsOf<F>, options?: QueueEnqueueOptions) => Promise<void>;
331
432
  /** Enqueue many dispatches in one `sendBatch`. Each job names its function `ref`. */
332
433
  enqueueBatch: (jobs: ReadonlyArray<{
333
434
  args?: Record<string, unknown>;
@@ -335,12 +436,40 @@ interface QueueWorkpool {
335
436
  shardKey?: string;
336
437
  }>, options?: QueueSendOptionsLike) => Promise<void>;
337
438
  }
338
- /** Dispatches a single {@link QueueJob} — the consumer's per-message worker. */
339
- type QueueDispatch = (job: QueueJob) => Promise<void>;
439
+ /**
440
+ * Dispatches a single {@link QueueJob} — the consumer's per-message worker.
441
+ * `messageId` is the queue message's native id, threaded through so the
442
+ * dispatcher can attribute a failure to the exact message that caused it.
443
+ */
444
+ type QueueDispatch = (job: QueueJob, messageId?: string) => Promise<void>;
340
445
  /** Options for `createQueueConsumer`. */
341
446
  interface QueueConsumerOptions {
342
447
  /** How each job is executed; e.g. the `httpDispatcher`. */
343
448
  dispatch: QueueDispatch;
449
+ /**
450
+ * The consumer's `max_retries` as deployed in `wrangler.jsonc`, which a
451
+ * consumer cannot read at runtime. Used only to recognise a message's last
452
+ * delivery, where a declined dispatch is re-enqueued through `requeue` (or
453
+ * logged as the drop or dead-letter it becomes without one). Defaults to
454
+ * Cloudflare's default of 3.
455
+ */
456
+ maxRetries?: number;
457
+ /**
458
+ * Re-enqueue a job whose dispatch is declined on its last delivery as a
459
+ * copy delayed past the in-flight claim, with a fresh retry budget, instead
460
+ * of letting it be dead-lettered or dropped while its call is still
461
+ * running. Without it that loss is only logged.
462
+ *
463
+ * `queue` is the producer binding of the queue this consumer reads (the
464
+ * one handed to `createQueueWorkpool`). `secret` keys the MAC the copy
465
+ * carries — pass the admin token (`LUNORA_ADMIN_TOKEN`). The copy
466
+ * dispatches under the replaced message's id so the shard serves the
467
+ * declined call's result, and without a valid MAC no body can claim an id.
468
+ */
469
+ requeue?: {
470
+ queue: QueueLike<QueueJob>;
471
+ secret: string;
472
+ };
344
473
  }
345
474
  /** Options for the `httpDispatcher` — the default HTTP dispatcher. */
346
475
  interface HttpDispatcherOptions {
@@ -350,6 +479,13 @@ interface HttpDispatcherOptions {
350
479
  fetchImpl?: typeof fetch;
351
480
  /** Origin where the Worker is mounted (the `/_lunora/scheduler/dispatch` endpoint). */
352
481
  originUrl: string;
482
+ /**
483
+ * Abort a job's dispatch after this many ms; the abort is retryable, so the
484
+ * consumer retries the message. Defaults to 5 minutes — raise it for a
485
+ * workpool running jobs that legitimately run longer, lower it to fail a
486
+ * stuck origin faster.
487
+ */
488
+ timeoutMs?: number;
353
489
  }
354
490
  /**
355
491
  * Client-side scheduler — forwards `runAfter` / `runAt` / `cancel` calls to a
@@ -360,9 +496,9 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
360
496
  /**
361
497
  * Bounded-concurrency action queue — the Lunora equivalent of
362
498
  * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
363
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
499
+ * `instanceName` options and is built on the SAME `SchedulerDO`:
364
500
  * 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).
501
+ * keyed by {@link WorkpoolOptions.name} under the `pool:<name>` storage key).
366
502
  * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
367
503
  * the scheduler already uses.
368
504
  *
@@ -370,8 +506,24 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
370
506
  * `maxConcurrency` of the pool's jobs at once and queues the rest durably,
371
507
  * draining them as the runtime reports completions (`POST /complete`).
372
508
  *
509
+ * The ceiling the platform adds: one alarm drain keeps at most SIX dispatches in
510
+ * flight, because a Durable Object may have only six connections simultaneously
511
+ * waiting for response headers. A `maxConcurrency` above six is therefore not
512
+ * wrong, just not reachable within a single drain — the surplus drains on the
513
+ * following alarms. The same invocation is bounded by the 15-minute alarm wall
514
+ * clock, which covers the WHOLE drain rather than one job; work that does not
515
+ * fit is deferred to the next alarm, never dropped.
516
+ *
517
+ * For jobs that are individually long — an LLM call, an export, a payment
518
+ * round-trip — prefer `createQueueWorkpool`: the dispatch this DO awaits
519
+ * only returns once the function has finished running, so a long job occupies
520
+ * one of those six slots and one slice of that 15-minute budget for its whole
521
+ * duration. A Queues consumer gets a fresh Worker invocation per batch, with its
522
+ * own connection budget and its own clock, and its `max_concurrency` is not
523
+ * capped at six. You give up the hard cap, per-job cancel, and per-job status.
524
+ *
373
525
  * ```ts
374
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
526
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, maxConcurrency: 5 });
375
527
  * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
376
528
  * ```
377
529
  *
@@ -415,8 +567,18 @@ interface CronTriggerSnippet {
415
567
  declare const createCronTrigger: (options: CronTriggerOptions) => CronTriggerSnippet;
416
568
  /** Sub-day recurrence. Exactly one unit must be provided. */
417
569
  interface IntervalSchedule {
570
+ /** 1–23, and must divide 24 (an interval repeats *within* a day). */
418
571
  hours?: number;
572
+ /** 1–59, and must divide 60. */
419
573
  minutes?: number;
574
+ /**
575
+ * Accepted by the type, **rejected at definition time**: Cloudflare Cron
576
+ * Triggers have a one-minute floor, so the 6-field expression this compiles
577
+ * to would survive codegen and land in the committed `wrangler.jsonc`, then
578
+ * fail at `wrangler deploy` naming neither the job nor the file. Declared so
579
+ * the rejection can name the job instead. Use `ctx.scheduler.runAfter`/
580
+ * `runAt` for sub-minute recurrence, or `{ minutes: 1 }`.
581
+ */
420
582
  seconds?: number;
421
583
  }
422
584
  /** Daily recurrence at a fixed UTC wall-clock time. */
@@ -426,6 +588,19 @@ interface DailySchedule {
426
588
  /** 0–59. */
427
589
  minuteUTC: number;
428
590
  }
591
+ /**
592
+ * Hourly recurrence at a fixed minute past the hour.
593
+ *
594
+ * `crons.interval({ hours: 1 })` compiles to the same expression, but the
595
+ * asymmetry of having `daily`/`weekly`/`monthly` and no `hourly` is its own
596
+ * papercut — and unlike the interval form this one lets
597
+ * the caller place the job off the hour boundary, which is how you stop a
598
+ * dozen hourly jobs from stampeding at `:00`.
599
+ */
600
+ interface HourlySchedule {
601
+ /** 0–59. */
602
+ minuteUTC: number;
603
+ }
429
604
  /** Weekly recurrence at a fixed UTC time on a given weekday. */
430
605
  interface WeeklySchedule extends DailySchedule {
431
606
  /** Long weekday name, case-insensitive (e.g. `"monday"`). */
@@ -457,22 +632,24 @@ interface CronJob {
457
632
  * Set when the job targets a durable workflow rather than a function: the
458
633
  * workflow's stable name (`defineWorkflow({ name })`) when one was declared,
459
634
  * otherwise `""`. `@lunora/codegen` statically resolves the concrete
460
- * `lunora/workflows.ts` export + its `WORKFLOW_*` binding for the emitted
635
+ * `lunora/workflows.ts` export + its class name for the emitted
461
636
  * dispatch map, so this authoring-time value is informational only.
462
637
  */
463
638
  workflow?: string;
464
639
  }
465
640
  /** The ergonomic builder methods, excluding the raw `.cron` escape hatch. */
466
- type CronScheduleKind = "daily" | "interval" | "monthly" | "weekly";
641
+ type CronScheduleKind = "daily" | "hourly" | "interval" | "monthly" | "weekly";
467
642
  /** The ergonomic schedule kinds as a runtime set (codegen reads this to detect cron builder methods). */
468
643
  declare const CRON_SCHEDULE_KINDS: ReadonlySet<CronScheduleKind>;
469
644
  /**
470
645
  * Compile one of the ergonomic schedule forms into a standard cron expression.
471
646
  * Exposed as a pure function so `@lunora/codegen` can reuse the exact same
472
647
  * compilation when it statically lifts a `crons.{kind}(...)` call out of the
473
- * AST — codegen imports this directly (no duplicated mirror).
648
+ * AST — codegen imports this directly (no duplicated mirror). `jobName` is
649
+ * optional (see {@link compileInterval}) so codegen's existing 2-arg call site
650
+ * keeps working unchanged.
474
651
  */
475
- declare const compileCronSchedule: (kind: CronScheduleKind, schedule: DailySchedule | IntervalSchedule | MonthlySchedule | WeeklySchedule) => string;
652
+ declare const compileCronSchedule: (kind: CronScheduleKind, schedule: DailySchedule | HourlySchedule | IntervalSchedule | MonthlySchedule | WeeklySchedule, jobName?: string) => string;
476
653
  /**
477
654
  * Builder returned by {@link cronJobs}. Each method registers one recurring
478
655
  * job; the compiled expression is validated immediately so authoring mistakes
@@ -480,20 +657,22 @@ declare const compileCronSchedule: (kind: CronScheduleKind, schedule: DailySched
480
657
  */
481
658
  interface CronJobsBuilder {
482
659
  /**
483
- * Raw cron expression escape hatch (5- or 6-field, full cron-parser grammar).
660
+ * Raw cron expression escape hatch (5- or 6-field, full `@visulima/cron-parser` Cloudflare grammar).
484
661
  * 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`.
662
+ * (`workflows.<name>`); a workflow's `args` are inferred from its `params`.
486
663
  */
487
664
  cron: <T extends CronTarget>(name: string, cronExpr: string, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
488
- /** Daily at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
665
+ /** Daily at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.<name>`). */
489
666
  daily: <T extends CronTarget>(name: string, schedule: DailySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
490
- /** Every `{ seconds | minutes | hours }`. The target may be a function or a durable workflow (`workflows.&lt;name>`). */
667
+ /** Hourly at `minuteUTC` past the hour. The target may be a function or a durable workflow (`workflows.<name>`). */
668
+ hourly: <T extends CronTarget>(name: string, schedule: HourlySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
669
+ /** Every `{ minutes | hours }` — `{ seconds }` throws (Cron Triggers have a one-minute floor). The target may be a function or a durable workflow (`workflows.<name>`). */
491
670
  interval: <T extends CronTarget>(name: string, schedule: IntervalSchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
492
671
  /** Snapshot of the registered jobs, in declaration order. */
493
672
  jobs: () => ReadonlyArray<CronJob>;
494
- /** Monthly on `day` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
673
+ /** Monthly on `day` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.<name>`). */
495
674
  monthly: <T extends CronTarget>(name: string, schedule: MonthlySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
496
- /** Weekly on `dayOfWeek` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.&lt;name>`). */
675
+ /** Weekly on `dayOfWeek` at `hourUTC:minuteUTC` (UTC). The target may be a function or a durable workflow (`workflows.<name>`). */
497
676
  weekly: <T extends CronTarget>(name: string, schedule: WeeklySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
498
677
  }
499
678
  /**
@@ -519,12 +698,49 @@ declare const createQueueWorkpool: (options: QueueWorkpoolOptions) => QueueWorkp
519
698
  */
520
699
  declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: MessageBatchLike) => Promise<void>);
521
700
  /**
522
- * Default {@link QueueDispatch}: POST each job to the Worker's
701
+ * Default {@link QueueDispatch}: dispatch each job to the Worker's
523
702
  * `/_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.
703
+ * through) via `@lunora/dispatch`'s `createDispatchRunner` — which bounds each
704
+ * job with {@link HttpDispatcherOptions.timeoutMs} (default
705
+ * {@link DEFAULT_JOB_TIMEOUT_MS}), so a hung origin no longer holds the whole
706
+ * `queue()` invocation open, and threads the queue message id through for
707
+ * failure attribution. Any dispatch failure throws so the consumer retries the
708
+ * message — including a 2xx carrying a non-empty non-JSON body, which is an
709
+ * intermediary's page rather than a function's return value and therefore no
710
+ * evidence the job ran. An empty 2xx is a normal success (a `void` function).
711
+ *
712
+ * `job.args` is forwarded VERBATIM: {@link encodeJobArgs} already put it in wire
713
+ * form at the producer, and the shard's dispatch loop is the single decoder.
714
+ * Encoding again here would leave the handler a tagged array.
526
715
  */
527
716
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
717
+ /**
718
+ * The id a new record is stored under: the caller's, or a freshly minted one when
719
+ * they did not name it.
720
+ *
721
+ * A caller id that is not a safe key segment is REFUSED, not replaced.
722
+ * `RunOptions.id` is not an idempotency key — an id already scheduled answers
723
+ * `409 DUPLICATE_SCHEDULE_ID` — so quietly swapping an invalid one for a random
724
+ * id made `runAt(ts, ref, args, { id: "-daily-2026-09-06" })` mint a fresh id on
725
+ * every call and run the job once per call, where naming it was the caller's way
726
+ * of saying "at most once".
727
+ *
728
+ * Minting swaps a leading `-` for `_` rather than re-rolling: 1 in 64 minted ids
729
+ * led with one and was refused by the workflow engine on dispatch. `_` is in the
730
+ * engine's leading class, the swap is a single pass, and 96 random bits stay 96
731
+ * random bits everywhere but that first character.
732
+ *
733
+ * Exported because two surfaces have to agree on it. The SchedulerDO applies it
734
+ * when the record is written (turning the refusal into a `400`);
735
+ * `@lunora/server`'s deferred-schedule facade applies it when the call is
736
+ * BUFFERED, because it answers the handler with the id synchronously — long
737
+ * before the DO sees the request. Restating the rule in the facade is how the two
738
+ * drift: an id the facade accepted and the DO replaced leaves the handler holding
739
+ * an id no job was ever stored under, so its later `cancel` silently misses.
740
+ * @param requested the caller's `RunOptions.id`, if any
741
+ * @throws LunoraError `INVALID_SCHEDULE_ID` when `requested` is supplied and is not a safe key segment
742
+ */
743
+ declare const resolveScheduleId: (requested: unknown) => string;
528
744
  /**
529
745
  * Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
530
746
  * structurally so unit tests can pass a fake state without booting the
@@ -537,6 +753,15 @@ interface SchedulerDOState {
537
753
  acceptWebSocket?: (ws: WebSocket) => void;
538
754
  /** Every accepted server WebSocket (workers `state.getWebSockets`). */
539
755
  getWebSockets?: () => WebSocket[];
756
+ /**
757
+ * Register a constant ping/pong auto-response so the runtime answers a
758
+ * known keepalive frame on a hibernated socket WITHOUT waking this DO (no
759
+ * billable request, no dispatch). Optional: absent in the unit harness and
760
+ * older runtimes, present on the real `DurableObjectState`. Mirrors
761
+ * `@lunora/do`'s `ShardDOState.setWebSocketAutoResponse` — see
762
+ * {@link SchedulerDO.armWebSocketKeepalive}.
763
+ */
764
+ setWebSocketAutoResponse?: (pair: WebSocketRequestResponsePair) => void;
540
765
  storage: {
541
766
  delete: (key: string | string[]) => Promise<number | boolean>;
542
767
  deleteAlarm: () => Promise<void> | void;
@@ -546,6 +771,7 @@ interface SchedulerDOState {
546
771
  end?: string;
547
772
  limit?: number;
548
773
  prefix?: string;
774
+ startAfter?: string;
549
775
  }) => Promise<Map<string, T>>;
550
776
  put: <T = unknown>(entries: Record<string, T> | string, value?: T) => Promise<void>;
551
777
  setAlarm: (scheduledTime: number | Date) => Promise<void> | void;
@@ -556,13 +782,13 @@ interface SchedulerEnv {
556
782
  /**
557
783
  * Fallback bearer token attached to the dispatch when
558
784
  * {@link SchedulerEnv.LUNORA_SCHEDULER_SECRET} is not configured. Sent as
559
- * `authorization: Bearer &lt;token>`.
785
+ * `authorization: Bearer <token>`.
560
786
  */
561
787
  LUNORA_ADMIN_TOKEN?: string;
562
788
  /**
563
789
  * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
564
790
  * 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.
791
+ * request body, which carries no dispatch target at all) to prevent SSRF.
566
792
  */
567
793
  LUNORA_ORIGIN_URL?: string;
568
794
  /**
@@ -573,6 +799,21 @@ interface SchedulerEnv {
573
799
  */
574
800
  LUNORA_SCHEDULER_SECRET?: string;
575
801
  }
802
+ /**
803
+ * Retries allowed after the original attempt before {@link SchedulerDO.recordRetry}
804
+ * parks a record in the dead-letter (`dead:`) prefix. Overridable per job via
805
+ * {@link RetryPolicy.maxAttempts}. Exported so test doubles of the scheduler
806
+ * (`@lunora/testing`'s fake scheduler) model the same budget instead of
807
+ * duplicating the number.
808
+ */
809
+ declare const MAX_RETRY_ATTEMPTS = 5;
810
+ /**
811
+ * Backoff before the first retry, in milliseconds; doubles on each subsequent
812
+ * retry under the default `"exponential"` backoff (`baseMs * 2 ** (attempts - 1)`).
813
+ * Overridable per job via {@link RetryPolicy.baseMs}. Exported alongside
814
+ * {@link MAX_RETRY_ATTEMPTS} for the same reason.
815
+ */
816
+ declare const RETRY_BASE_DELAY_MS = 3e4;
576
817
  /**
577
818
  * One pool's live backlog, as surfaced by `GET /status`. `inFlight`/
578
819
  * `maxConcurrency` mirror the durable {@link PoolState} semaphore; `queued`
@@ -583,7 +824,7 @@ interface SchedulerPoolStatus {
583
824
  inFlight: number;
584
825
  /** The pool's concurrency cap. */
585
826
  maxConcurrency: number;
586
- /** The logical workpool name (the `pool:&lt;name>` suffix). */
827
+ /** The logical workpool name (the `pool:<name>` suffix). */
587
828
  name: string;
588
829
  /** Pending jobs routed to this pool but not yet dispatched. */
589
830
  queued: number;
@@ -598,13 +839,13 @@ interface SchedulerStatus {
598
839
  backlog: number;
599
840
  /** Sum of every pool's `inFlight` count — the total held concurrency slots. */
600
841
  inFlight: number;
601
- /** Per-pool backlog breakdown, one entry per `pool:&lt;name>` record. */
842
+ /** Per-pool backlog breakdown, one entry per `pool:<name>` record. */
602
843
  pools: SchedulerPoolStatus[];
603
844
  }
604
845
  /**
605
846
  * Durable Object that stores pending scheduled invocations sorted by their
606
847
  * `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
848
+ * `id:<id>` maps to {@link ScheduleRecord}; `t:<paddedTime>:<id>` maps to the
608
849
  * id (used as a sorted index).
609
850
  *
610
851
  * On every mutation the DO recomputes the earliest pending task and updates
@@ -651,15 +892,57 @@ declare class SchedulerDO {
651
892
  /**
652
893
  * Normalize the mutually-exclusive dispatch target off an untrusted body: a
653
894
  * one-shot function path (`functionPath`) or a durable workflow/agent
654
- * instance (`workflow`, a `WORKFLOW_*`/`AGENT_*` binding). Returns `undefined`
895
+ * instance (`workflow`, a workflow/agent export key). Returns `undefined`
655
896
  * when neither is present so the caller can reject the schedule.
656
897
  */
657
898
  private static resolveScheduleTarget;
658
899
  protected readonly state: SchedulerDOState;
659
900
  protected readonly env: SchedulerEnv;
901
+ /**
902
+ * Whether {@link SchedulerDO.reindexOrphanedRecords} has already run in THIS
903
+ * instance. Once is enough: an orphan can only be minted by an eviction, and
904
+ * an eviction ends the instance that minted it.
905
+ */
906
+ private reindexed;
907
+ /**
908
+ * Tail of the serialized `pool:<name>` critical section — see
909
+ * the pool lock (`withPoolLock`). Per-instance, which is the right scope: the pool rows
910
+ * live in this Durable Object's own storage and only this instance writes
911
+ * them.
912
+ */
913
+ private poolLock;
914
+ /**
915
+ * Record id → the `t:` index key its in-flight claim currently holds, for
916
+ * every dispatch this instance has open. See `drainRecordGuarded()`.
917
+ *
918
+ * A lease moves a record's index entry without rewriting its `scheduledFor`,
919
+ * so while a claim is held the live key is NOT the one derivable from the
920
+ * record. Anything that has to remove such a record — `removeRecord()`, on
921
+ * the `/cancel` path — would otherwise delete a key that no longer exists
922
+ * and strand the real one.
923
+ *
924
+ * In-memory and per-instance on purpose: it answers "is THIS instance
925
+ * dispatching that record right now", which is exactly when the divergence
926
+ * can be observed by another request. A lease left behind by an instance that
927
+ * died has no live dispatch to protect and is reconciled from storage
928
+ * instead — `alarm()` drops it as a dangling entry once the header is gone.
929
+ */
930
+ private readonly activeLeases;
931
+ /**
932
+ * Ids `/cancel` removed while this instance held their dispatch lease.
933
+ *
934
+ * The attempt is already in flight when such a cancel lands, and whatever it
935
+ * settles to must not write the job back: `recordRetry()` and
936
+ * `requeuePooled()` re-create the `id:` header and a `t:` entry, so a
937
+ * failed attempt would resurrect a job `/cancel` answered `{ cancelled: true }`
938
+ * for, and it would fire again. Both check this set synchronously right
939
+ * before their single write, so no `/cancel` can land between the check and
940
+ * the write. Cleared with the lease in `drainRecordGuarded()`.
941
+ */
942
+ private readonly cancelledLeases;
660
943
  constructor(state: SchedulerDOState, env: SchedulerEnv);
661
944
  fetch(request: Request): Promise<Response>;
662
- /** Called by the Workers runtime when the alarm previously set by `_rescheduleAlarm()` fires. */
945
+ /** Called by the Workers runtime when the alarm previously set by `rescheduleAlarm()` fires. */
663
946
  alarm(): Promise<void>;
664
947
  /**
665
948
  * Internal dispatch hook; overridden in unit tests to capture the outgoing
@@ -672,31 +955,119 @@ declare class SchedulerDO {
672
955
  * drop. After {@link MAX_RETRY_ATTEMPTS} the record is parked under a
673
956
  * `dead:` key for inspection — never silently deleted.
674
957
  *
958
+ * One non-2xx is told apart: a `409` carrying `DISPATCH_IN_PROGRESS` returns
959
+ * `"in-progress"`. The shard is saying the first attempt of this very record
960
+ * is still running, so {@link recordRetry} re-arms it without charging an
961
+ * attempt — see there.
962
+ *
675
963
  * 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
964
+ * stored record) so a schedule request can never name where the DO calls
965
+ * back — that would be SSRF. If that env var is missing at fire time (a deploy/binding
678
966
  * regression — schedule time already enforced its presence) we return
679
967
  * `false` so the record is retried rather than silently dropped.
680
968
  */
681
- protected dispatch(record: ScheduleRecord): Promise<boolean>;
969
+ protected dispatch(record: ScheduleRecord): Promise<boolean | "in-progress">;
970
+ /**
971
+ * Register the hibernation-safe ping/pong keepalive. The runtime answers a
972
+ * {@link WS_KEEPALIVE_PING} text frame with {@link WS_KEEPALIVE_PONG}
973
+ * WITHOUT waking this Durable Object, keeping an idle `/ws` subscription
974
+ * alive across hibernation with no billable wakeup and no dispatch. Without
975
+ * this, a client's heartbeat ping goes unanswered and its watchdog force-
976
+ * closes the socket every ~90s, defeating hibernation (each unanswered ping
977
+ * wakes the DO to reconnect) — mirrors `@lunora/do`'s
978
+ * `ShardDO.armWebSocketKeepalive`. The auto-response is per-instance, so
979
+ * this re-runs on every construction (including a post-hibernation wake).
980
+ * Guarded: the API and the `WebSocketRequestResponsePair` global are absent
981
+ * in the unit harness and on older runtimes, where it degrades to a no-op.
982
+ */
983
+ private armWebSocketKeepalive;
984
+ /**
985
+ * Drain the due slice with up to {@link MAX_CONCURRENT_DISPATCHES} records in
986
+ * flight at once, so a slow job delays only its own lane instead of every
987
+ * other due job in the app (see the constant for why the drain used to
988
+ * serialise whole jobs, not just their kicks).
989
+ *
990
+ * Lanes pull from the head of the slice, so records still ENTER dispatch in
991
+ * the slice's order — the same `t:<paddedTime>:<id>` order the sequential
992
+ * drain used. What is no longer implied is that they FINISH in that order,
993
+ * which was never a guarantee worth relying on anyway: two jobs due at the
994
+ * same instant already ran in storage-key (id) order rather than arrival
995
+ * order, a saturated pooled job is pushed {@link POOL_BACKPRESSURE_DELAY_MS}
996
+ * into the future ahead of its queue-mates, and a failed job re-enters at the
997
+ * end of a backoff. Nothing in the public surface documents an ordering
998
+ * guarantee; jobs that must be ordered must chain themselves.
999
+ *
1000
+ * {@link drainRecordGuarded} swallows every throw, so no lane can reject and
1001
+ * abandon its siblings.
1002
+ */
1003
+ private drainDue;
1004
+ /**
1005
+ * Run `critical` with exclusive access to the `pool:<name>` rows.
1006
+ *
1007
+ * Every pool mutation is a read-modify-write (`loadPool` → mutate →
1008
+ * `savePool`), and the two halves are separated by an `await`. That was safe
1009
+ * while the drain ran one record at a time — the Durable Object input gate
1010
+ * keeps a foreign event (a concurrent `/complete`) out while a storage
1011
+ * operation is in flight, and nothing else in this instance could interleave.
1012
+ * Concurrent lanes break exactly that assumption: lane A and lane B can both
1013
+ * issue their `get` before either `put` lands, both observe `inFlight: 0`,
1014
+ * and the second `put` then erases the first lane's reservation — the pool
1015
+ * oversubscribes and one holder's id is lost, so its slot is never released.
1016
+ *
1017
+ * The lock is a promise chain rather than anything cleverer because the
1018
+ * critical section is two local storage ops with no I/O in it. It is held
1019
+ * across NO outbound fetch: `dispatch()` runs outside it, which is the whole
1020
+ * point of draining concurrently.
1021
+ *
1022
+ * The chain is rebuilt from a swallowed copy so one rejecting section can
1023
+ * never wedge every later one.
1024
+ */
1025
+ private withPoolLock;
682
1026
  /**
683
1027
  * Claim + drain one due record with per-record fault isolation, so a storage
684
1028
  * throw can never abort the whole alarm pass (which would skip the remaining
685
1029
  * due records and the `rescheduleAlarm()` that re-arms the clock).
686
1030
  *
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.
1031
+ * The claim is a LEASE, not a deletion. The record's `t:` entry is moved
1032
+ * from its due time to `now + DISPATCH_LEASE_MS` before
1033
+ * {@link SchedulerDO.dispatch} is called, so this alarm pass (and the next)
1034
+ * will not pick it up again, while the record is never left WITHOUT an index
1035
+ * entry. That distinction is the whole point: deleting the entry outright
1036
+ * made an instance evicted mid-dispatch leave an `id:` header with no index,
1037
+ * which {@link SchedulerDO.reindexOrphanedRecords} re-armed and the
1038
+ * successor fired AGAIN on sight — concurrently with an attempt that could
1039
+ * still be running at the origin. A leased record is not an orphan, so the
1040
+ * successor leaves it alone until the lease lapses; see
1041
+ * {@link DISPATCH_LEASE_MS} for why that horizon is fifteen minutes and what
1042
+ * it does and does not bound.
1043
+ *
1044
+ * The lease is released as soon as this instance knows the attempt settled —
1045
+ * dispatched, re-armed for retry, backpressured, or dead-lettered — so the
1046
+ * horizon only ever governs the one case nobody is left to report: a lost
1047
+ * instance.
690
1048
  *
691
1049
  * A throw reaching here always means the job was NOT dispatched:
692
1050
  * {@link drainRecord} swallows its own post-dispatch cleanup errors and
693
1051
  * 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.
1052
+ * comes from the pre-dispatch or failed-dispatch paths. Nothing is in flight,
1053
+ * so the lease is dropped and the due-time claim re-asserted — keeping the
1054
+ * job re-fireable on the very next alarm (at-least-once) rather than letting
1055
+ * a transient storage blip cost it a whole lease.
1056
+ *
1057
+ * With one exception, checked first: a record that already has a durable
1058
+ * `dead:` row is TERMINAL, and re-claiming it would re-dispatch a job the
1059
+ * dead-letter says is finished. See the comment on that branch.
1060
+ *
1061
+ * `claimKey` is the index key `alarm()` SELECTED this record under, passed
1062
+ * down rather than recomputed. That is load-bearing, not tidiness: a lease
1063
+ * moves the record's index entry and deliberately does NOT rewrite its
1064
+ * `scheduledFor` (that field is the job's real due time, which `/list`,
1065
+ * `/get`, `/dead` and the studio all show, and which `parkDead` preserves).
1066
+ * So from the moment a lease is taken the live key and the key derivable
1067
+ * from the record disagree — and a claim that recomputed it would delete a
1068
+ * key that no longer exists while adding a second one, leaving the record
1069
+ * indexed twice and dispatched twice. That is the same double-run the lease
1070
+ * exists to close, re-entering through the expiry path.
700
1071
  */
701
1072
  private drainRecordGuarded;
702
1073
  /**
@@ -727,12 +1098,15 @@ declare class SchedulerDO {
727
1098
  * always return `true` without touching any pool state.
728
1099
  *
729
1100
  * 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).
1101
+ * across the drain — and the read-modify-write runs under
1102
+ * the pool lock (`withPoolLock`). Both halves are load-bearing. Freshness is what
1103
+ * keeps a concurrent `/complete` landing during a dispatch from being
1104
+ * clobbered by a stale in-memory copy (which would leak a slot permanently: a
1105
+ * POOL SLOT has no expiry, unlike the dispatch claim's). The lock is what
1106
+ * keeps two drain lanes from both reading the same pre-reservation row and
1107
+ * both believing a slot was free — without it the pool oversubscribes past
1108
+ * `maxConcurrency` and one holder's id is dropped from `inFlightIds`, so its
1109
+ * slot is never released.
736
1110
  */
737
1111
  private reservePoolSlot;
738
1112
  /**
@@ -744,13 +1118,33 @@ declare class SchedulerDO {
744
1118
  */
745
1119
  private handleWebSocketUpgrade;
746
1120
  /**
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.
1121
+ * Re-list the jobs (bounded — see {@link listPage}) and push them to
1122
+ * every connected subscriber. Called after any change (schedule / cancel /
1123
+ * alarm-fire) so live studios reflect it immediately. A no-op when the
1124
+ * runtime doesn't support hibernated sockets.
750
1125
  */
751
1126
  private broadcastChange;
752
- /** The current pending job records (shared by `/list` and the live channel). */
753
- private listRecords;
1127
+ /**
1128
+ * One bounded page of the rows under `prefix`, in key order, plus the
1129
+ * `cursor` a caller resumes from (the last key of the page) when `truncated`.
1130
+ * Lists `limit + 1` and slices back down so both facts are known without a
1131
+ * second round-trip.
1132
+ *
1133
+ * Shared by `/list` (pending headers) and `/dead` (dead-letter records) so
1134
+ * NEITHER can materialize an unbounded set into one JSON response: nothing
1135
+ * prunes `dead:`, so a workpool with a broken origin parks thousands of rows
1136
+ * and the studio's only view of them — and only way to requeue them — would
1137
+ * fail exactly when it is needed.
1138
+ */
1139
+ private listPage;
1140
+ /**
1141
+ * Page through every row under `prefix` exactly once with bounded per-page
1142
+ * memory (a `limit`+`startAfter` cursor loop), invoking `visit` for each.
1143
+ * Unlike {@link listPage}, which intentionally truncates for the studio's
1144
+ * live view, `/status` and `/pool` need EXACT counts — this walks the full
1145
+ * set, but never materializes more than one page at a time.
1146
+ */
1147
+ private forEachPage;
754
1148
  /**
755
1149
  * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
756
1150
  * returning a base64url signature, or `undefined` when no secret is
@@ -763,9 +1157,31 @@ declare class SchedulerDO {
763
1157
  * The retry budget/backoff comes from the record's {@link RetryPolicy}
764
1158
  * (falling back to the DO defaults); on exhaustion the record is parked
765
1159
  * under a `dead:` key for manual inspection.
1160
+ *
1161
+ * **A `DISPATCH_IN_PROGRESS` decline (`inProgress`) is not charged.** It
1162
+ * means the record's first attempt is still running on the shard, not that
1163
+ * anything failed; charging it would spend the budget while that attempt is
1164
+ * doing the work, and if it then failed the job would dead-letter having
1165
+ * run once. It still re-arms at the backoff of the NEXT charged attempt, at
1166
+ * least {@link RETRY_BASE_DELAY_MS} apart — nothing else rate-limits an
1167
+ * uncharged retry, and polling a running attempt faster gains nothing.
1168
+ *
1169
+ * It is bounded, not forever, only because the shard's claim is: a claim
1170
+ * older than the shard's fifteen-minute ceiling is treated as stale and the
1171
+ * next delivery runs (and is charged as usual if it fails). A handler that
1172
+ * hangs therefore costs at most that long in declines before the ordinary
1173
+ * budget applies again. Without that ceiling, not charging would let one hung
1174
+ * handler keep its record retrying and never dead-letter.
766
1175
  */
767
1176
  private recordRetry;
768
- /** Read the durable `pool:&lt;name>` row, defaulting to a fresh `inFlight: 0` pool. */
1177
+ /**
1178
+ * Terminal park into the dead-letter (`dead:`) prefix, with `reason` naming
1179
+ * why in the emitted warning. Shared by the two ways a retry ends for good:
1180
+ * an exhausted attempt budget, and a backoff that ran past the largest
1181
+ * schedulable time (see {@link isIndexableTime}).
1182
+ */
1183
+ private parkDead;
1184
+ /** Read the durable `pool:<name>` row, defaulting to a fresh `inFlight: 0` pool. */
769
1185
  private loadPool;
770
1186
  private savePool;
771
1187
  /**
@@ -787,7 +1203,7 @@ declare class SchedulerDO {
787
1203
  private handlePoolStatus;
788
1204
  /**
789
1205
  * `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`/
1206
+ * SLO view. Enumerates every durable `pool:<name>` row for its `inFlight`/
791
1207
  * `maxConcurrency` semaphore, counts the pending (not-yet-dispatched) jobs
792
1208
  * routed to each pool with the same single-pass scan {@link handlePoolStatus}
793
1209
  * uses, and rolls those up into app-wide `backlog` (sum of `queued`) and
@@ -796,19 +1212,64 @@ declare class SchedulerDO {
796
1212
  * Pools that have rows but no queued jobs still appear (with `queued: 0`) so
797
1213
  * a saturated-but-idle pool stays visible; a pool that only ever existed as
798
1214
  * 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.
1215
+ * always writes a `pool:<name>` row before the job's header), so a single
1216
+ * scan over `pool:` plus a cursor loop over `id:` is sufficient.
801
1217
  */
802
1218
  private handleStatus;
1219
+ /**
1220
+ * Persist (or refresh) a pool's concurrency cap, so the alarm-time gate has
1221
+ * a durable `maxConcurrency` even after the enqueuing client is gone.
1222
+ */
1223
+ private persistPoolCap;
1224
+ /**
1225
+ * The `409` a caller-supplied id earns when something durable already holds
1226
+ * it, or `undefined` when the id is free.
1227
+ *
1228
+ * A pending header is the obvious half: `put` on `id:<id>` overwrites, but
1229
+ * the `t:` index is keyed by TIME as well as id, so the OLD entry survives.
1230
+ * The drain then dispatches the NEW record at the OLD time and deletes the
1231
+ * entry it should have fired at — the job runs early and never runs again.
1232
+ * Refused rather than made a replace: `RunOptions.id` exists so a deferred
1233
+ * schedule can name its own job, and silently retiming someone else's is the
1234
+ * worse failure.
1235
+ *
1236
+ * The `dead:` row holds the id too, and for a worse reason. A dead record
1237
+ * keeps NO `id:` header, so a pending-only check leaves the id apparently
1238
+ * free — and a later `/dead/retry` writes the revived corpse straight over
1239
+ * the new job's header and adds a SECOND time index under the same id. The
1240
+ * new job is gone and the dead one fires in its place. Recovering a dead job
1241
+ * is an operator action taken minutes or days after the schedule, so nothing
1242
+ * at schedule time would ever have surfaced the collision.
1243
+ */
1244
+ private idConflict;
1245
+ /**
1246
+ * The id the record is stored under, or the `Response` refusing it.
1247
+ *
1248
+ * A caller id that is not a safe key segment is refused with a `400` rather
1249
+ * than minted over: `RunOptions.id` is not an idempotency key, so swapping an
1250
+ * invalid one for a random id made two calls naming it schedule two jobs
1251
+ * where the second should have answered `409`.
1252
+ *
1253
+ * Only an id the CALLER chose can collide — a minted one is 96 random bits —
1254
+ * so {@link idConflict} costs two `get`s on the deferred path and nothing on
1255
+ * the ordinary one.
1256
+ */
1257
+ private resolveId;
803
1258
  private handleSchedule;
804
1259
  private handleCancel;
1260
+ /**
1261
+ * `GET /list[?cursor=]` — one bounded page of pending jobs. `truncated` says
1262
+ * whether more rows follow and `cursor` is what a caller passes back to get
1263
+ * them (`createScheduler.list()` walks every page; the studio shows one).
1264
+ */
805
1265
  private handleList;
806
1266
  /**
807
1267
  * `GET /dead` — list the dead-letter records: jobs that exhausted their
808
- * retry budget ({@link recordRetry}) and were parked under `dead:&lt;id>`
1268
+ * retry budget ({@link recordRetry}) and were parked under `dead:<id>`
809
1269
  * instead of being silently dropped. These never appear in `/list` (their
810
1270
  * `id:` header is deleted on park), so this is the ONLY way the studio can
811
- * surface — and recover — a permanently-failed job.
1271
+ * surface — and recover — a permanently-failed job. Bounded and cursored
1272
+ * like `/list`: nothing prunes `dead:`, so this set grows without limit.
812
1273
  */
813
1274
  private handleDeadList;
814
1275
  /**
@@ -826,7 +1287,7 @@ declare class SchedulerDO {
826
1287
  */
827
1288
  private handleDeadCancel;
828
1289
  /**
829
- * Resolve a single pending job by id via a direct `id:&lt;id>` storage read —
1290
+ * Resolve a single pending job by id via a direct `id:<id>` storage read —
830
1291
  * O(1), versus scanning the whole `/list` view. Responds `{ record }` on a
831
1292
  * hit and `{}` on a miss (an absent `record` field — JSON has no `undefined`
832
1293
  * — which the client reads back as `null`).
@@ -840,14 +1301,129 @@ declare class SchedulerDO {
840
1301
  * full `t:` rescan is unnecessary unless the new job is the new earliest.
841
1302
  */
842
1303
  private armAlarmIfEarlier;
1304
+ /**
1305
+ * Re-index every pending job whose time-index entry is gone, and fire it now.
1306
+ *
1307
+ * A record with an `id:` header but no `t:` entry is invisible to every
1308
+ * clock in this class: {@link SchedulerDO.rescheduleAlarm} derives the alarm
1309
+ * from `t:` alone, and `alarm()`'s inline reconciliation only handles the
1310
+ * INVERSE orphan (a `t:` entry whose header is gone). Left alone such a job
1311
+ * sits in `/list` and `/status.backlog` forever — never fires, never reaches
1312
+ * `/dead`.
1313
+ *
1314
+ * **This is no longer how a lost dispatch looks.**
1315
+ * {@link SchedulerDO.drainRecordGuarded} used to claim a record by deleting
1316
+ * its `t:` entry outright, so an instance evicted mid-dispatch minted an
1317
+ * orphan on every claimed record and this method re-fired each of them ON
1318
+ * SIGHT — concurrently with attempts that could still have been running at
1319
+ * the origin. The claim is a lease now (see {@link DISPATCH_LEASE_MS}): a
1320
+ * claimed record keeps a `t:` entry at the lease horizon, so it is not an
1321
+ * orphan, is not recovered here, and is re-fired by the ordinary alarm drain
1322
+ * only once the lease lapses.
1323
+ *
1324
+ * What still reaches this method is the residue of a storage failure — a
1325
+ * claim released with no lease written, or a pre-lease record written by an
1326
+ * older build — where nothing was dispatched and firing immediately is
1327
+ * exactly right.
1328
+ *
1329
+ * The dispatch is deduplicated by the record id, which the receiver spends
1330
+ * as `x-lunora-mutation-id` for a function target and as the workflow
1331
+ * INSTANCE id for a `workflow` target. A mutation's dedup read runs inside
1332
+ * the shard's single-writer gate, so a mutation is exactly-once even under a
1333
+ * genuinely concurrent re-fire; a workflow re-attaches to the running
1334
+ * instance rather than starting a second one. An ACTION takes no gate —
1335
+ * gating one would let any caller freeze a whole shard for the length of an
1336
+ * action's outbound I/O — and its dedup row is written only after the
1337
+ * handler returns, so the cache alone could not tell "already ran" from
1338
+ * "still running". The shard closes that itself (#803): it claims the dedup
1339
+ * key before entering the handler and answers a second delivery of a live
1340
+ * key `409 DISPATCH_IN_PROGRESS`, which is a non-2xx, so the record is
1341
+ * re-armed rather than cleared.
1342
+ *
1343
+ * Two bounded walks (all `t:` values, then all `id:` headers) rather than a
1344
+ * per-header `get`, so the cost is one pass over each prefix.
1345
+ */
1346
+ private reindexOrphanedRecords;
1347
+ /**
1348
+ * The time component of the earliest pending `t:` entry, or `undefined` when
1349
+ * there is no pending entry at all. Reads exactly one row — the index's
1350
+ * lexical order is its numeric order. A present-but-unreadable key yields
1351
+ * `NaN`, which callers must distinguish from "nothing pending": the two
1352
+ * answers mean opposite things for the alarm.
1353
+ */
1354
+ private earliestPendingTime;
843
1355
  private rescheduleAlarm;
844
1356
  }
845
- /** Standard cron expression (5- or 6-field) or a supported `@macro`, per `cron-parser`. */
846
- declare const isValidCronExpression: (schedule: string) => boolean;
1357
+ /** What the Cloudflare scheduler host needs from the Worker's environment. */
1358
+ interface SchedulerHostOptions {
1359
+ /**
1360
+ * Named scheduler instance — one `SchedulerDO` per name, useful for tenant
1361
+ * isolation. Defaults to `"default"`.
1362
+ */
1363
+ instanceName?: string;
1364
+ /**
1365
+ * Data-residency jurisdiction for the `SchedulerDO`. Pass the same value as
1366
+ * the worker's own jurisdiction so scheduled state co-resides with app data.
1367
+ */
1368
+ jurisdiction?: "eu" | "fedramp" | "us";
1369
+ /**
1370
+ * The `SchedulerDO` namespace binding.
1371
+ *
1372
+ * The origin the DO dispatches back to is not configured here — it reads
1373
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, so a wrong value
1374
+ * there (or none) is what makes jobs fire into nothing.
1375
+ */
1376
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
1377
+ }
847
1378
  /**
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.
1379
+ * Build the Cloudflare {@link SchedulerHost}.
1380
+ *
1381
+ * The returned host has no `cron` member — see the module docstring.
1382
+ */
1383
+ declare const createSchedulerHost: (options: SchedulerHostOptions) => SchedulerHost;
1384
+ /**
1385
+ * Reject a `delayMs` a scheduler cannot act on, before it reaches the
1386
+ * SchedulerDO.
1387
+ *
1388
+ * A `NaN`/`Infinity` delay serializes to `null` through JSON and lands as a
1389
+ * malformed `scheduledFor`; a negative one schedules into the past. Both are the
1390
+ * caller's argument, so the answer has to name the argument — which is why the
1391
+ * code is `INVALID_INPUT` (400) and not `INTERNAL`: `toErrorBody` replaces an
1392
+ * internal-coded message with "Internal error", redacting the one sentence that
1393
+ * says what to fix.
1394
+ *
1395
+ * Exported because four surfaces enforce it — `createScheduler().runAfter`,
1396
+ * `createWorkpool().enqueue`, `@lunora/server`'s deferred-schedule facade (which
1397
+ * must reject BEFORE the transaction commits, not at flush time) and
1398
+ * `@lunora/testing`'s fake scheduler. They used to restate it and threw three
1399
+ * different codes between them, so a test written against the harness caught one
1400
+ * code while production threw another.
1401
+ * @param delayMs the delay to validate
1402
+ * @param surface what to name in the message — the call the delay was passed to (e.g. `"ctx.scheduler.runAfter"`)
1403
+ * @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
1404
+ */
1405
+ declare const assertScheduleDelay: (delayMs: number, surface: string, argument?: string) => void;
1406
+ /**
1407
+ * Reject a `runAt` instant a scheduler cannot act on — {@link assertScheduleDelay}'s
1408
+ * bound, restated for the absolute form by converting the instant to the delay it
1409
+ * implies.
1410
+ *
1411
+ * `runAfter` has refused a `NaN`/`Infinity` argument since the guard was written;
1412
+ * `runAt` took the same value through a different door and let it reach the DO,
1413
+ * where it serializes to `null` through JSON and lands as a `scheduledFor` no
1414
+ * alarm can ever fire. `new Date("2026-13-01")`, `runAt(row.dueAt + delay)` on a
1415
+ * row whose `dueAt` is absent — both arrive here as a number that is not one.
1416
+ *
1417
+ * An instant already in the PAST is not refused. It is an overdue job
1418
+ * (`runAt(row.dueAt)` on a row that came due while the request was in flight),
1419
+ * and `runAfter` itself reaches `runAt` a fraction of a millisecond after
1420
+ * capturing its own clock reading — so a strict sign check would fail the
1421
+ * documented `runAfter(0, …)` call at random. The delay is therefore clamped at
1422
+ * zero while it is still a finite number, which leaves the half that matters:
1423
+ * a value that is not a number at all.
1424
+ * @param timestampMs the absolute instant (epoch ms) the caller passed
1425
+ * @param nowMs the clock to measure it against — the wall clock in production, the harness's virtual clock in a test
1426
+ * @param surface what to name in the message — the call the instant was passed to (e.g. `"ctx.scheduler.runAt"`)
851
1427
  */
852
- declare const assertValidCronExpression: (schedule: string, context?: string) => void;
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 };
1428
+ declare const assertScheduleInstant: (timestampMs: number, nowMs: number, surface: string) => void;
1429
+ 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, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createSchedulerHost, createWorkpool, cronJobs, httpDispatcher, isWorkflowReference, resolveScheduleId };