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