@lunora/scheduler 1.0.0-alpha.49 → 1.0.0-alpha.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +149 -10
- package/dist/index.d.ts +149 -10
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/MAX_RETRY_ATTEMPTS-BGuDiMHI.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/createQueueConsumer-UOOsL96M.mjs +1 -0
- package/dist/packem_shared/createScheduler-wS5CM4jl.mjs +1 -0
- package/dist/packem_shared/{createSchedulerHost-bI3AKuoP.mjs → createSchedulerHost-BJnHgqi_.mjs} +1 -1
- package/dist/packem_shared/createWorkpool-B-fAnJ7E.mjs +1 -0
- package/dist/packem_shared/do-client-B8HGJ5LF.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 +3 -3
- package/dist/packem_shared/MAX_RETRY_ATTEMPTS-B2mOoWHR.mjs +0 -1
- package/dist/packem_shared/createQueueConsumer-adiyW-4L.mjs +0 -1
- package/dist/packem_shared/createScheduler-DismdyOw.mjs +0 -1
- package/dist/packem_shared/createWorkpool-d3THj5gc.mjs +0 -1
- package/dist/packem_shared/do-client-BKEFA9pM.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -111,12 +111,34 @@ interface RetryPolicy {
|
|
|
111
111
|
backoff?: "exponential" | "linear";
|
|
112
112
|
/** Base delay in milliseconds for the first retry. Default `30_000`. */
|
|
113
113
|
baseMs?: number;
|
|
114
|
-
/**
|
|
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
|
+
*/
|
|
115
119
|
maxAttempts?: number;
|
|
116
120
|
/** Optional ceiling clamping the computed backoff delay. */
|
|
117
121
|
maxMs?: number;
|
|
118
122
|
}
|
|
119
123
|
interface RunOptions {
|
|
124
|
+
/**
|
|
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;
|
|
120
142
|
/**
|
|
121
143
|
* Cap for the {@link RunOptions.pool} this job joins, applied when the pool
|
|
122
144
|
* is first created and refreshed on every enqueue that carries one. Ignored
|
|
@@ -491,8 +513,18 @@ interface CronTriggerSnippet {
|
|
|
491
513
|
declare const createCronTrigger: (options: CronTriggerOptions) => CronTriggerSnippet;
|
|
492
514
|
/** Sub-day recurrence. Exactly one unit must be provided. */
|
|
493
515
|
interface IntervalSchedule {
|
|
516
|
+
/** 1–23, and must divide 24 (an interval repeats *within* a day). */
|
|
494
517
|
hours?: number;
|
|
518
|
+
/** 1–59, and must divide 60. */
|
|
495
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
|
+
*/
|
|
496
528
|
seconds?: number;
|
|
497
529
|
}
|
|
498
530
|
/** Daily recurrence at a fixed UTC wall-clock time. */
|
|
@@ -580,7 +612,7 @@ interface CronJobsBuilder {
|
|
|
580
612
|
daily: <T extends CronTarget>(name: string, schedule: DailySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
|
|
581
613
|
/** Hourly at `minuteUTC` past the hour. The target may be a function or a durable workflow (`workflows.<name>`). */
|
|
582
614
|
hourly: <T extends CronTarget>(name: string, schedule: HourlySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
|
|
583
|
-
/** Every `{
|
|
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>`). */
|
|
584
616
|
interval: <T extends CronTarget>(name: string, schedule: IntervalSchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
|
|
585
617
|
/** Snapshot of the registered jobs, in declaration order. */
|
|
586
618
|
jobs: () => ReadonlyArray<CronJob>;
|
|
@@ -624,6 +656,20 @@ declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: M
|
|
|
624
656
|
* evidence the job ran. An empty 2xx is a normal success (a `void` function).
|
|
625
657
|
*/
|
|
626
658
|
declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
|
|
659
|
+
/**
|
|
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;
|
|
627
673
|
/**
|
|
628
674
|
* Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
|
|
629
675
|
* structurally so unit tests can pass a fake state without booting the
|
|
@@ -781,6 +827,12 @@ declare class SchedulerDO {
|
|
|
781
827
|
private static resolveScheduleTarget;
|
|
782
828
|
protected readonly state: SchedulerDOState;
|
|
783
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;
|
|
784
836
|
constructor(state: SchedulerDOState, env: SchedulerEnv);
|
|
785
837
|
fetch(request: Request): Promise<Response>;
|
|
786
838
|
/** Called by the Workers runtime when the alarm previously set by `_rescheduleAlarm()` fires. */
|
|
@@ -885,7 +937,7 @@ declare class SchedulerDO {
|
|
|
885
937
|
*/
|
|
886
938
|
private handleWebSocketUpgrade;
|
|
887
939
|
/**
|
|
888
|
-
* Re-list the jobs (bounded — see {@link
|
|
940
|
+
* Re-list the jobs (bounded — see {@link listPage}) and push them to
|
|
889
941
|
* every connected subscriber. Called after any change (schedule / cancel /
|
|
890
942
|
* alarm-fire) so live studios reflect it immediately. A no-op when the
|
|
891
943
|
* runtime doesn't support hibernated sockets.
|
|
@@ -904,12 +956,6 @@ declare class SchedulerDO {
|
|
|
904
956
|
* fail exactly when it is needed.
|
|
905
957
|
*/
|
|
906
958
|
private listPage;
|
|
907
|
-
/**
|
|
908
|
-
* The current pending job records (shared by `/list` and the live channel),
|
|
909
|
-
* bounded to `limit` (default {@link DEFAULT_LIST_LIMIT}) so a large backlog
|
|
910
|
-
* can't be JSON-serialized and fanned out to every socket in one shot.
|
|
911
|
-
*/
|
|
912
|
-
private listRecords;
|
|
913
959
|
/**
|
|
914
960
|
* Page through every row under `prefix` exactly once with bounded per-page
|
|
915
961
|
* memory (a `limit`+`startAfter` cursor loop), invoking `visit` for each.
|
|
@@ -974,6 +1020,32 @@ declare class SchedulerDO {
|
|
|
974
1020
|
* scan over `pool:` plus a cursor loop over `id:` is sufficient.
|
|
975
1021
|
*/
|
|
976
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;
|
|
977
1049
|
private handleSchedule;
|
|
978
1050
|
private handleCancel;
|
|
979
1051
|
/**
|
|
@@ -1020,6 +1092,28 @@ declare class SchedulerDO {
|
|
|
1020
1092
|
* full `t:` rescan is unnecessary unless the new job is the new earliest.
|
|
1021
1093
|
*/
|
|
1022
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;
|
|
1023
1117
|
private rescheduleAlarm;
|
|
1024
1118
|
}
|
|
1025
1119
|
/** What the Cloudflare scheduler host needs from the Worker's environment. */
|
|
@@ -1076,4 +1170,49 @@ declare const warnIfSecondsLeading: (schedule: string, context: string) => void;
|
|
|
1076
1170
|
* {@link warnIfSecondsLeading}.
|
|
1077
1171
|
*/
|
|
1078
1172
|
declare const assertValidCronExpression: (schedule: string, context?: string) => void;
|
|
1079
|
-
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -111,12 +111,34 @@ interface RetryPolicy {
|
|
|
111
111
|
backoff?: "exponential" | "linear";
|
|
112
112
|
/** Base delay in milliseconds for the first retry. Default `30_000`. */
|
|
113
113
|
baseMs?: number;
|
|
114
|
-
/**
|
|
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
|
+
*/
|
|
115
119
|
maxAttempts?: number;
|
|
116
120
|
/** Optional ceiling clamping the computed backoff delay. */
|
|
117
121
|
maxMs?: number;
|
|
118
122
|
}
|
|
119
123
|
interface RunOptions {
|
|
124
|
+
/**
|
|
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;
|
|
120
142
|
/**
|
|
121
143
|
* Cap for the {@link RunOptions.pool} this job joins, applied when the pool
|
|
122
144
|
* is first created and refreshed on every enqueue that carries one. Ignored
|
|
@@ -491,8 +513,18 @@ interface CronTriggerSnippet {
|
|
|
491
513
|
declare const createCronTrigger: (options: CronTriggerOptions) => CronTriggerSnippet;
|
|
492
514
|
/** Sub-day recurrence. Exactly one unit must be provided. */
|
|
493
515
|
interface IntervalSchedule {
|
|
516
|
+
/** 1–23, and must divide 24 (an interval repeats *within* a day). */
|
|
494
517
|
hours?: number;
|
|
518
|
+
/** 1–59, and must divide 60. */
|
|
495
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
|
+
*/
|
|
496
528
|
seconds?: number;
|
|
497
529
|
}
|
|
498
530
|
/** Daily recurrence at a fixed UTC wall-clock time. */
|
|
@@ -580,7 +612,7 @@ interface CronJobsBuilder {
|
|
|
580
612
|
daily: <T extends CronTarget>(name: string, schedule: DailySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
|
|
581
613
|
/** Hourly at `minuteUTC` past the hour. The target may be a function or a durable workflow (`workflows.<name>`). */
|
|
582
614
|
hourly: <T extends CronTarget>(name: string, schedule: HourlySchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
|
|
583
|
-
/** Every `{
|
|
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>`). */
|
|
584
616
|
interval: <T extends CronTarget>(name: string, schedule: IntervalSchedule, target: T, args?: CronTargetArgs<T>) => CronJobsBuilder;
|
|
585
617
|
/** Snapshot of the registered jobs, in declaration order. */
|
|
586
618
|
jobs: () => ReadonlyArray<CronJob>;
|
|
@@ -624,6 +656,20 @@ declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: M
|
|
|
624
656
|
* evidence the job ran. An empty 2xx is a normal success (a `void` function).
|
|
625
657
|
*/
|
|
626
658
|
declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
|
|
659
|
+
/**
|
|
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;
|
|
627
673
|
/**
|
|
628
674
|
* Minimal projection of `DurableObjectState` for the SchedulerDO. Declared
|
|
629
675
|
* structurally so unit tests can pass a fake state without booting the
|
|
@@ -781,6 +827,12 @@ declare class SchedulerDO {
|
|
|
781
827
|
private static resolveScheduleTarget;
|
|
782
828
|
protected readonly state: SchedulerDOState;
|
|
783
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;
|
|
784
836
|
constructor(state: SchedulerDOState, env: SchedulerEnv);
|
|
785
837
|
fetch(request: Request): Promise<Response>;
|
|
786
838
|
/** Called by the Workers runtime when the alarm previously set by `_rescheduleAlarm()` fires. */
|
|
@@ -885,7 +937,7 @@ declare class SchedulerDO {
|
|
|
885
937
|
*/
|
|
886
938
|
private handleWebSocketUpgrade;
|
|
887
939
|
/**
|
|
888
|
-
* Re-list the jobs (bounded — see {@link
|
|
940
|
+
* Re-list the jobs (bounded — see {@link listPage}) and push them to
|
|
889
941
|
* every connected subscriber. Called after any change (schedule / cancel /
|
|
890
942
|
* alarm-fire) so live studios reflect it immediately. A no-op when the
|
|
891
943
|
* runtime doesn't support hibernated sockets.
|
|
@@ -904,12 +956,6 @@ declare class SchedulerDO {
|
|
|
904
956
|
* fail exactly when it is needed.
|
|
905
957
|
*/
|
|
906
958
|
private listPage;
|
|
907
|
-
/**
|
|
908
|
-
* The current pending job records (shared by `/list` and the live channel),
|
|
909
|
-
* bounded to `limit` (default {@link DEFAULT_LIST_LIMIT}) so a large backlog
|
|
910
|
-
* can't be JSON-serialized and fanned out to every socket in one shot.
|
|
911
|
-
*/
|
|
912
|
-
private listRecords;
|
|
913
959
|
/**
|
|
914
960
|
* Page through every row under `prefix` exactly once with bounded per-page
|
|
915
961
|
* memory (a `limit`+`startAfter` cursor loop), invoking `visit` for each.
|
|
@@ -974,6 +1020,32 @@ declare class SchedulerDO {
|
|
|
974
1020
|
* scan over `pool:` plus a cursor loop over `id:` is sufficient.
|
|
975
1021
|
*/
|
|
976
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;
|
|
977
1049
|
private handleSchedule;
|
|
978
1050
|
private handleCancel;
|
|
979
1051
|
/**
|
|
@@ -1020,6 +1092,28 @@ declare class SchedulerDO {
|
|
|
1020
1092
|
* full `t:` rescan is unnecessary unless the new job is the new earliest.
|
|
1021
1093
|
*/
|
|
1022
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;
|
|
1023
1117
|
private rescheduleAlarm;
|
|
1024
1118
|
}
|
|
1025
1119
|
/** What the Cloudflare scheduler host needs from the Worker's environment. */
|
|
@@ -1076,4 +1170,49 @@ declare const warnIfSecondsLeading: (schedule: string, context: string) => void;
|
|
|
1076
1170
|
* {@link warnIfSecondsLeading}.
|
|
1077
1171
|
*/
|
|
1078
1172
|
declare const assertValidCronExpression: (schedule: string, context?: string) => void;
|
|
1079
|
-
|
|
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{default as o}from"./packem_shared/createScheduler-
|
|
1
|
+
import{default as o}from"./packem_shared/createScheduler-wS5CM4jl.mjs";import{default as a}from"./packem_shared/createWorkpool-B-fAnJ7E.mjs";import{createCronTrigger as f}from"./packem_shared/createCronTrigger-DSuDZtqj.mjs";import{CRON_SCHEDULE_KINDS as p,compileCronSchedule as c,cronJobs as d}from"./packem_shared/CRON_SCHEDULE_KINDS-xyFk800s.mjs";import{createQueueConsumer as m,createQueueWorkpool as n,httpDispatcher as x}from"./packem_shared/createQueueConsumer-UOOsL96M.mjs";import{r as i}from"./packem_shared/resolve-schedule-id-B3a6xuQf.mjs";import{MAX_RETRY_ATTEMPTS as E,RETRY_BASE_DELAY_MS as C,SchedulerDO as _}from"./packem_shared/MAX_RETRY_ATTEMPTS-BGuDiMHI.mjs";import{createSchedulerHost as R}from"./packem_shared/createSchedulerHost-BJnHgqi_.mjs";import{isWorkflowReference as A}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as g,isValidCronExpression as k,warnIfSecondsLeading as L}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";import{default as W}from"./packem_shared/assertScheduleDelay-BgA4K1WB.mjs";import{default as w}from"./packem_shared/assertScheduleInstant-BzESPqyw.mjs";export{p as CRON_SCHEDULE_KINDS,E as MAX_RETRY_ATTEMPTS,C as RETRY_BASE_DELAY_MS,_ as SchedulerDO,W as assertScheduleDelay,w as assertScheduleInstant,g as assertValidCronExpression,c as compileCronSchedule,f as createCronTrigger,m as createQueueConsumer,n as createQueueWorkpool,o as createScheduler,R as createSchedulerHost,a as createWorkpool,d as cronJobs,x as httpDispatcher,k as isValidCronExpression,A as isWorkflowReference,i as resolveScheduleId,L as warnIfSecondsLeading};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as P,r as b}from"./resolve-schedule-id-B3a6xuQf.mjs";const I=(u,t=200,e)=>Response.json(u,{headers:{"content-type":"application/json",...e},status:t}),F="lunora-ping",x="lunora-pong",r="id:",f="retry:",g="dead:",y="pool:",m=100,R=5,A=3e4,N=1e3,$=999999999999999,E=15,v=u=>String(u).padStart(E,"0"),w=u=>Number.isInteger(u)&&u>0&&u<=$;class i{static indexKey(t,e){return`t:${v(t)}:${e}`}static json(t,e=200){return I(t,e)}static error(t,e,s){return i.json({error:{code:e,message:s}},t)}static resolveRetry(t){const e=t.retry,s=typeof e?.maxAttempts=="number"&&Number.isInteger(e.maxAttempts)&&e.maxAttempts>0?e.maxAttempts:R,a=typeof e?.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0?e.baseMs:A,n=e?.backoff==="linear"?"linear":"exponential",o=typeof e?.maxMs=="number"&&Number.isFinite(e.maxMs)&&e.maxMs>=0?e.maxMs:void 0;return{backoff:n,baseMs:a,maxAttempts:s,maxMs:o}}static normalizeConcurrency(t,e){return typeof t=="number"&&Number.isInteger(t)&&t>0?t:e}static normalizeRetry(t){if(typeof t!="object"||t===null)return;const e=t,s={};return typeof e.maxAttempts=="number"&&Number.isInteger(e.maxAttempts)&&e.maxAttempts>0&&(s.maxAttempts=e.maxAttempts),typeof e.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0&&(s.baseMs=e.baseMs),(e.backoff==="exponential"||e.backoff==="linear")&&(s.backoff=e.backoff),typeof e.maxMs=="number"&&Number.isFinite(e.maxMs)&&e.maxMs>=0&&(s.maxMs=e.maxMs),Object.keys(s).length===0?void 0:s}static releaseSlot(t,e){if(t.inFlightIds===void 0)return{...t,inFlight:Math.max(0,t.inFlight-1)};const s=t.inFlightIds.filter(a=>a!==e);return{...t,inFlight:s.length,inFlightIds:s}}static releaseFirstSlot(t){if(t.inFlightIds===void 0)return{...t,inFlight:Math.max(0,t.inFlight-1)};const e=t.inFlightIds.slice(0,Math.max(0,t.inFlightIds.length-1));return{...t,inFlight:e.length,inFlightIds:e}}static resolveScheduleTarget(t){const e=typeof t?.functionPath=="string"&&t.functionPath.length>0?t.functionPath:void 0,s=typeof t?.workflow=="string"&&t.workflow.length>0?t.workflow:void 0;if(!(e===void 0&&s===void 0))return{functionPath:e,workflow:s}}state;env;reindexed=!1;constructor(t,e){this.state=t,this.env=e,this.armWebSocketKeepalive()}async fetch(t){await this.reindexOrphanedRecords();const e=new URL(t.url);if(e.pathname==="/ws"&&t.headers.get("Upgrade")==="websocket")return this.handleWebSocketUpgrade();switch(`${t.method} ${e.pathname}`){case"GET /dead":return this.handleDeadList(e);case"GET /get":return this.handleGet(e);case"GET /list":return this.handleList(e);case"GET /pool":return this.handlePoolStatus(e);case"GET /status":return this.handleStatus();case"POST /cancel":return this.handleCancel(t);case"POST /complete":return this.handleComplete(t);case"POST /dead/cancel":return this.handleDeadCancel(t);case"POST /dead/retry":return this.handleDeadRetry(t);case"POST /schedule":return this.handleSchedule(t)}return I({error:{code:"NOT_FOUND"}},404)}async alarm(){await this.reindexOrphanedRecords();const t=Date.now(),e=[],s=await this.state.storage.list({end:`t:${v(t)}:~`,limit:100,prefix:"t:"});for(const[a,n]of s.entries()){const o=Number.parseInt(a.slice(2,a.indexOf(":",2)),10);if(Number.isFinite(o)&&o<=t){const c=await this.state.storage.get(`${r}${n}`);c?e.push(c):await this.state.storage.delete(a)}}try{for(const a of e)await this.drainRecordGuarded(a)}finally{await this.rescheduleAlarm()}e.length>0&&await this.broadcastChange()}async dispatch(t){const e=typeof this.env.LUNORA_ORIGIN_URL=="string"&&this.env.LUNORA_ORIGIN_URL.length>0?this.env.LUNORA_ORIGIN_URL:void 0;if(!e)return!1;const s=JSON.stringify({args:t.args,functionPath:t.functionPath,id:t.id,instanceName:t.instanceName,pool:t.pool,scheduledFor:t.scheduledFor,shardKey:t.shardKey,workflow:t.workflow});try{const a={"content-type":"application/json"},n=await this.signDispatch(s);return n!==void 0?a["x-lunora-scheduler-signature"]=n:typeof this.env.LUNORA_ADMIN_TOKEN=="string"&&this.env.LUNORA_ADMIN_TOKEN.length>0&&(a.authorization=`Bearer ${this.env.LUNORA_ADMIN_TOKEN}`),(await fetch(`${e}/_lunora/scheduler/dispatch`,{body:s,headers:a,method:"POST"})).ok}catch{return!1}}armWebSocketKeepalive(){const t=this.state.setWebSocketAutoResponse;typeof t!="function"||typeof WebSocketRequestResponsePair>"u"||t.call(this.state,new WebSocketRequestResponsePair(F,x))}async drainRecordGuarded(t){try{await this.state.storage.delete(i.indexKey(t.scheduledFor,t.id)),await this.drainRecord(t)}catch{try{if(await this.state.storage.get(`${g}${t.id}`)!==void 0){await this.state.storage.delete([`${f}${t.id}`,`${r}${t.id}`]);return}await this.state.storage.put(i.indexKey(t.scheduledFor,t.id),t.id)}catch{}}}async drainRecord(t){if(!await this.reservePoolSlot(t))return!1;const s=await this.dispatch(t);if(!s&&t.pool!==void 0){const a=await this.loadPool(t.pool),n=i.releaseSlot(a,t.id);await this.savePool(t.pool,n)}if(s){try{await this.state.storage.delete([`${r}${t.id}`,`${f}${t.id}`])}catch{}return!0}return await this.recordRetry(t),!1}async reservePoolSlot(t){if(t.pool===void 0)return!0;const e=await this.loadPool(t.pool);if(e.inFlight>=e.maxConcurrency)return await this.requeuePooled(t),!1;const s=e.inFlightIds??[];return s.includes(t.id)||s.push(t.id),e.inFlightIds=s,e.inFlight=s.length,await this.savePool(t.pool,e),!0}async handleWebSocketUpgrade(){if(this.state.acceptWebSocket===void 0)return i.error(501,"WS_UNSUPPORTED","WebSocket subscriptions are not supported in this runtime");const t=new WebSocketPair,e=t[0],s=t[1];this.state.acceptWebSocket(s);const a=await this.listPage(r,m);return s.send(JSON.stringify({records:a.records,truncated:a.truncated,type:"jobs"})),new Response(null,{status:101,webSocket:e})}async broadcastChange(){const t=this.state.getWebSockets?.();if(t===void 0||t.length===0)return;const{records:e,truncated:s}=await this.listPage(r,m),a=JSON.stringify({records:e,truncated:s,type:"jobs"});for(const n of t)try{n.send(a)}catch{}}async listPage(t,e,s){const a=await this.state.storage.list({limit:e+1,prefix:t,...s===void 0?{}:{startAfter:s}}),n=[...a.keys()],o=[...a.values()],c=o.length>e;return c?{cursor:n[e-1],records:o.slice(0,e),truncated:c}:{records:o,truncated:c}}async forEachPage(t,e,s=m){let a;for(;;){const n=await this.state.storage.list(a===void 0?{limit:s,prefix:t}:{limit:s,prefix:t,startAfter:a});if(n.size===0)break;for(const[c,l]of n.entries())e(l,c);if(a=[...n.keys()].at(-1),n.size<s)break}}async signDispatch(t){const e=typeof this.env.LUNORA_SCHEDULER_SECRET=="string"?this.env.LUNORA_SCHEDULER_SECRET:void 0;if(!e||e.length===0)return;const s=new TextEncoder,a=await crypto.subtle.importKey("raw",s.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),n=await crypto.subtle.sign("HMAC",a,s.encode(t));return P(new Uint8Array(n))}async recordRetry(t){const e=(t.attempts??0)+1,{backoff:s,baseMs:a,maxAttempts:n,maxMs:o}=i.resolveRetry(t),c=s==="linear"?a*e:a*2**(e-1),l=o===void 0?c:Math.min(c,o),d=Math.round(Date.now()+l);if(e>n){await this.parkDead(t,e,`after ${String(e)} attempts`);return}if(!w(d)){await this.parkDead(t,e,`at attempt ${String(e)}: the retry backoff exceeded the largest schedulable time`);return}const h={...t,attempts:e,scheduledFor:d};await this.state.storage.put(`${f}${t.id}`,h),await this.state.storage.put(`${r}${t.id}`,h),await this.state.storage.put(i.indexKey(d,t.id),t.id)}async parkDead(t,e,s){await this.state.storage.put(`${g}${t.id}`,{...t,attempts:e}),await this.state.storage.delete([`${f}${t.id}`,`${r}${t.id}`]),console.warn(`@lunora/scheduler: job "${t.id}" (${t.functionPath??t.workflow??"unknown"}) parked in dead-letter ${s}`)}async loadPool(t,e){const s=await this.state.storage.get(`${y}${t}`);return s!==void 0?Array.isArray(s.inFlightIds)?{inFlight:s.inFlightIds.length,inFlightIds:[...s.inFlightIds],maxConcurrency:s.maxConcurrency}:{inFlight:Math.max(0,s.inFlight),maxConcurrency:s.maxConcurrency}:{inFlight:0,inFlightIds:[],maxConcurrency:i.normalizeConcurrency(e,1)}}async savePool(t,e){await this.state.storage.put(`${y}${t}`,e)}async requeuePooled(t){const e=Date.now()+N,s={...t,scheduledFor:e};await this.state.storage.put(`${r}${t.id}`,s),await this.state.storage.put(i.indexKey(e,t.id),t.id)}async handleComplete(t){const e=await t.json().catch(()=>{}),s=typeof e?.pool=="string"&&e.pool.length>0?e.pool:void 0,a=typeof e?.id=="string"&&e.id.length>0?e.id:void 0;if(s===void 0)return i.error(400,"INVALID_INPUT","pool is required");const n=await this.loadPool(s),o=a===void 0?i.releaseFirstSlot(n):i.releaseSlot(n,a);return await this.savePool(s,o),await this.armAlarmIfEarlier(Date.now()),i.json({inFlight:o.inFlight})}async handlePoolStatus(t){const e=t.searchParams.get("name");if(e===null||e.length===0)return i.error(400,"INVALID_INPUT","name is required");const s=await this.loadPool(e);let a=0;return await this.forEachPage(r,n=>{n.pool===e&&(a+=1)}),i.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const t=new Map;await this.forEachPage(r,o=>{o.pool!==void 0&&t.set(o.pool,(t.get(o.pool)??0)+1)});const e=[];let s=0,a=0;await this.forEachPage(y,(o,c)=>{const l=c.slice(y.length),d=Math.max(0,o.inFlight),h=t.get(l)??0;e.push({inFlight:d,maxConcurrency:o.maxConcurrency,name:l,queued:h}),s+=h,a+=d});const n={backlog:s,inFlight:a,pools:e};return i.json(n)}async persistPoolCap(t,e){const s=await this.loadPool(t,e);await this.savePool(t,{inFlight:s.inFlight,...s.inFlightIds===void 0?{}:{inFlightIds:s.inFlightIds},maxConcurrency:i.normalizeConcurrency(e,s.maxConcurrency)})}async idConflict(t){if(await this.state.storage.get(`${r}${t}`)!==void 0)return i.error(409,"DUPLICATE_SCHEDULE_ID",`a job with id "${t}" is already scheduled — cancel it first, or schedule under a different id`);if(await this.state.storage.get(`${g}${t}`)!==void 0)return i.error(409,"DUPLICATE_SCHEDULE_ID",`id "${t}" is held by a dead-letter record — retry or cancel it (POST /dead/retry, POST /dead/cancel) first, or schedule under a different id`)}async handleSchedule(t){const e=await t.json().catch(()=>{}),s=i.resolveScheduleTarget(e);if(!e||s===void 0)return i.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:a,workflow:n}=s;if(typeof e.scheduledFor!="number"||!w(e.scheduledFor))return i.error(400,"INVALID_INPUT","scheduledFor must be a positive integer epoch-millisecond number no greater than 999999999999999");if(typeof this.env.LUNORA_ORIGIN_URL!="string"||this.env.LUNORA_ORIGIN_URL.length===0)return i.error(500,"ORIGIN_NOT_CONFIGURED","LUNORA_ORIGIN_URL env binding must be set on the SchedulerDO");const o=typeof e.pool=="string"&&e.pool.length>0?e.pool:void 0,c=typeof e.instanceName=="string"&&e.instanceName.length>0?e.instanceName:void 0,l=i.normalizeRetry(e.retry),d=b(e.id),h=d===e.id?await this.idConflict(d):void 0;if(h)return h;const p={args:e.args??{},enqueuedAt:Date.now(),id:d,...a===void 0?{}:{functionPath:a},...c===void 0?{}:{instanceName:c},...o===void 0?{}:{pool:o},...l===void 0?{}:{retry:l},scheduledFor:e.scheduledFor,shardKey:e.shardKey,...n===void 0?{}:{workflow:n}};return o!==void 0&&await this.persistPoolCap(o,e.maxConcurrency),await this.state.storage.put(`${r}${d}`,p),await this.state.storage.put(i.indexKey(p.scheduledFor,d),d),await this.armAlarmIfEarlier(p.scheduledFor),await this.broadcastChange(),i.json({id:d,scheduledFor:p.scheduledFor})}async handleCancel(t){const e=await t.json().catch(()=>{});if(!e?.id)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${r}${e.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),i.json({cancelled:!0})):i.json({cancelled:!1})}async handleList(t){const{cursor:e,records:s,truncated:a}=await this.listPage(r,m,t.searchParams.get("cursor")??void 0);return i.json({cursor:e,records:s,truncated:a})}async handleDeadList(t){const{cursor:e,records:s,truncated:a}=await this.listPage(g,m,t.searchParams.get("cursor")??void 0);return i.json({cursor:e,records:s,truncated:a})}async handleDeadRetry(t){const e=await t.json().catch(()=>{});if(typeof e?.id!="string"||e.id.length===0)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${g}${e.id}`);if(s===void 0)return i.json({retried:!1});const a=Date.now(),n={...s,attempts:0,scheduledFor:a};return await this.state.storage.put(`${r}${s.id}`,n),await this.state.storage.put(i.indexKey(a,s.id),s.id),await this.state.storage.delete(`${g}${s.id}`),await this.armAlarmIfEarlier(a),await this.broadcastChange(),i.json({id:s.id,retried:!0,scheduledFor:a})}async handleDeadCancel(t){const e=await t.json().catch(()=>{});if(typeof e?.id!="string"||e.id.length===0)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.delete(`${g}${e.id}`);return i.json({removed:!!s})}async handleGet(t){const e=t.searchParams.get("id");if(e===null||e.length===0)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${r}${e}`);return i.json(s===void 0?{}:{record:s})}async removeRecord(t){await this.state.storage.delete([`${r}${t.id}`,i.indexKey(t.scheduledFor,t.id),`${f}${t.id}`])}async armAlarmIfEarlier(t){const e=await this.state.storage.getAlarm();(e===null||t<e)&&await this.state.storage.setAlarm(t)}async reindexOrphanedRecords(){if(this.reindexed)return;this.reindexed=!0;const t=new Set;await this.forEachPage("t:",s=>{t.add(s)});const e=[];await this.forEachPage(r,s=>{!t.has(s.id)&&w(s.scheduledFor)&&e.push(s)});for(const s of e)await this.state.storage.put(i.indexKey(s.scheduledFor,s.id),s.id),await this.armAlarmIfEarlier(s.scheduledFor)}async rescheduleAlarm(){const e=(await this.state.storage.list({limit:1,prefix:"t:"})).entries().next();if(e.done){await this.state.storage.deleteAlarm();return}const[s]=e.value,a=Number.parseInt(s.slice(2,s.indexOf(":",2)),10);Number.isFinite(a)&&await this.state.storage.setAlarm(a)}}export{R as MAX_RETRY_ATTEMPTS,A as RETRY_BASE_DELAY_MS,i as SchedulerDO};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";const o=(e,r,t="delayMs")=>{if(!Number.isFinite(e)||e<0)throw new n("INVALID_INPUT",`${r}: \`${t}\` must be a non-negative finite number`)};export{o as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import r from"./assertScheduleDelay-BgA4K1WB.mjs";const l=(t,s,a)=>{const e=t-s;r(Number.isFinite(e)&&e<0?0:e,a,"date")};export{l as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as i}from"@lunora/errors";const O=(t,e,n)=>{if(e===void 0)return{dispose:()=>{},signal:t};const o=new AbortController,a=setTimeout(()=>{o.abort(n())},e);return{dispose:()=>{clearTimeout(a)},signal:o.signal}},c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",A=t=>{let e="",n=0;const o=t.length-2;for(;n<o;n+=3){const r=t[n]<<16|t[n+1]<<8|t[n+2];e+=c.charAt(r>>18&63)+c.charAt(r>>12&63)+c.charAt(r>>6&63)+c.charAt(r&63)}const a=t.length-n;if(a===1){const r=t[n]<<16;e+=c.charAt(r>>18&63)+c.charAt(r>>12&63)}else if(a===2){const r=t[n]<<16|t[n+1]<<8;e+=c.charAt(r>>18&63)+c.charAt(r>>12&63)+c.charAt(r>>6&63)}return e};new TextDecoder;const _=new TextEncoder,m="=",b=t=>{for(let e=0;e<t.length;e+=1)if(t.charCodeAt(e)>255)return!1;return!0},E=t=>A(_.encode(JSON.stringify(t))),L=t=>!t.startsWith(m)&&b(t)?t:`${m}${A(_.encode(t))}`,$="/_lunora/scheduler/dispatch",S=3e4,v=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},U=Symbol("lunoraDispatchFailure"),M=Symbol("lunoraDispatchMessageId"),I=(t,e)=>(e!==void 0&&Object.defineProperty(t,M,{value:e}),t),D=(t,e)=>(Object.defineProperty(t,U,{value:!0}),I(t,e)),K=(t,e,n,o)=>{try{const a=JSON.parse(n)?.error;if(typeof a=="object"&&a!==null&&typeof a.code=="string"){const{code:r,data:s,message:u}=a;return D(new i(r,typeof u=="string"?u:void 0,{data:s,status:e}),o)}}catch{}return I(new i("INTERNAL",`${t}: function dispatch failed (${String(e)}): ${n}`,{status:e}),o)},P=(t,e,n)=>new i("INTERNAL",`${t}: function dispatch to "${e}" timed out after ${String(n)}ms`,{status:503}),q=t=>{const{label:e}=t,n=globalThis.fetch,o=t.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(a,r,s={})=>{if(typeof o!="function")throw new TypeError(`${e}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const u=t.env.LUNORA_ORIGIN_URL;if(typeof u!="string"||u.length===0)throw new i("INTERNAL",`${e}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const f=t.env.LUNORA_ADMIN_TOKEN;if(typeof f!="string"||f.length===0)throw new i("INTERNAL",`${e}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const R=`${v(u)}${$}`,y={authorization:`Bearer ${f}`,"content-type":"application/json"};t.identity?.userId!==void 0&&(y["x-lunora-userid"]=L(t.identity.userId)),t.identity?.claims!==void 0&&(y["x-lunora-identity"]=E(t.identity.claims));const p=s.timeoutMs??S,N=O(void 0,p,()=>new DOMException(`dispatch timed out after ${String(p)}ms`,"TimeoutError")),g=d=>{throw d instanceof Error&&d.name==="TimeoutError"?P(e,a.__lunoraRef,p):d};let l;try{try{l=await o(R,{body:JSON.stringify({args:r??{},functionPath:a.__lunoraRef,id:s.dedupId,shardKey:s.shardKey}),headers:y,method:"POST",signal:N.signal})}catch(h){return g(h)}if(!l.ok){let h;try{h=await l.text()}catch(T){return g(T)}throw K(e,l.status,h,s.messageId)}let d;try{d=await l.text()}catch(h){return g(h)}if(d.length===0)return;try{return JSON.parse(d)}catch{throw new i("INTERNAL",`${e}: function dispatch returned a non-JSON body (${String(l.status)}): ${d}`,{status:l.status})}}finally{N.dispose()}}},w=100,x=3e5,B=t=>{if(!t.queue)throw new i("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(o,a,r={})=>{const s={args:a,functionPath:o.__lunoraRef,shardKey:r.shardKey},u=r.delaySeconds===void 0?void 0:{delaySeconds:r.delaySeconds};await t.queue.send(s,u)},enqueueBatch:async(o,a)=>{if(o.length>w)throw new i("VALIDATION_ERROR",`@lunora/scheduler: enqueueBatch exceeds ${String(w)} (got ${String(o.length)}) — split across calls`);const r=o.map(s=>({body:{args:s.args,functionPath:s.ref.__lunoraRef,shardKey:s.shardKey}}));await t.queue.sendBatch(r,a)}}},J=t=>typeof t=="object"&&t!==null&&typeof t.functionPath=="string",C=t=>async e=>{await Promise.all(e.messages.map(async n=>{try{if(!J(n.body))throw new i("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await t.dispatch(n.body,n.id),n.ack()}catch{n.retry()}}))},Q=t=>{const e=q({env:{LUNORA_ADMIN_TOKEN:t.adminToken,LUNORA_ORIGIN_URL:t.originUrl},fetchImpl:t.fetchImpl,label:"@lunora/scheduler"}),n=t.timeoutMs??x;return async(o,a)=>{await e({__lunoraRef:o.functionPath},o.args,{dedupId:a,messageId:a,shardKey:o.shardKey,timeoutMs:n})}};export{C as createQueueConsumer,B as createQueueWorkpool,Q as httpDispatcher};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as h}from"@lunora/errors";import{a as g,c as o,g as f}from"./do-client-B8HGJ5LF.mjs";import{isWorkflowReference as m}from"./isWorkflowReference-CT3tdefh.mjs";import w from"./assertScheduleDelay-BgA4K1WB.mjs";import A from"./assertScheduleInstant-BzESPqyw.mjs";const b=async n=>{const s=[];let a;for(;;){const c=await n(a);if(s.push(...Array.isArray(c.records)?c.records:[]),c.truncated!==!0||typeof c.cursor!="string"||c.cursor.length===0)return s;if(c.cursor===a)throw new Error("collectPages: the list did not advance its cursor — refusing to page forever");a=c.cursor}},E=n=>{g(n);const s=async(e,r,d,t={})=>{const i=e instanceof Date?e.getTime():e;A(i,Date.now(),"ctx.scheduler.runAt");const u={args:d,id:t.id,instanceName:n.instanceName??"default",maxConcurrency:t.pool===void 0?void 0:t.maxConcurrency,originUrl:n.originUrl,pool:t.pool,retry:t.retry,scheduledFor:i,shardKey:t.shardKey};if(m(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new h("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await o(n,"/schedule",{...u,workflow:r.binding})).id}const y=typeof r=="string"?r:r.__lunoraRef;return(await o(n,"/schedule",{...u,functionPath:y})).id},a=async(e,r,d,t={})=>(w(e,"ctx.scheduler.runAfter"),s(Date.now()+e,r,d,t)),c=async e=>o(n,"/cancel",{id:e}),l=async e=>await b(async r=>f(n,r===void 0?e:`${e}?cursor=${encodeURIComponent(r)}`));return{cancel:c,dead:async()=>l("/dead"),deadRetry:async e=>{const{retried:r}=await o(n,"/dead/retry",{id:e});return r===!0},get:async e=>(await f(n,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>l("/list"),runAfter:a,runAt:s}};export{E as default};
|
package/dist/packem_shared/{createSchedulerHost-bI3AKuoP.mjs → createSchedulerHost-BJnHgqi_.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import u from"./createScheduler-
|
|
1
|
+
import u from"./createScheduler-wS5CM4jl.mjs";const i=t=>t?.at!==void 0?typeof t.at=="number"?t.at:t.at.getTime():Date.now()+(t?.delayMs??0),m=t=>{const a=u({instanceName:t.instanceName,jurisdiction:t.jurisdiction,namespace:t.namespace,originUrl:t.originUrl}),n=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:r}=await a.cancel(e);return r},deadLetter:{list:async()=>(await a.dead()).map(r=>n(r)),requeue:async e=>a.deadRetry(e)},list:async()=>(await a.list()).map(r=>n(r)),schedule:async(e,r,c)=>{const d=a.runAt,s=i(c);return{id:await d(s,e,r,{retry:c?.retry,shardKey:c?.shardKey}),scheduledFor:s}}}};export{m as createSchedulerHost};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as l}from"@lunora/errors";import{a as s,g as m,c as u}from"./do-client-B8HGJ5LF.mjs";import o from"./assertScheduleDelay-BgA4K1WB.mjs";const C=e=>{if(s(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new l("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const r=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async a=>u(e,"/cancel",{id:a}),enqueue:async(a,t,n={})=>{const c=n.delayMs??0;return o(c,"workpool.enqueue"),u(e,"/schedule",{args:t,functionPath:a.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,originUrl:e.originUrl,pool:r,retry:n.retry,scheduledFor:Date.now()+c,shardKey:n.shardKey})},name:r,status:async()=>m(e,`/pool?name=${encodeURIComponent(r)}`)}};export{C as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as s}from"@lunora/errors";const a=(e,r)=>{if(r===void 0)return e;if(typeof e.jurisdiction!="function")throw new TypeError(`@lunora/scheduler: Durable Object namespace does not support jurisdiction("${r}") — update @cloudflare/workers-types or remove the jurisdiction option`);return e.jurisdiction(r)},d=e=>{if(!e.namespace)throw new s("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required");if(!e.originUrl)throw new s("INTERNAL","@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker")},c=e=>{const r=a(e.namespace,e.jurisdiction);return r.get(r.idFromName(e.instanceName??"default"))},u=(e,r,n)=>{let o,t;try{({code:o,message:t}=JSON.parse(n).error??{})}catch{}throw typeof o=="string"&&o.length>0?new s(o,typeof t=="string"&&t.length>0?t:`@lunora/scheduler: SchedulerDO ${e} failed`,{status:r}):new s("INTERNAL",`@lunora/scheduler: SchedulerDO ${e} failed (${String(r)}): ${n}`)},i=async(e,r,n)=>{const t=await c(e).fetch(`https://scheduler.internal${r}`,n);return t.ok||u(r,t.status,await t.text()),await t.json()},h=async(e,r,n)=>i(e,r,{body:JSON.stringify(n),headers:{"content-type":"application/json"},method:"POST"}),p=async(e,r)=>i(e,r,{method:"GET"});export{d as a,h as c,p as g};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",A=t=>{let n="",c=0;const l=t.length-2;for(;c<l;c+=3){const r=t[c]<<16|t[c+1]<<8|t[c+2];n+=a.charAt(r>>18&63)+a.charAt(r>>12&63)+a.charAt(r>>6&63)+a.charAt(r&63)}const o=t.length-c;if(o===1){const r=t[c]<<16;n+=a.charAt(r>>18&63)+a.charAt(r>>12&63)}else if(o===2){const r=t[c]<<16|t[c+1]<<8;n+=a.charAt(r>>18&63)+a.charAt(r>>12&63)+a.charAt(r>>6&63)}return n},h=/^[\w-]{1,64}$/u,i=t=>typeof t=="string"&&h.test(t)?t:A(crypto.getRandomValues(new Uint8Array(12)));export{i as r,A as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as e}from"./resolve-schedule-id-B3a6xuQf.mjs";export{e as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/scheduler",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.50",
|
|
4
4
|
"description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
50
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.30",
|
|
50
|
+
"@lunora/platform": "1.0.0-alpha.25",
|
|
51
51
|
"cron-parser": "5.8.1"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const u="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",I=c=>{let e="",t=0;const s=c.length-2;for(;t<s;t+=3){const i=c[t]<<16|c[t+1]<<8|c[t+2];e+=u.charAt(i>>18&63)+u.charAt(i>>12&63)+u.charAt(i>>6&63)+u.charAt(i&63)}const a=c.length-t;if(a===1){const i=c[t]<<16;e+=u.charAt(i>>18&63)+u.charAt(i>>12&63)}else if(a===2){const i=c[t]<<16|c[t+1]<<8;e+=u.charAt(i>>18&63)+u.charAt(i>>12&63)+u.charAt(i>>6&63)}return e},y=(c,e=200,t)=>Response.json(c,{headers:{"content-type":"application/json",...t},status:e}),R="lunora-ping",A="lunora-pong";const m="retry:",f="dead:",p="pool:";const P=5,v=3e4;const w=c=>String(c).padStart(15,"0"),E=c=>Number.isInteger(c)&&c>0&&c<=999999999999999,F=()=>I(crypto.getRandomValues(new Uint8Array(12)));class n{static indexKey(e,t){return`t:${w(e)}:${t}`}static json(e,t=200){return y(e,t)}static error(e,t,s){return n.json({error:{code:t,message:s}},e)}static resolveRetry(e){const t=e.retry,s=typeof t?.maxAttempts=="number"&&Number.isInteger(t.maxAttempts)&&t.maxAttempts>0?t.maxAttempts:5,a=typeof t?.baseMs=="number"&&Number.isFinite(t.baseMs)&&t.baseMs>=0?t.baseMs:3e4,i=t?.backoff==="linear"?"linear":"exponential",o=typeof t?.maxMs=="number"&&Number.isFinite(t.maxMs)&&t.maxMs>=0?t.maxMs:void 0;return{backoff:i,baseMs:a,maxAttempts:s,maxMs:o}}static normalizeConcurrency(e,t){return typeof e=="number"&&Number.isInteger(e)&&e>0?e:t}static normalizeRetry(e){if(typeof e!="object"||e===null)return;const t=e,s={};return typeof t.maxAttempts=="number"&&Number.isInteger(t.maxAttempts)&&t.maxAttempts>0&&(s.maxAttempts=t.maxAttempts),typeof t.baseMs=="number"&&Number.isFinite(t.baseMs)&&t.baseMs>=0&&(s.baseMs=t.baseMs),(t.backoff==="exponential"||t.backoff==="linear")&&(s.backoff=t.backoff),typeof t.maxMs=="number"&&Number.isFinite(t.maxMs)&&t.maxMs>=0&&(s.maxMs=t.maxMs),Object.keys(s).length===0?void 0:s}static releaseSlot(e,t){if(e.inFlightIds===void 0)return{...e,inFlight:Math.max(0,e.inFlight-1)};const s=e.inFlightIds.filter(a=>a!==t);return{...e,inFlight:s.length,inFlightIds:s}}static releaseFirstSlot(e){if(e.inFlightIds===void 0)return{...e,inFlight:Math.max(0,e.inFlight-1)};const t=e.inFlightIds.slice(0,Math.max(0,e.inFlightIds.length-1));return{...e,inFlight:t.length,inFlightIds:t}}static resolveScheduleTarget(e){const t=typeof e?.functionPath=="string"&&e.functionPath.length>0?e.functionPath:void 0,s=typeof e?.workflow=="string"&&e.workflow.length>0?e.workflow:void 0;if(!(t===void 0&&s===void 0))return{functionPath:t,workflow:s}}state;env;constructor(e,t){this.state=e,this.env=t,this.armWebSocketKeepalive()}async fetch(e){const t=new URL(e.url);if(t.pathname==="/ws"&&e.headers.get("Upgrade")==="websocket")return this.handleWebSocketUpgrade();switch(`${e.method} ${t.pathname}`){case"GET /dead":return this.handleDeadList(t);case"GET /get":return this.handleGet(t);case"GET /list":return this.handleList(t);case"GET /pool":return this.handlePoolStatus(t);case"GET /status":return this.handleStatus();case"POST /cancel":return this.handleCancel(e);case"POST /complete":return this.handleComplete(e);case"POST /dead/cancel":return this.handleDeadCancel(e);case"POST /dead/retry":return this.handleDeadRetry(e);case"POST /schedule":return this.handleSchedule(e)}return y({error:{code:"NOT_FOUND"}},404)}async alarm(){const e=Date.now(),t=[],s=await this.state.storage.list({end:`t:${w(e)}:~`,limit:100,prefix:"t:"});for(const[a,i]of s.entries()){const o=Number.parseInt(a.slice(2,a.indexOf(":",2)),10);if(Number.isFinite(o)&&o<=e){const r=await this.state.storage.get(`id:${i}`);r?t.push(r):await this.state.storage.delete(a)}}try{for(const a of t)await this.drainRecordGuarded(a)}finally{await this.rescheduleAlarm()}t.length>0&&await this.broadcastChange()}async dispatch(e){const t=typeof this.env.LUNORA_ORIGIN_URL=="string"&&this.env.LUNORA_ORIGIN_URL.length>0?this.env.LUNORA_ORIGIN_URL:void 0;if(!t)return!1;const s=JSON.stringify({args:e.args,functionPath:e.functionPath,id:e.id,instanceName:e.instanceName,pool:e.pool,scheduledFor:e.scheduledFor,shardKey:e.shardKey,workflow:e.workflow});try{const a={"content-type":"application/json"},i=await this.signDispatch(s);return i!==void 0?a["x-lunora-scheduler-signature"]=i:typeof this.env.LUNORA_ADMIN_TOKEN=="string"&&this.env.LUNORA_ADMIN_TOKEN.length>0&&(a.authorization=`Bearer ${this.env.LUNORA_ADMIN_TOKEN}`),(await fetch(`${t}/_lunora/scheduler/dispatch`,{body:s,headers:a,method:"POST"})).ok}catch{return!1}}armWebSocketKeepalive(){const e=this.state.setWebSocketAutoResponse;typeof e!="function"||typeof WebSocketRequestResponsePair>"u"||e.call(this.state,new WebSocketRequestResponsePair(R,A))}async drainRecordGuarded(e){try{await this.state.storage.delete(n.indexKey(e.scheduledFor,e.id)),await this.drainRecord(e)}catch{try{if(await this.state.storage.get(`${f}${e.id}`)!==void 0){await this.state.storage.delete([`${m}${e.id}`,`id:${e.id}`]);return}await this.state.storage.put(n.indexKey(e.scheduledFor,e.id),e.id)}catch{}}}async drainRecord(e){if(!await this.reservePoolSlot(e))return!1;const s=await this.dispatch(e);if(!s&&e.pool!==void 0){const a=await this.loadPool(e.pool),i=n.releaseSlot(a,e.id);await this.savePool(e.pool,i)}if(s){try{await this.state.storage.delete([`id:${e.id}`,`${m}${e.id}`])}catch{}return!0}return await this.recordRetry(e),!1}async reservePoolSlot(e){if(e.pool===void 0)return!0;const t=await this.loadPool(e.pool);if(t.inFlight>=t.maxConcurrency)return await this.requeuePooled(e),!1;const s=t.inFlightIds??[];return s.includes(e.id)||s.push(e.id),t.inFlightIds=s,t.inFlight=s.length,await this.savePool(e.pool,t),!0}async handleWebSocketUpgrade(){if(this.state.acceptWebSocket===void 0)return n.error(501,"WS_UNSUPPORTED","WebSocket subscriptions are not supported in this runtime");const e=new WebSocketPair,t=e[0],s=e[1];this.state.acceptWebSocket(s);const a=await this.listRecords();return s.send(JSON.stringify({records:a.records,truncated:a.truncated,type:"jobs"})),new Response(null,{status:101,webSocket:t})}async broadcastChange(){const e=this.state.getWebSockets?.();if(e===void 0||e.length===0)return;const{records:t,truncated:s}=await this.listRecords(),a=JSON.stringify({records:t,truncated:s,type:"jobs"});for(const i of e)try{i.send(a)}catch{}}async listPage(e,t,s){const a=await this.state.storage.list({limit:t+1,prefix:e,...s===void 0?{}:{startAfter:s}}),i=[...a.keys()],o=[...a.values()],r=o.length>t;return r?{cursor:i[t-1],records:o.slice(0,t),truncated:r}:{records:o,truncated:r}}async listRecords(e=100,t){return this.listPage("id:",e,t)}async forEachPage(e,t,s=100){let a;for(;;){const i=await this.state.storage.list(a===void 0?{limit:s,prefix:e}:{limit:s,prefix:e,startAfter:a});if(i.size===0)break;for(const[r,h]of i.entries())t(h,r);if(a=[...i.keys()].at(-1),i.size<s)break}}async signDispatch(e){const t=typeof this.env.LUNORA_SCHEDULER_SECRET=="string"?this.env.LUNORA_SCHEDULER_SECRET:void 0;if(!t||t.length===0)return;const s=new TextEncoder,a=await crypto.subtle.importKey("raw",s.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",a,s.encode(e));return I(new Uint8Array(i))}async recordRetry(e){const t=(e.attempts??0)+1,{backoff:s,baseMs:a,maxAttempts:i,maxMs:o}=n.resolveRetry(e),r=s==="linear"?a*t:a*2**(t-1),h=o===void 0?r:Math.min(r,o),l=Math.round(Date.now()+h);if(t>i){await this.parkDead(e,t,`after ${String(t)} attempts`);return}if(!E(l)){await this.parkDead(e,t,`at attempt ${String(t)}: the retry backoff exceeded the largest schedulable time`);return}const d={...e,attempts:t,scheduledFor:l};await this.state.storage.put(`${m}${e.id}`,d),await this.state.storage.put(`id:${e.id}`,d),await this.state.storage.put(n.indexKey(l,e.id),e.id)}async parkDead(e,t,s){await this.state.storage.put(`${f}${e.id}`,{...e,attempts:t}),await this.state.storage.delete([`${m}${e.id}`,`id:${e.id}`]),console.warn(`@lunora/scheduler: job "${e.id}" (${e.functionPath??e.workflow??"unknown"}) parked in dead-letter ${s}`)}async loadPool(e,t){const s=await this.state.storage.get(`${p}${e}`);return s!==void 0?Array.isArray(s.inFlightIds)?{inFlight:s.inFlightIds.length,inFlightIds:[...s.inFlightIds],maxConcurrency:s.maxConcurrency}:{inFlight:Math.max(0,s.inFlight),maxConcurrency:s.maxConcurrency}:{inFlight:0,inFlightIds:[],maxConcurrency:n.normalizeConcurrency(t,1)}}async savePool(e,t){await this.state.storage.put(`${p}${e}`,t)}async requeuePooled(e){const t=Date.now()+1e3,s={...e,scheduledFor:t};await this.state.storage.put(`id:${e.id}`,s),await this.state.storage.put(n.indexKey(t,e.id),e.id)}async handleComplete(e){const t=await e.json().catch(()=>{}),s=typeof t?.pool=="string"&&t.pool.length>0?t.pool:void 0,a=typeof t?.id=="string"&&t.id.length>0?t.id:void 0;if(s===void 0)return n.error(400,"INVALID_INPUT","pool is required");const i=await this.loadPool(s),o=a===void 0?n.releaseFirstSlot(i):n.releaseSlot(i,a);return await this.savePool(s,o),await this.armAlarmIfEarlier(Date.now()),n.json({inFlight:o.inFlight})}async handlePoolStatus(e){const t=e.searchParams.get("name");if(t===null||t.length===0)return n.error(400,"INVALID_INPUT","name is required");const s=await this.loadPool(t);let a=0;return await this.forEachPage("id:",i=>{i.pool===t&&(a+=1)}),n.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const e=new Map;await this.forEachPage("id:",o=>{const r=o;r.pool!==void 0&&e.set(r.pool,(e.get(r.pool)??0)+1)});const t=[];let s=0,a=0;await this.forEachPage(p,(o,r)=>{const h=o,l=r.slice(p.length),d=Math.max(0,h.inFlight),g=e.get(l)??0;t.push({inFlight:d,maxConcurrency:h.maxConcurrency,name:l,queued:g}),s+=g,a+=d});const i={backlog:s,inFlight:a,pools:t};return n.json(i)}async handleSchedule(e){const t=await e.json().catch(()=>{}),s=n.resolveScheduleTarget(t);if(!t||s===void 0)return n.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:a,workflow:i}=s;if(typeof t.scheduledFor!="number"||!E(t.scheduledFor))return n.error(400,"INVALID_INPUT","scheduledFor must be a positive integer epoch-millisecond number no greater than 999999999999999");if(typeof this.env.LUNORA_ORIGIN_URL!="string"||this.env.LUNORA_ORIGIN_URL.length===0)return n.error(500,"ORIGIN_NOT_CONFIGURED","LUNORA_ORIGIN_URL env binding must be set on the SchedulerDO");const o=typeof t.pool=="string"&&t.pool.length>0?t.pool:void 0,r=typeof t.instanceName=="string"&&t.instanceName.length>0?t.instanceName:void 0,h=n.normalizeRetry(t.retry),l=F(),d={args:t.args??{},enqueuedAt:Date.now(),id:l,...a===void 0?{}:{functionPath:a},...r===void 0?{}:{instanceName:r},...o===void 0?{}:{pool:o},...h===void 0?{}:{retry:h},scheduledFor:t.scheduledFor,shardKey:t.shardKey,...i===void 0?{}:{workflow:i}};if(o!==void 0){const g=await this.loadPool(o,t.maxConcurrency);await this.savePool(o,{inFlight:g.inFlight,...g.inFlightIds===void 0?{}:{inFlightIds:g.inFlightIds},maxConcurrency:n.normalizeConcurrency(t.maxConcurrency,g.maxConcurrency)})}return await this.state.storage.put(`id:${l}`,d),await this.state.storage.put(n.indexKey(d.scheduledFor,l),l),await this.armAlarmIfEarlier(d.scheduledFor),await this.broadcastChange(),n.json({id:l,scheduledFor:d.scheduledFor})}async handleCancel(e){const t=await e.json().catch(()=>{});if(!t?.id)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${t.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),n.json({cancelled:!0})):n.json({cancelled:!1})}async handleList(e){const{cursor:t,records:s,truncated:a}=await this.listRecords(100,e.searchParams.get("cursor")??void 0);return n.json({cursor:t,records:s,truncated:a})}async handleDeadList(e){const{cursor:t,records:s,truncated:a}=await this.listPage(f,100,e.searchParams.get("cursor")??void 0);return n.json({cursor:t,records:s,truncated:a})}async handleDeadRetry(e){const t=await e.json().catch(()=>{});if(typeof t?.id!="string"||t.id.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${f}${t.id}`);if(s===void 0)return n.json({retried:!1});const a=Date.now(),i={...s,attempts:0,scheduledFor:a};return await this.state.storage.put(`id:${s.id}`,i),await this.state.storage.put(n.indexKey(a,s.id),s.id),await this.state.storage.delete(`${f}${s.id}`),await this.armAlarmIfEarlier(a),await this.broadcastChange(),n.json({id:s.id,retried:!0,scheduledFor:a})}async handleDeadCancel(e){const t=await e.json().catch(()=>{});if(typeof t?.id!="string"||t.id.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.delete(`${f}${t.id}`);return n.json({removed:!!s})}async handleGet(e){const t=e.searchParams.get("id");if(t===null||t.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${t}`);return n.json(s===void 0?{}:{record:s})}async removeRecord(e){await this.state.storage.delete([`id:${e.id}`,n.indexKey(e.scheduledFor,e.id),`${m}${e.id}`])}async armAlarmIfEarlier(e){const t=await this.state.storage.getAlarm();(t===null||e<t)&&await this.state.storage.setAlarm(e)}async rescheduleAlarm(){const t=(await this.state.storage.list({limit:1,prefix:"t:"})).entries().next();if(t.done){await this.state.storage.deleteAlarm();return}const[s]=t.value,a=Number.parseInt(s.slice(2,s.indexOf(":",2)),10);Number.isFinite(a)&&await this.state.storage.setAlarm(a)}}export{P as MAX_RETRY_ATTEMPTS,v as RETRY_BASE_DELAY_MS,n as SchedulerDO};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as i}from"@lunora/errors";const O=(t,e,n)=>{if(e===void 0)return{dispose:()=>{},signal:t};const o=new AbortController,a=setTimeout(()=>{o.abort(n())},e);return{dispose:()=>{clearTimeout(a)},signal:o.signal}},c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",_=t=>{let e="",n=0;const o=t.length-2;for(;n<o;n+=3){const r=t[n]<<16|t[n+1]<<8|t[n+2];e+=c.charAt(r>>18&63)+c.charAt(r>>12&63)+c.charAt(r>>6&63)+c.charAt(r&63)}const a=t.length-n;if(a===1){const r=t[n]<<16;e+=c.charAt(r>>18&63)+c.charAt(r>>12&63)}else if(a===2){const r=t[n]<<16|t[n+1]<<8;e+=c.charAt(r>>18&63)+c.charAt(r>>12&63)+c.charAt(r>>6&63)}return e};new TextDecoder;const I=new TextEncoder,m="=",b=t=>{for(let e=0;e<t.length;e+=1)if(t.charCodeAt(e)>255)return!1;return!0},E=t=>_(I.encode(JSON.stringify(t))),L=t=>!t.startsWith(m)&&b(t)?t:`${m}${_(I.encode(t))}`,$="/_lunora/scheduler/dispatch",S=3e4,v=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},U=Symbol("lunoraDispatchFailure"),D=Symbol("lunoraDispatchMessageId"),w=(t,e)=>(Object.defineProperty(t,U,{value:!0}),e!==void 0&&Object.defineProperty(t,D,{value:e}),t),K=(t,e,n,o)=>{try{const a=JSON.parse(n)?.error;if(typeof a=="object"&&a!==null&&typeof a.code=="string"){const{code:r,data:s,message:u}=a;return w(new i(r,typeof u=="string"?u:void 0,{data:s,status:e}),o)}}catch{}return w(new i("INTERNAL",`${t}: function dispatch failed (${String(e)}): ${n}`,{status:e}),o)},M=(t,e,n)=>new i("INTERNAL",`${t}: function dispatch to "${e}" timed out after ${String(n)}ms`,{status:503}),x=t=>{const{label:e}=t,n=globalThis.fetch,o=t.fetchImpl??(typeof n=="function"?n.bind(globalThis):void 0);return async(a,r,s={})=>{if(typeof o!="function")throw new TypeError(`${e}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const u=t.env.LUNORA_ORIGIN_URL;if(typeof u!="string"||u.length===0)throw new i("INTERNAL",`${e}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const f=t.env.LUNORA_ADMIN_TOKEN;if(typeof f!="string"||f.length===0)throw new i("INTERNAL",`${e}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const R=`${v(u)}${$}`,y={authorization:`Bearer ${f}`,"content-type":"application/json"};t.identity?.userId!==void 0&&(y["x-lunora-userid"]=L(t.identity.userId)),t.identity?.claims!==void 0&&(y["x-lunora-identity"]=E(t.identity.claims));const p=s.timeoutMs??S,N=O(void 0,p,()=>new DOMException(`dispatch timed out after ${String(p)}ms`,"TimeoutError")),g=d=>{throw d instanceof Error&&d.name==="TimeoutError"?M(e,a.__lunoraRef,p):d};let l;try{try{l=await o(R,{body:JSON.stringify({args:r??{},functionPath:a.__lunoraRef,id:s.dedupId,shardKey:s.shardKey}),headers:y,method:"POST",signal:N.signal})}catch(h){return g(h)}if(!l.ok){let h;try{h=await l.text()}catch(T){return g(T)}throw K(e,l.status,h,s.messageId)}let d;try{d=await l.text()}catch(h){return g(h)}if(d.length===0)return;try{return JSON.parse(d)}catch{throw new i("INTERNAL",`${e}: function dispatch returned a non-JSON body (${String(l.status)}): ${d}`,{status:l.status})}}finally{N.dispose()}}},A=100,P=3e5,k=t=>{if(!t.queue)throw new i("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(o,a,r={})=>{const s={args:a,functionPath:o.__lunoraRef,shardKey:r.shardKey},u=r.delaySeconds===void 0?void 0:{delaySeconds:r.delaySeconds};await t.queue.send(s,u)},enqueueBatch:async(o,a)=>{if(o.length>A)throw new i("VALIDATION_ERROR",`@lunora/scheduler: enqueueBatch exceeds ${String(A)} (got ${String(o.length)}) — split across calls`);const r=o.map(s=>({body:{args:s.args,functionPath:s.ref.__lunoraRef,shardKey:s.shardKey}}));await t.queue.sendBatch(r,a)}}},q=t=>typeof t=="object"&&t!==null&&typeof t.functionPath=="string",B=t=>async e=>{await Promise.all(e.messages.map(async n=>{try{if(!q(n.body))throw new i("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await t.dispatch(n.body,n.id),n.ack()}catch{n.retry()}}))},C=t=>{const e=x({env:{LUNORA_ADMIN_TOKEN:t.adminToken,LUNORA_ORIGIN_URL:t.originUrl},fetchImpl:t.fetchImpl,label:"@lunora/scheduler"}),n=t.timeoutMs??P;return async(o,a)=>{await e({__lunoraRef:o.functionPath},o.args,{dedupId:a,messageId:a,shardKey:o.shardKey,timeoutMs:n})}};export{B as createQueueConsumer,k as createQueueWorkpool,C as httpDispatcher};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as u}from"@lunora/errors";import{a as m,c as a,g as l}from"./do-client-BKEFA9pM.mjs";import{isWorkflowReference as g}from"./isWorkflowReference-CT3tdefh.mjs";const I=n=>{m(n);const o=async(e,r,s,c={})=>{const t=e instanceof Date?e.getTime():e,i={args:s,instanceName:n.instanceName??"default",maxConcurrency:c.pool===void 0?void 0:c.maxConcurrency,originUrl:n.originUrl,pool:c.pool,retry:c.retry,scheduledFor:t,shardKey:c.shardKey};if(g(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new u("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await a(n,"/schedule",{...i,workflow:r.binding})).id}const h=typeof r=="string"?r:r.__lunoraRef;return(await a(n,"/schedule",{...i,functionPath:h})).id},y=async(e,r,s,c={})=>{if(!Number.isFinite(e)||e<0)throw new u("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return o(Date.now()+e,r,s,c)},f=async e=>a(n,"/cancel",{id:e}),d=async e=>{const r=[];let s;for(;;){const c=s===void 0?"":`?cursor=${encodeURIComponent(s)}`,t=await l(n,`${e}${c}`);if(r.push(...Array.isArray(t.records)?t.records:[]),t.truncated!==!0||typeof t.cursor!="string"||t.cursor.length===0)return r;s=t.cursor}};return{cancel:f,dead:async()=>d("/dead"),deadRetry:async e=>{const{retried:r}=await a(n,"/dead/retry",{id:e});return r===!0},get:async e=>(await l(n,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>d("/list"),runAfter:y,runAt:o}};export{I as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as u}from"@lunora/errors";import{a as s,g as m,c as t}from"./do-client-BKEFA9pM.mjs";const i=e=>{if(s(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new u("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const r=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async a=>t(e,"/cancel",{id:a}),enqueue:async(a,l,n={})=>{const c=n.delayMs??0;if(!Number.isFinite(c)||c<0)throw new u("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return t(e,"/schedule",{args:l,functionPath:a.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,originUrl:e.originUrl,pool:r,retry:n.retry,scheduledFor:Date.now()+c,shardKey:n.shardKey})},name:r,status:async()=>m(e,`/pool?name=${encodeURIComponent(r)}`)}};export{i as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as o}from"@lunora/errors";const i=(e,r)=>{if(r===void 0)return e;if(typeof e.jurisdiction!="function")throw new TypeError(`@lunora/scheduler: Durable Object namespace does not support jurisdiction("${r}") — update @cloudflare/workers-types or remove the jurisdiction option`);return e.jurisdiction(r)},l=e=>{if(!e.namespace)throw new o("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required");if(!e.originUrl)throw new o("INTERNAL","@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker")},c=e=>{const r=i(e.namespace,e.jurisdiction);return r.get(r.idFromName(e.instanceName??"default"))},s=async(e,r,n)=>{const t=await c(e).fetch(`https://scheduler.internal${r}`,n);if(!t.ok){const a=await t.text();throw new o("INTERNAL",`@lunora/scheduler: SchedulerDO ${r} failed (${String(t.status)}): ${a}`)}return await t.json()},h=async(e,r,n)=>s(e,r,{body:JSON.stringify(n),headers:{"content-type":"application/json"},method:"POST"}),p=async(e,r)=>s(e,r,{method:"GET"});export{l as a,h as c,p as g};
|