@lunora/scheduler 1.0.0-alpha.54 → 1.0.0-alpha.55

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/README.md CHANGED
@@ -81,7 +81,7 @@ import { createScheduler } from "@lunora/scheduler";
81
81
 
82
82
  import { api } from "@/lunora/_generated/api";
83
83
 
84
- const scheduler = createScheduler({ namespace: env.SCHEDULER, originUrl: "https://app.acme.test" });
84
+ const scheduler = createScheduler({ namespace: env.SCHEDULER });
85
85
 
86
86
  const id = await scheduler.runAfter(5 * 60_000, api.email.sendReminder, { userId: "u-1" });
87
87
  await scheduler.cancel(id);
package/dist/index.d.mts CHANGED
@@ -128,8 +128,13 @@ interface RunOptions {
128
128
  * `runAfter`/`runAt` is buffered until the transaction commits, but the
129
129
  * handler is handed the id synchronously, so the id has to be decided before
130
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.
131
+ * take the minted id from the return value. Anything that is not a plain
132
+ * `[A-Za-z0-9_-]` id of at most 64 characters, or that LEADS with `-`, is
133
+ * REFUSED (`400 INVALID_SCHEDULE_ID`) rather than replaced: the id is handed
134
+ * to `WorkflowBinding.create({ id })` verbatim for a workflow target, and the
135
+ * engine's instance-id grammar (`^[a-zA-Z0-9_][a-zA-Z0-9-_]*$`) refuses that
136
+ * first character. Minting over it would mean two calls naming the same bad
137
+ * id ran the job twice instead of the second answering `409`.
133
138
  *
134
139
  * **Not an idempotency key.** An id that is already scheduled is REFUSED
135
140
  * (`409 DUPLICATE_SCHEDULE_ID`), not replaced or de-duplicated: the time
@@ -281,13 +286,14 @@ interface LunoraSchedulerOptions {
281
286
  * un-pinned global namespace.
282
287
  */
283
288
  jurisdiction?: DurableObjectJurisdiction;
284
- /** Binding to the `SchedulerDO` durable object namespace. */
285
- namespace: DurableObjectNamespaceLike;
286
289
  /**
287
- * Origin where the Worker is mounted. SchedulerDO uses this base URL when
288
- * dispatching scheduled functions back to the Worker on alarm fire.
290
+ * Binding to the `SchedulerDO` durable object namespace.
291
+ *
292
+ * The origin the DO dispatches back to is NOT passed here: it reads
293
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, because a
294
+ * caller-supplied dispatch target would be an SSRF vector.
289
295
  */
290
- originUrl: string;
296
+ namespace: DurableObjectNamespaceLike;
291
297
  }
292
298
  /** Per-enqueue options for a {@link Workpool}. Extends {@link RunOptions} minus the implicit `pool` (the pool sets that). */
293
299
  interface EnqueueOptions {
@@ -300,7 +306,7 @@ interface EnqueueOptions {
300
306
  }
301
307
  /**
302
308
  * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
303
- * (same `namespace` / `originUrl` / `instanceName`) plus the bounded-concurrency
309
+ * (same `namespace` / `instanceName`) plus the bounded-concurrency
304
310
  * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
305
311
  * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
306
312
  * the scheduler already uses.
@@ -458,7 +464,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
458
464
  /**
459
465
  * Bounded-concurrency action queue — the Lunora equivalent of
460
466
  * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
461
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
467
+ * `instanceName` options and is built on the SAME `SchedulerDO`:
462
468
  * a workpool is just a NAMED logical pool inside that DO (concurrency counter
463
469
  * keyed by {@link WorkpoolOptions.name} under the `pool:<name>` storage key).
464
470
  * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
@@ -469,7 +475,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
469
475
  * draining them as the runtime reports completions (`POST /complete`).
470
476
  *
471
477
  * ```ts
472
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
478
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, maxConcurrency: 5 });
473
479
  * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
474
480
  * ```
475
481
  *
@@ -657,17 +663,30 @@ declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: M
657
663
  */
658
664
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
659
665
  /**
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.
666
+ * The id a new record is stored under: the caller's, or a freshly minted one when
667
+ * they did not name it.
668
+ *
669
+ * A caller id that is not a safe key segment is REFUSED, not replaced.
670
+ * `RunOptions.id` is not an idempotency key — an id already scheduled answers
671
+ * `409 DUPLICATE_SCHEDULE_ID` — so quietly swapping an invalid one for a random
672
+ * id made `runAt(ts, ref, args, { id: "-daily-2026-09-06" })` mint a fresh id on
673
+ * every call and run the job once per call, where naming it was the caller's way
674
+ * of saying "at most once".
675
+ *
676
+ * Minting swaps a leading `-` for `_` rather than re-rolling: 1 in 64 minted ids
677
+ * led with one and was refused by the workflow engine on dispatch. `_` is in the
678
+ * engine's leading class, the swap is a single pass, and 96 random bits stay 96
679
+ * random bits everywhere but that first character.
662
680
  *
663
681
  * 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.
682
+ * when the record is written (turning the refusal into a `400`);
683
+ * `@lunora/server`'s deferred-schedule facade applies it when the call is
684
+ * BUFFERED, because it answers the handler with the id synchronously — long
685
+ * before the DO sees the request. Restating the rule in the facade is how the two
686
+ * drift: an id the facade accepted and the DO replaced leaves the handler holding
687
+ * an id no job was ever stored under, so its later `cancel` silently misses.
670
688
  * @param requested the caller's `RunOptions.id`, if any
689
+ * @throws LunoraError `INVALID_SCHEDULE_ID` when `requested` is supplied and is not a safe key segment
671
690
  */
672
691
  declare const resolveScheduleId: (requested: unknown) => string;
673
692
  /**
@@ -717,7 +736,7 @@ interface SchedulerEnv {
717
736
  /**
718
737
  * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
719
738
  * time to call back into the Worker. Read at fire time (NOT taken from the
720
- * request body) to prevent SSRF via a forged `originUrl` field.
739
+ * request body, which carries no dispatch target at all) to prevent SSRF.
721
740
  */
722
741
  LUNORA_ORIGIN_URL?: string;
723
742
  /**
@@ -849,8 +868,8 @@ declare class SchedulerDO {
849
868
  * `dead:` key for inspection — never silently deleted.
850
869
  *
851
870
  * The dispatch target is taken from `env.LUNORA_ORIGIN_URL` (NOT from the
852
- * stored record) to prevent SSRF via a forged `originUrl` on the schedule
853
- * request. If that env var is missing at fire time (a deploy/binding
871
+ * stored record) so a schedule request can never name where the DO calls
872
+ * back — that would be SSRF. If that env var is missing at fire time (a deploy/binding
854
873
  * regression — schedule time already enforced its presence) we return
855
874
  * `false` so the record is retried rather than silently dropped.
856
875
  */
@@ -1046,6 +1065,19 @@ declare class SchedulerDO {
1046
1065
  * at schedule time would ever have surfaced the collision.
1047
1066
  */
1048
1067
  private idConflict;
1068
+ /**
1069
+ * The id the record is stored under, or the `Response` refusing it.
1070
+ *
1071
+ * A caller id that is not a safe key segment is refused with a `400` rather
1072
+ * than minted over: `RunOptions.id` is not an idempotency key, so swapping an
1073
+ * invalid one for a random id made two calls naming it schedule two jobs
1074
+ * where the second should have answered `409`.
1075
+ *
1076
+ * Only an id the CALLER chose can collide — a minted one is 96 random bits —
1077
+ * so {@link idConflict} costs two `get`s on the deferred path and nothing on
1078
+ * the ordinary one.
1079
+ */
1080
+ private resolveId;
1049
1081
  private handleSchedule;
1050
1082
  private handleCancel;
1051
1083
  /**
@@ -1129,14 +1161,14 @@ interface SchedulerHostOptions {
1129
1161
  * the worker's own jurisdiction so scheduled state co-resides with app data.
1130
1162
  */
1131
1163
  jurisdiction?: "eu" | "fedramp" | "us";
1132
- /** The `SchedulerDO` namespace binding. */
1133
- namespace: Parameters<typeof createScheduler>[0]["namespace"];
1134
1164
  /**
1135
- * Public origin the Worker is mounted at. `SchedulerDO` dispatches back to
1136
- * this base URL when an alarm fires, so a wrong value means jobs fire into
1137
- * nothing.
1165
+ * The `SchedulerDO` namespace binding.
1166
+ *
1167
+ * The origin the DO dispatches back to is not configured here — it reads
1168
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, so a wrong value
1169
+ * there (or none) is what makes jobs fire into nothing.
1138
1170
  */
1139
- originUrl: string;
1171
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
1140
1172
  }
1141
1173
  /**
1142
1174
  * Build the Cloudflare {@link SchedulerHost}.
package/dist/index.d.ts CHANGED
@@ -128,8 +128,13 @@ interface RunOptions {
128
128
  * `runAfter`/`runAt` is buffered until the transaction commits, but the
129
129
  * handler is handed the id synchronously, so the id has to be decided before
130
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.
131
+ * take the minted id from the return value. Anything that is not a plain
132
+ * `[A-Za-z0-9_-]` id of at most 64 characters, or that LEADS with `-`, is
133
+ * REFUSED (`400 INVALID_SCHEDULE_ID`) rather than replaced: the id is handed
134
+ * to `WorkflowBinding.create({ id })` verbatim for a workflow target, and the
135
+ * engine's instance-id grammar (`^[a-zA-Z0-9_][a-zA-Z0-9-_]*$`) refuses that
136
+ * first character. Minting over it would mean two calls naming the same bad
137
+ * id ran the job twice instead of the second answering `409`.
133
138
  *
134
139
  * **Not an idempotency key.** An id that is already scheduled is REFUSED
135
140
  * (`409 DUPLICATE_SCHEDULE_ID`), not replaced or de-duplicated: the time
@@ -281,13 +286,14 @@ interface LunoraSchedulerOptions {
281
286
  * un-pinned global namespace.
282
287
  */
283
288
  jurisdiction?: DurableObjectJurisdiction;
284
- /** Binding to the `SchedulerDO` durable object namespace. */
285
- namespace: DurableObjectNamespaceLike;
286
289
  /**
287
- * Origin where the Worker is mounted. SchedulerDO uses this base URL when
288
- * dispatching scheduled functions back to the Worker on alarm fire.
290
+ * Binding to the `SchedulerDO` durable object namespace.
291
+ *
292
+ * The origin the DO dispatches back to is NOT passed here: it reads
293
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, because a
294
+ * caller-supplied dispatch target would be an SSRF vector.
289
295
  */
290
- originUrl: string;
296
+ namespace: DurableObjectNamespaceLike;
291
297
  }
292
298
  /** Per-enqueue options for a {@link Workpool}. Extends {@link RunOptions} minus the implicit `pool` (the pool sets that). */
293
299
  interface EnqueueOptions {
@@ -300,7 +306,7 @@ interface EnqueueOptions {
300
306
  }
301
307
  /**
302
308
  * Options for `createWorkpool`. Mirrors {@link LunoraSchedulerOptions}
303
- * (same `namespace` / `originUrl` / `instanceName`) plus the bounded-concurrency
309
+ * (same `namespace` / `instanceName`) plus the bounded-concurrency
304
310
  * controls. A workpool is a NAMED logical pool inside the existing SchedulerDO —
305
311
  * it needs no extra Durable Object or wrangler binding beyond the SchedulerDO
306
312
  * the scheduler already uses.
@@ -458,7 +464,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
458
464
  /**
459
465
  * Bounded-concurrency action queue — the Lunora equivalent of
460
466
  * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
461
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
467
+ * `instanceName` options and is built on the SAME `SchedulerDO`:
462
468
  * a workpool is just a NAMED logical pool inside that DO (concurrency counter
463
469
  * keyed by {@link WorkpoolOptions.name} under the `pool:<name>` storage key).
464
470
  * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
@@ -469,7 +475,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
469
475
  * draining them as the runtime reports completions (`POST /complete`).
470
476
  *
471
477
  * ```ts
472
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
478
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, maxConcurrency: 5 });
473
479
  * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
474
480
  * ```
475
481
  *
@@ -657,17 +663,30 @@ declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: M
657
663
  */
658
664
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
659
665
  /**
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.
666
+ * The id a new record is stored under: the caller's, or a freshly minted one when
667
+ * they did not name it.
668
+ *
669
+ * A caller id that is not a safe key segment is REFUSED, not replaced.
670
+ * `RunOptions.id` is not an idempotency key — an id already scheduled answers
671
+ * `409 DUPLICATE_SCHEDULE_ID` — so quietly swapping an invalid one for a random
672
+ * id made `runAt(ts, ref, args, { id: "-daily-2026-09-06" })` mint a fresh id on
673
+ * every call and run the job once per call, where naming it was the caller's way
674
+ * of saying "at most once".
675
+ *
676
+ * Minting swaps a leading `-` for `_` rather than re-rolling: 1 in 64 minted ids
677
+ * led with one and was refused by the workflow engine on dispatch. `_` is in the
678
+ * engine's leading class, the swap is a single pass, and 96 random bits stay 96
679
+ * random bits everywhere but that first character.
662
680
  *
663
681
  * 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.
682
+ * when the record is written (turning the refusal into a `400`);
683
+ * `@lunora/server`'s deferred-schedule facade applies it when the call is
684
+ * BUFFERED, because it answers the handler with the id synchronously — long
685
+ * before the DO sees the request. Restating the rule in the facade is how the two
686
+ * drift: an id the facade accepted and the DO replaced leaves the handler holding
687
+ * an id no job was ever stored under, so its later `cancel` silently misses.
670
688
  * @param requested the caller's `RunOptions.id`, if any
689
+ * @throws LunoraError `INVALID_SCHEDULE_ID` when `requested` is supplied and is not a safe key segment
671
690
  */
672
691
  declare const resolveScheduleId: (requested: unknown) => string;
673
692
  /**
@@ -717,7 +736,7 @@ interface SchedulerEnv {
717
736
  /**
718
737
  * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
719
738
  * time to call back into the Worker. Read at fire time (NOT taken from the
720
- * request body) to prevent SSRF via a forged `originUrl` field.
739
+ * request body, which carries no dispatch target at all) to prevent SSRF.
721
740
  */
722
741
  LUNORA_ORIGIN_URL?: string;
723
742
  /**
@@ -849,8 +868,8 @@ declare class SchedulerDO {
849
868
  * `dead:` key for inspection — never silently deleted.
850
869
  *
851
870
  * The dispatch target is taken from `env.LUNORA_ORIGIN_URL` (NOT from the
852
- * stored record) to prevent SSRF via a forged `originUrl` on the schedule
853
- * request. If that env var is missing at fire time (a deploy/binding
871
+ * stored record) so a schedule request can never name where the DO calls
872
+ * back — that would be SSRF. If that env var is missing at fire time (a deploy/binding
854
873
  * regression — schedule time already enforced its presence) we return
855
874
  * `false` so the record is retried rather than silently dropped.
856
875
  */
@@ -1046,6 +1065,19 @@ declare class SchedulerDO {
1046
1065
  * at schedule time would ever have surfaced the collision.
1047
1066
  */
1048
1067
  private idConflict;
1068
+ /**
1069
+ * The id the record is stored under, or the `Response` refusing it.
1070
+ *
1071
+ * A caller id that is not a safe key segment is refused with a `400` rather
1072
+ * than minted over: `RunOptions.id` is not an idempotency key, so swapping an
1073
+ * invalid one for a random id made two calls naming it schedule two jobs
1074
+ * where the second should have answered `409`.
1075
+ *
1076
+ * Only an id the CALLER chose can collide — a minted one is 96 random bits —
1077
+ * so {@link idConflict} costs two `get`s on the deferred path and nothing on
1078
+ * the ordinary one.
1079
+ */
1080
+ private resolveId;
1049
1081
  private handleSchedule;
1050
1082
  private handleCancel;
1051
1083
  /**
@@ -1129,14 +1161,14 @@ interface SchedulerHostOptions {
1129
1161
  * the worker's own jurisdiction so scheduled state co-resides with app data.
1130
1162
  */
1131
1163
  jurisdiction?: "eu" | "fedramp" | "us";
1132
- /** The `SchedulerDO` namespace binding. */
1133
- namespace: Parameters<typeof createScheduler>[0]["namespace"];
1134
1164
  /**
1135
- * Public origin the Worker is mounted at. `SchedulerDO` dispatches back to
1136
- * this base URL when an alarm fires, so a wrong value means jobs fire into
1137
- * nothing.
1165
+ * The `SchedulerDO` namespace binding.
1166
+ *
1167
+ * The origin the DO dispatches back to is not configured here — it reads
1168
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, so a wrong value
1169
+ * there (or none) is what makes jobs fire into nothing.
1138
1170
  */
1139
- originUrl: string;
1171
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
1140
1172
  }
1141
1173
  /**
1142
1174
  * Build the Cloudflare {@link SchedulerHost}.
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{default as o}from"./packem_shared/createScheduler-D0Z1KGvI.mjs";import{default as a}from"./packem_shared/createWorkpool-66-2C7VQ.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-BqnY49Ex.mjs";import{default as i}from"./packem_shared/resolveScheduleId-BrT1GGNR.mjs";import{MAX_RETRY_ATTEMPTS as E,RETRY_BASE_DELAY_MS as C,SchedulerDO as _}from"./packem_shared/MAX_RETRY_ATTEMPTS-D941CspE.mjs";import{createSchedulerHost as R}from"./packem_shared/createSchedulerHost-w58TU4G2.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};
1
+ import{default as o}from"./packem_shared/createScheduler-BOmi216-.mjs";import{default as a}from"./packem_shared/createWorkpool-CtJ7PF85.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-BqnY49Ex.mjs";import{default as i}from"./packem_shared/resolveScheduleId-DgysKSrN.mjs";import{MAX_RETRY_ATTEMPTS as E,RETRY_BASE_DELAY_MS as C,SchedulerDO as _}from"./packem_shared/MAX_RETRY_ATTEMPTS-CoEnaemn.mjs";import{createSchedulerHost as R}from"./packem_shared/createSchedulerHost-CYA-_uLO.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};
@@ -1 +1 @@
1
- import{t as b}from"./base64-BFBvYeZM.mjs";import P from"./resolveScheduleId-BrT1GGNR.mjs";const I=(u,t=200,e)=>{const s=new Headers({"content-type":"application/json"});for(const[a,n]of Object.entries({}))s.set(a,n);return Response.json(u,{headers:s,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 b(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=P(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};
1
+ import{t as b}from"./base64-BFBvYeZM.mjs";import P from"./resolveScheduleId-DgysKSrN.mjs";const I=(u,t=200,e)=>{const s=new Headers({"content-type":"application/json"});for(const[a,n]of Object.entries({}))s.set(a,n);return Response.json(u,{headers:s,status:t})},F="lunora-ping",x="lunora-pong",r="id:",f="retry:",g="dead:",y="pool:",m=100,R=5,A=3e4,N=1e3,E=999999999999999,$=15,v=u=>String(u).padStart($,"0"),w=u=>Number.isInteger(u)&&u>0&&u<=E;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,h]of n.entries())e(h,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 b(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),h=o===void 0?c:Math.min(c,o),l=Math.round(Date.now()+h);if(e>n){await this.parkDead(t,e,`after ${String(e)} attempts`);return}if(!w(l)){await this.parkDead(t,e,`at attempt ${String(e)}: the retry backoff exceeded the largest schedulable time`);return}const d={...t,attempts:e,scheduledFor:l};await this.state.storage.put(`${f}${t.id}`,d),await this.state.storage.put(`${r}${t.id}`,d),await this.state.storage.put(i.indexKey(l,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 h=c.slice(y.length),l=Math.max(0,o.inFlight),d=t.get(h)??0;e.push({inFlight:l,maxConcurrency:o.maxConcurrency,name:h,queued:d}),s+=d,a+=l});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 resolveId(t){let e;try{e=P(t)}catch(s){return i.error(400,"INVALID_SCHEDULE_ID",s instanceof Error?s.message:"invalid `id`")}return t===void 0?e:await this.idConflict(e)??e}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,h=i.normalizeRetry(e.retry),l=await this.resolveId(e.id);if(l instanceof Response)return l;const d=l,p={args:e.args??{},enqueuedAt:Date.now(),id:d,...a===void 0?{}:{functionPath:a},...c===void 0?{}:{instanceName:c},...o===void 0?{}:{pool:o},...h===void 0?{}:{retry:h},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 m}from"@lunora/errors";import{a as w,e as A,c as d,g as f,d as R}from"./do-client-C_KdpYgk.mjs";import{isWorkflowReference as y}from"./isWorkflowReference-CT3tdefh.mjs";import b from"./assertScheduleDelay-BgA4K1WB.mjs";import p from"./assertScheduleInstant-BzESPqyw.mjs";const v=async e=>{const o=[];let a;for(;;){const c=await e(a);if(o.push(...Array.isArray(c.records)?c.records:[]),c.truncated!==!0||typeof c.cursor!="string"||c.cursor.length===0)return o;if(c.cursor===a)throw new Error("collectPages: the list did not advance its cursor — refusing to page forever");a=c.cursor}},x=e=>typeof e=="string"?e:(y(e)?e.binding:e.__lunoraRef)??"<unknown>",h=e=>({...e,args:R(e.args)}),$=e=>{w(e);const o=async(n,r,s,t={})=>{const l=n instanceof Date?n.getTime():n;p(l,Date.now(),"ctx.scheduler.runAt");const i={args:A("ctx.scheduler.runAt",x(r),s),id:t.id,instanceName:e.instanceName??"default",maxConcurrency:t.pool===void 0?void 0:t.maxConcurrency,pool:t.pool,retry:t.retry,scheduledFor:l,shardKey:t.shardKey};if(y(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new m("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await d(e,"/schedule",{...i,workflow:r.binding})).id}const g=typeof r=="string"?r:r.__lunoraRef;return(await d(e,"/schedule",{...i,functionPath:g})).id},a=async(n,r,s,t={})=>(b(n,"ctx.scheduler.runAfter"),o(Date.now()+n,r,s,t)),c=async n=>d(e,"/cancel",{id:n}),u=async n=>(await v(async s=>f(e,s===void 0?n:`${n}?cursor=${encodeURIComponent(s)}`))).map(s=>h(s));return{cancel:c,dead:async()=>u("/dead"),deadRetry:async n=>{const{retried:r}=await d(e,"/dead/retry",{id:n});return r===!0},get:async n=>{const r=await f(e,`/get?id=${encodeURIComponent(n)}`);return r.record===void 0?null:h(r.record)},list:async()=>u("/list"),runAfter:a,runAt:o}};export{$ as default};
@@ -0,0 +1 @@
1
+ import u from"./createScheduler-BOmi216-.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 r=u({instanceName:t.instanceName,jurisdiction:t.jurisdiction,namespace:t.namespace}),n=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:a}=await r.cancel(e);return a},deadLetter:{list:async()=>(await r.dead()).map(a=>n(a)),requeue:async e=>r.deadRetry(e)},list:async()=>(await r.list()).map(a=>n(a)),schedule:async(e,a,c)=>{const d=r.runAt,s=i(c);return{id:await d(s,e,a,{retry:c?.retry,shardKey:c?.shardKey}),scheduledFor:s}}}};export{m as createSchedulerHost};
@@ -0,0 +1 @@
1
+ import{LunoraError as s}from"@lunora/errors";import{a as l,g as o,c as u,e as m}from"./do-client-C_KdpYgk.mjs";import y from"./assertScheduleDelay-BgA4K1WB.mjs";const C=e=>{if(l(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new s("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const a=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async r=>u(e,"/cancel",{id:r}),enqueue:async(r,t,n={})=>{const c=n.delayMs??0;return y(c,"workpool.enqueue"),u(e,"/schedule",{args:m("workpool.enqueue",r.__lunoraRef,t),functionPath:r.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,pool:a,retry:n.retry,scheduledFor:Date.now()+c,shardKey:n.shardKey})},name:a,status:async()=>o(e,`/pool?name=${encodeURIComponent(a)}`)}};export{C as default};
@@ -1 +1 @@
1
- import{a as m,f as O}from"./base64-BFBvYeZM.mjs";import{LunoraError as w}from"@lunora/errors";const s="$lunora.wire$",l=64,g=1024,p="__proto__",h={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},A={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},j=r=>{if(r===null||typeof r!="object")return!1;const t=Object.getPrototypeOf(r);return t===null||t===Object.prototype},d=(r,t=0)=>{if(t>l)throw new RangeError(`wire-codec: value nesting exceeds the ${l}-level limit`);if(r===void 0)return[s,"undefined"];if(r===null)return null;const c=typeof r;if(c==="bigint")return[s,"bigint",r.toString()];if(c==="number"){const e=r;return Number.isNaN(e)?[s,"nan"]:e===1/0?[s,"inf"]:e===-1/0?[s,"-inf"]:e}if(c!=="object")return r;if(r instanceof Date)return[s,"date",d(r.getTime(),t+1)];if(r instanceof Error){const e=r,n={};for(const a of Object.keys(e)){if(e[a]===void 0)continue;const y=d(e[a],t+1);a===p?Object.defineProperty(n,a,{configurable:!0,enumerable:!0,value:y,writable:!0}):n[a]=y}const f=[s,"error",String(e.name),String(e.message),n];return e.cause!==void 0&&f.push(d(e.cause,t+1)),f}if(r instanceof URL)return[s,"url",r.href];if(r instanceof Map)return[s,"map",[...r.entries()].map(([e,n])=>[d(e,t+1),d(n,t+1)])];if(r instanceof Set)return[s,"set",[...r].map(e=>d(e,t+1))];if(r instanceof ArrayBuffer)return[s,"bytes",m(new Uint8Array(r)),"ArrayBuffer"];if(ArrayBuffer.isView(r)){const e=r,n=e.constructor.name,f=new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return n==="Uint8Array"?[s,"bytes",m(f)]:[s,"bytes",m(f),n]}if(Array.isArray(r)){const e=r.map(n=>d(n,t+1));return e.length>0&&e[0]===s?[s,"arr",e]:e}if(!j(r)){const e=r.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${e} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const i=r,o={};for(const e of Object.keys(i)){const n=i[e];if(n===void 0)continue;const f=d(n,t+1);e===p?Object.defineProperty(o,e,{configurable:!0,enumerable:!0,value:f,writable:!0}):o[e]=f}return o},u=(r,t=0)=>{if(t>l)throw new RangeError(`wire-codec: value nesting exceeds the ${l}-level limit`);if(r===null||typeof r!="object")return r;if(Array.isArray(r)){if(r[0]===s)switch(r[1]){case"-inf":return-1/0;case"arr":return r[2].map(e=>u(e,t+1));case"bigint":{const e=r[2];if(typeof e!="string"||e.length>g||!/^-?\d+$/.test(e))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${g} digits)`);return BigInt(e)}case"date":{const e=u(r[2],t+1);if(typeof e!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(e)}case"map":{const e=r[2];return new Map(e.map(n=>{if(!Array.isArray(n)||n.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[u(n[0],t+1),u(n[1],t+1)]}))}case"set":return new Set(r[2].map(e=>u(e,t+1)));case"url":{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(e)}case"error":{const e=r[2],n=r[3];if(typeof e!="string"||typeof n!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const f=(Object.hasOwn(A,e)?A[e]:void 0)??Error,a=new f(n);a.name!==e&&Object.defineProperty(a,"name",{configurable:!0,value:e,writable:!0});const y=u(r[4],t+1);if(y===null||typeof y!="object"||Array.isArray(y))throw new TypeError("wire-codec: malformed error — props must be an object");for(const b of Object.keys(y))b===p?Object.defineProperty(a,b,{configurable:!0,enumerable:!0,value:y[b],writable:!0}):a[b]=y[b];return r.length>5&&Object.defineProperty(a,"cause",{configurable:!0,value:u(r[5],t+1),writable:!0}),a}case"bytes":{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const n=O(e);if(m(n)!==e)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const f=r[3]??"Uint8Array";if(f==="ArrayBuffer")return n.buffer.byteLength===n.byteLength?n.buffer:n.slice().buffer;const a=Object.hasOwn(h,f)?h[f]:void 0;return a?new a(n.slice().buffer):n}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return r.map(e=>u(e,t+1))}return r.map(o=>u(o,t+1))}const c=r,i={};for(const o of Object.keys(c)){const e=u(c[o],t+1);o===p?Object.defineProperty(i,o,{configurable:!0,enumerable:!0,value:e,writable:!0}):i[o]=e}return i},U=(r,t,c)=>{try{return d(c)}catch(i){throw new TypeError(`${r}: cannot encode args for '${t}' — ${i instanceof Error?i.message:String(i)}`,i instanceof Error?{cause:i}:void 0)}},T=(r,t)=>{if(t===void 0)return r;if(typeof r.jurisdiction!="function")throw new TypeError(`@lunora/scheduler: Durable Object namespace does not support jurisdiction("${t}") — update @cloudflare/workers-types or remove the jurisdiction option`);return r.jurisdiction(t)},$=r=>{if(!r.namespace)throw new w("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required");if(!r.originUrl)throw new w("INTERNAL","@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker")},S=r=>{const t=T(r.namespace,r.jurisdiction);return t.get(t.idFromName(r.instanceName??"default"))},N=(r,t,c)=>{let i,o;try{({code:i,message:o}=JSON.parse(c).error??{})}catch{}throw typeof i=="string"&&i.length>0?new w(i,typeof o=="string"&&o.length>0?o:`@lunora/scheduler: SchedulerDO ${r} failed`,{status:t}):new w("INTERNAL",`@lunora/scheduler: SchedulerDO ${r} failed (${String(t)}): ${c}`)},E=async(r,t,c)=>{const o=await S(r).fetch(`https://scheduler.internal${t}`,c);return o.ok||N(t,o.status,await o.text()),await o.json()},D=async(r,t,c)=>E(r,t,{body:JSON.stringify(c),headers:{"content-type":"application/json"},method:"POST"}),k=async(r,t)=>E(r,t,{method:"GET"});export{$ as a,D as c,u as d,U as e,k as g};
1
+ import{a as m,f as O}from"./base64-BFBvYeZM.mjs";import{LunoraError as l}from"@lunora/errors";const s="$lunora.wire$",w=64,g=1024,p="__proto__",h={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},A={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},j=r=>{if(r===null||typeof r!="object")return!1;const t=Object.getPrototypeOf(r);return t===null||t===Object.prototype},d=(r,t=0)=>{if(t>w)throw new RangeError(`wire-codec: value nesting exceeds the ${w}-level limit`);if(r===void 0)return[s,"undefined"];if(r===null)return null;const c=typeof r;if(c==="bigint")return[s,"bigint",r.toString()];if(c==="number"){const e=r;return Number.isNaN(e)?[s,"nan"]:e===1/0?[s,"inf"]:e===-1/0?[s,"-inf"]:e}if(c!=="object")return r;if(r instanceof Date)return[s,"date",d(r.getTime(),t+1)];if(r instanceof Error){const e=r,n={};for(const a of Object.keys(e)){if(e[a]===void 0)continue;const y=d(e[a],t+1);a===p?Object.defineProperty(n,a,{configurable:!0,enumerable:!0,value:y,writable:!0}):n[a]=y}const f=[s,"error",String(e.name),String(e.message),n];return e.cause!==void 0&&f.push(d(e.cause,t+1)),f}if(r instanceof URL)return[s,"url",r.href];if(r instanceof Map)return[s,"map",[...r.entries()].map(([e,n])=>[d(e,t+1),d(n,t+1)])];if(r instanceof Set)return[s,"set",[...r].map(e=>d(e,t+1))];if(r instanceof ArrayBuffer)return[s,"bytes",m(new Uint8Array(r)),"ArrayBuffer"];if(ArrayBuffer.isView(r)){const e=r,n=e.constructor.name,f=new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return n==="Uint8Array"?[s,"bytes",m(f)]:[s,"bytes",m(f),n]}if(Array.isArray(r)){const e=r.map(n=>d(n,t+1));return e.length>0&&e[0]===s?[s,"arr",e]:e}if(!j(r)){const e=r.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${e} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const i=r,o={};for(const e of Object.keys(i)){const n=i[e];if(n===void 0)continue;const f=d(n,t+1);e===p?Object.defineProperty(o,e,{configurable:!0,enumerable:!0,value:f,writable:!0}):o[e]=f}return o},u=(r,t=0)=>{if(t>w)throw new RangeError(`wire-codec: value nesting exceeds the ${w}-level limit`);if(r===null||typeof r!="object")return r;if(Array.isArray(r)){if(r[0]===s)switch(r[1]){case"-inf":return-1/0;case"arr":return r[2].map(e=>u(e,t+1));case"bigint":{const e=r[2];if(typeof e!="string"||e.length>g||!/^-?\d+$/.test(e))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${g} digits)`);return BigInt(e)}case"date":{const e=u(r[2],t+1);if(typeof e!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(e)}case"map":{const e=r[2];return new Map(e.map(n=>{if(!Array.isArray(n)||n.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[u(n[0],t+1),u(n[1],t+1)]}))}case"set":return new Set(r[2].map(e=>u(e,t+1)));case"url":{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(e)}case"error":{const e=r[2],n=r[3];if(typeof e!="string"||typeof n!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const f=(Object.hasOwn(A,e)?A[e]:void 0)??Error,a=new f(n);a.name!==e&&Object.defineProperty(a,"name",{configurable:!0,value:e,writable:!0});const y=u(r[4],t+1);if(y===null||typeof y!="object"||Array.isArray(y))throw new TypeError("wire-codec: malformed error — props must be an object");for(const b of Object.keys(y))b===p?Object.defineProperty(a,b,{configurable:!0,enumerable:!0,value:y[b],writable:!0}):a[b]=y[b];return r.length>5&&Object.defineProperty(a,"cause",{configurable:!0,value:u(r[5],t+1),writable:!0}),a}case"bytes":{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const n=O(e);if(m(n)!==e)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const f=r[3]??"Uint8Array";if(f==="ArrayBuffer")return n.buffer.byteLength===n.byteLength?n.buffer:n.slice().buffer;const a=Object.hasOwn(h,f)?h[f]:void 0;return a?new a(n.slice().buffer):n}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return r.map(e=>u(e,t+1))}return r.map(o=>u(o,t+1))}const c=r,i={};for(const o of Object.keys(c)){const e=u(c[o],t+1);o===p?Object.defineProperty(i,o,{configurable:!0,enumerable:!0,value:e,writable:!0}):i[o]=e}return i},$=(r,t,c)=>{try{return d(c)}catch(i){throw new TypeError(`${r}: cannot encode args for '${t}' — ${i instanceof Error?i.message:String(i)}`,i instanceof Error?{cause:i}:void 0)}},T=(r,t)=>{if(t===void 0)return r;if(typeof r.jurisdiction!="function")throw new TypeError(`@lunora/scheduler: Durable Object namespace does not support jurisdiction("${t}") — update @cloudflare/workers-types or remove the jurisdiction option`);return r.jurisdiction(t)},D=r=>{if(!r.namespace)throw new l("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required")},S=r=>{const t=T(r.namespace,r.jurisdiction);return t.get(t.idFromName(r.instanceName??"default"))},N=(r,t,c)=>{let i,o;try{({code:i,message:o}=JSON.parse(c).error??{})}catch{}throw typeof i=="string"&&i.length>0?new l(i,typeof o=="string"&&o.length>0?o:`@lunora/scheduler: SchedulerDO ${r} failed`,{status:t}):new l("INTERNAL",`@lunora/scheduler: SchedulerDO ${r} failed (${String(t)}): ${c}`)},E=async(r,t,c)=>{const o=await S(r).fetch(`https://scheduler.internal${t}`,c);return o.ok||N(t,o.status,await o.text()),await o.json()},U=async(r,t,c)=>E(r,t,{body:JSON.stringify(c),headers:{"content-type":"application/json"},method:"POST"}),k=async(r,t)=>E(r,t,{method:"GET"});export{D as a,U as c,u as d,$ as e,k as g};
@@ -0,0 +1 @@
1
+ import{LunoraError as o}from"@lunora/errors";import{t as r}from"./base64-BFBvYeZM.mjs";const a=/^\w[\w-]{0,63}$/u,n=/^-/u,i=t=>{if(t===void 0)return r(crypto.getRandomValues(new Uint8Array(12))).replace(n,"_");if(typeof t!="string"||!a.test(t))throw new o("INVALID_SCHEDULE_ID","@lunora/scheduler: `id` must be 1-64 characters of [A-Za-z0-9_-] and must not start with `-`; omit it to have one minted",{status:400});return t};export{i as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "1.0.0-alpha.54",
3
+ "version": "1.0.0-alpha.55",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.33",
49
+ "@lunora/errors": "1.0.0-alpha.34",
50
50
  "@lunora/platform": "1.0.0-alpha.27",
51
51
  "cron-parser": "5.8.1"
52
52
  },
@@ -1 +0,0 @@
1
- import{LunoraError as m}from"@lunora/errors";import{a as w,e as A,c as d,g as f,d as R}from"./do-client-dmDTFkkC.mjs";import{isWorkflowReference as y}from"./isWorkflowReference-CT3tdefh.mjs";import b from"./assertScheduleDelay-BgA4K1WB.mjs";import p from"./assertScheduleInstant-BzESPqyw.mjs";const v=async e=>{const o=[];let a;for(;;){const c=await e(a);if(o.push(...Array.isArray(c.records)?c.records:[]),c.truncated!==!0||typeof c.cursor!="string"||c.cursor.length===0)return o;if(c.cursor===a)throw new Error("collectPages: the list did not advance its cursor — refusing to page forever");a=c.cursor}},x=e=>typeof e=="string"?e:(y(e)?e.binding:e.__lunoraRef)??"<unknown>",h=e=>({...e,args:R(e.args)}),T=e=>{w(e);const o=async(n,r,s,t={})=>{const l=n instanceof Date?n.getTime():n;p(l,Date.now(),"ctx.scheduler.runAt");const i={args:A("ctx.scheduler.runAt",x(r),s),id:t.id,instanceName:e.instanceName??"default",maxConcurrency:t.pool===void 0?void 0:t.maxConcurrency,originUrl:e.originUrl,pool:t.pool,retry:t.retry,scheduledFor:l,shardKey:t.shardKey};if(y(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new m("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await d(e,"/schedule",{...i,workflow:r.binding})).id}const g=typeof r=="string"?r:r.__lunoraRef;return(await d(e,"/schedule",{...i,functionPath:g})).id},a=async(n,r,s,t={})=>(b(n,"ctx.scheduler.runAfter"),o(Date.now()+n,r,s,t)),c=async n=>d(e,"/cancel",{id:n}),u=async n=>(await v(async s=>f(e,s===void 0?n:`${n}?cursor=${encodeURIComponent(s)}`))).map(s=>h(s));return{cancel:c,dead:async()=>u("/dead"),deadRetry:async n=>{const{retried:r}=await d(e,"/dead/retry",{id:n});return r===!0},get:async n=>{const r=await f(e,`/get?id=${encodeURIComponent(n)}`);return r.record===void 0?null:h(r.record)},list:async()=>u("/list"),runAfter:a,runAt:o}};export{T as default};
@@ -1 +0,0 @@
1
- import u from"./createScheduler-D0Z1KGvI.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 +0,0 @@
1
- import{LunoraError as t}from"@lunora/errors";import{a as s,g as o,c as u,e as m}from"./do-client-dmDTFkkC.mjs";import y from"./assertScheduleDelay-BgA4K1WB.mjs";const C=e=>{if(s(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new t("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const a=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async r=>u(e,"/cancel",{id:r}),enqueue:async(r,l,n={})=>{const c=n.delayMs??0;return y(c,"workpool.enqueue"),u(e,"/schedule",{args:m("workpool.enqueue",r.__lunoraRef,l),functionPath:r.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,originUrl:e.originUrl,pool:a,retry:n.retry,scheduledFor:Date.now()+c,shardKey:n.shardKey})},name:a,status:async()=>o(e,`/pool?name=${encodeURIComponent(a)}`)}};export{C as default};
@@ -1 +0,0 @@
1
- import{t as o}from"./base64-BFBvYeZM.mjs";const a=/^[\w-]{1,64}$/u,s=t=>typeof t=="string"&&a.test(t)?t:o(crypto.getRandomValues(new Uint8Array(12)));export{s as default};