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