@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 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
- /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
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 `{ seconds | minutes | hours }`. The target may be a function or a durable workflow (`workflows.<name>`). */
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 listRecords}) and push them to
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
- 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, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createSchedulerHost, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference, warnIfSecondsLeading };
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
- /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
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 `{ seconds | minutes | hours }`. The target may be a function or a durable workflow (`workflows.<name>`). */
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 listRecords}) and push them to
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
- 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, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createSchedulerHost, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference, warnIfSecondsLeading };
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-DismdyOw.mjs";import{default as a}from"./packem_shared/createWorkpool-d3THj5gc.mjs";import{createCronTrigger as c}from"./packem_shared/createCronTrigger-DSuDZtqj.mjs";import{CRON_SCHEDULE_KINDS as f,compileCronSchedule as l,cronJobs as n}from"./packem_shared/CRON_SCHEDULE_KINDS-xyFk800s.mjs";import{createQueueConsumer as u,createQueueWorkpool as x,httpDispatcher as d}from"./packem_shared/createQueueConsumer-adiyW-4L.mjs";import{MAX_RETRY_ATTEMPTS as S,RETRY_BASE_DELAY_MS as E,SchedulerDO as C}from"./packem_shared/MAX_RETRY_ATTEMPTS-B2mOoWHR.mjs";import{createSchedulerHost as h}from"./packem_shared/createSchedulerHost-bI3AKuoP.mjs";import{isWorkflowReference as T}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as A,isValidCronExpression as g,warnIfSecondsLeading as k}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";export{f as CRON_SCHEDULE_KINDS,S as MAX_RETRY_ATTEMPTS,E as RETRY_BASE_DELAY_MS,C as SchedulerDO,A as assertValidCronExpression,l as compileCronSchedule,c as createCronTrigger,u as createQueueConsumer,x as createQueueWorkpool,o as createScheduler,h as createSchedulerHost,a as createWorkpool,n as cronJobs,d as httpDispatcher,g as isValidCronExpression,T as isWorkflowReference,k as warnIfSecondsLeading};
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};
@@ -1 +1 @@
1
- import u from"./createScheduler-DismdyOw.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};
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.49",
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.29",
50
- "@lunora/platform": "1.0.0-alpha.24",
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};