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

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.
@@ -389,6 +395,13 @@ interface MessageBatchLike<Body = unknown> {
389
395
  }
390
396
  /** The wire payload Lunora puts on the queue: a function dispatch. */
391
397
  interface QueueJob {
398
+ /**
399
+ * The call's arguments in WIRE form (`shared/wire-codec`), so a `bigint`,
400
+ * `Date` or bytes survives the queue's own JSON serialisation. The producers
401
+ * encode; the shard's dispatch loop is the single decoder. A custom
402
+ * {@link QueueDispatch} must forward this untouched — decoding it here and
403
+ * letting the shard decode again flattens a `Date` to `{}`.
404
+ */
392
405
  args?: Record<string, unknown>;
393
406
  functionPath: string;
394
407
  /** Routing hint forwarded to the Worker so the call lands on the right shard. */
@@ -458,7 +471,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
458
471
  /**
459
472
  * Bounded-concurrency action queue — the Lunora equivalent of
460
473
  * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
461
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
474
+ * `instanceName` options and is built on the SAME `SchedulerDO`:
462
475
  * a workpool is just a NAMED logical pool inside that DO (concurrency counter
463
476
  * keyed by {@link WorkpoolOptions.name} under the `pool:<name>` storage key).
464
477
  * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
@@ -469,7 +482,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
469
482
  * draining them as the runtime reports completions (`POST /complete`).
470
483
  *
471
484
  * ```ts
472
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
485
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, maxConcurrency: 5 });
473
486
  * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
474
487
  * ```
475
488
  *
@@ -654,20 +667,37 @@ declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: M
654
667
  * message — including a 2xx carrying a non-empty non-JSON body, which is an
655
668
  * intermediary's page rather than a function's return value and therefore no
656
669
  * evidence the job ran. An empty 2xx is a normal success (a `void` function).
670
+ *
671
+ * `job.args` is forwarded VERBATIM: {@link encodeJobArgs} already put it in wire
672
+ * form at the producer, and the shard's dispatch loop is the single decoder.
673
+ * Encoding again here would leave the handler a tagged array.
657
674
  */
658
675
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
659
676
  /**
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.
677
+ * The id a new record is stored under: the caller's, or a freshly minted one when
678
+ * they did not name it.
679
+ *
680
+ * A caller id that is not a safe key segment is REFUSED, not replaced.
681
+ * `RunOptions.id` is not an idempotency key — an id already scheduled answers
682
+ * `409 DUPLICATE_SCHEDULE_ID` — so quietly swapping an invalid one for a random
683
+ * id made `runAt(ts, ref, args, { id: "-daily-2026-09-06" })` mint a fresh id on
684
+ * every call and run the job once per call, where naming it was the caller's way
685
+ * of saying "at most once".
686
+ *
687
+ * Minting swaps a leading `-` for `_` rather than re-rolling: 1 in 64 minted ids
688
+ * led with one and was refused by the workflow engine on dispatch. `_` is in the
689
+ * engine's leading class, the swap is a single pass, and 96 random bits stay 96
690
+ * random bits everywhere but that first character.
662
691
  *
663
692
  * 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.
693
+ * when the record is written (turning the refusal into a `400`);
694
+ * `@lunora/server`'s deferred-schedule facade applies it when the call is
695
+ * BUFFERED, because it answers the handler with the id synchronously — long
696
+ * before the DO sees the request. Restating the rule in the facade is how the two
697
+ * drift: an id the facade accepted and the DO replaced leaves the handler holding
698
+ * an id no job was ever stored under, so its later `cancel` silently misses.
670
699
  * @param requested the caller's `RunOptions.id`, if any
700
+ * @throws LunoraError `INVALID_SCHEDULE_ID` when `requested` is supplied and is not a safe key segment
671
701
  */
672
702
  declare const resolveScheduleId: (requested: unknown) => string;
673
703
  /**
@@ -717,7 +747,7 @@ interface SchedulerEnv {
717
747
  /**
718
748
  * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
719
749
  * 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.
750
+ * request body, which carries no dispatch target at all) to prevent SSRF.
721
751
  */
722
752
  LUNORA_ORIGIN_URL?: string;
723
753
  /**
@@ -849,8 +879,8 @@ declare class SchedulerDO {
849
879
  * `dead:` key for inspection — never silently deleted.
850
880
  *
851
881
  * 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
882
+ * stored record) so a schedule request can never name where the DO calls
883
+ * back — that would be SSRF. If that env var is missing at fire time (a deploy/binding
854
884
  * regression — schedule time already enforced its presence) we return
855
885
  * `false` so the record is retried rather than silently dropped.
856
886
  */
@@ -1046,6 +1076,19 @@ declare class SchedulerDO {
1046
1076
  * at schedule time would ever have surfaced the collision.
1047
1077
  */
1048
1078
  private idConflict;
1079
+ /**
1080
+ * The id the record is stored under, or the `Response` refusing it.
1081
+ *
1082
+ * A caller id that is not a safe key segment is refused with a `400` rather
1083
+ * than minted over: `RunOptions.id` is not an idempotency key, so swapping an
1084
+ * invalid one for a random id made two calls naming it schedule two jobs
1085
+ * where the second should have answered `409`.
1086
+ *
1087
+ * Only an id the CALLER chose can collide — a minted one is 96 random bits —
1088
+ * so {@link idConflict} costs two `get`s on the deferred path and nothing on
1089
+ * the ordinary one.
1090
+ */
1091
+ private resolveId;
1049
1092
  private handleSchedule;
1050
1093
  private handleCancel;
1051
1094
  /**
@@ -1129,14 +1172,14 @@ interface SchedulerHostOptions {
1129
1172
  * the worker's own jurisdiction so scheduled state co-resides with app data.
1130
1173
  */
1131
1174
  jurisdiction?: "eu" | "fedramp" | "us";
1132
- /** The `SchedulerDO` namespace binding. */
1133
- namespace: Parameters<typeof createScheduler>[0]["namespace"];
1134
1175
  /**
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.
1176
+ * The `SchedulerDO` namespace binding.
1177
+ *
1178
+ * The origin the DO dispatches back to is not configured here — it reads
1179
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, so a wrong value
1180
+ * there (or none) is what makes jobs fire into nothing.
1138
1181
  */
1139
- originUrl: string;
1182
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
1140
1183
  }
1141
1184
  /**
1142
1185
  * 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.
@@ -389,6 +395,13 @@ interface MessageBatchLike<Body = unknown> {
389
395
  }
390
396
  /** The wire payload Lunora puts on the queue: a function dispatch. */
391
397
  interface QueueJob {
398
+ /**
399
+ * The call's arguments in WIRE form (`shared/wire-codec`), so a `bigint`,
400
+ * `Date` or bytes survives the queue's own JSON serialisation. The producers
401
+ * encode; the shard's dispatch loop is the single decoder. A custom
402
+ * {@link QueueDispatch} must forward this untouched — decoding it here and
403
+ * letting the shard decode again flattens a `Date` to `{}`.
404
+ */
392
405
  args?: Record<string, unknown>;
393
406
  functionPath: string;
394
407
  /** Routing hint forwarded to the Worker so the call lands on the right shard. */
@@ -458,7 +471,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
458
471
  /**
459
472
  * Bounded-concurrency action queue — the Lunora equivalent of
460
473
  * `@convex-dev/workpool`. Mirrors `createScheduler`'s `namespace` /
461
- * `originUrl` / `instanceName` options and is built on the SAME `SchedulerDO`:
474
+ * `instanceName` options and is built on the SAME `SchedulerDO`:
462
475
  * a workpool is just a NAMED logical pool inside that DO (concurrency counter
463
476
  * keyed by {@link WorkpoolOptions.name} under the `pool:<name>` storage key).
464
477
  * It needs no extra Durable Object or wrangler binding beyond the SchedulerDO
@@ -469,7 +482,7 @@ declare const createScheduler: (options: LunoraSchedulerOptions) => Scheduler;
469
482
  * draining them as the runtime reports completions (`POST /complete`).
470
483
  *
471
484
  * ```ts
472
- * const pool = createWorkpool({ namespace: env.SCHEDULER, originUrl, maxConcurrency: 5 });
485
+ * const pool = createWorkpool({ namespace: env.SCHEDULER, maxConcurrency: 5 });
473
486
  * await pool.enqueue(internal.stripe.sync, { invoiceId }, { retry: { maxAttempts: 3 } });
474
487
  * ```
475
488
  *
@@ -654,20 +667,37 @@ declare const createQueueConsumer: (options: QueueConsumerOptions) => ((batch: M
654
667
  * message — including a 2xx carrying a non-empty non-JSON body, which is an
655
668
  * intermediary's page rather than a function's return value and therefore no
656
669
  * evidence the job ran. An empty 2xx is a normal success (a `void` function).
670
+ *
671
+ * `job.args` is forwarded VERBATIM: {@link encodeJobArgs} already put it in wire
672
+ * form at the producer, and the shard's dispatch loop is the single decoder.
673
+ * Encoding again here would leave the handler a tagged array.
657
674
  */
658
675
  declare const httpDispatcher: (options: HttpDispatcherOptions) => QueueDispatch;
659
676
  /**
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.
677
+ * The id a new record is stored under: the caller's, or a freshly minted one when
678
+ * they did not name it.
679
+ *
680
+ * A caller id that is not a safe key segment is REFUSED, not replaced.
681
+ * `RunOptions.id` is not an idempotency key — an id already scheduled answers
682
+ * `409 DUPLICATE_SCHEDULE_ID` — so quietly swapping an invalid one for a random
683
+ * id made `runAt(ts, ref, args, { id: "-daily-2026-09-06" })` mint a fresh id on
684
+ * every call and run the job once per call, where naming it was the caller's way
685
+ * of saying "at most once".
686
+ *
687
+ * Minting swaps a leading `-` for `_` rather than re-rolling: 1 in 64 minted ids
688
+ * led with one and was refused by the workflow engine on dispatch. `_` is in the
689
+ * engine's leading class, the swap is a single pass, and 96 random bits stay 96
690
+ * random bits everywhere but that first character.
662
691
  *
663
692
  * 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.
693
+ * when the record is written (turning the refusal into a `400`);
694
+ * `@lunora/server`'s deferred-schedule facade applies it when the call is
695
+ * BUFFERED, because it answers the handler with the id synchronously — long
696
+ * before the DO sees the request. Restating the rule in the facade is how the two
697
+ * drift: an id the facade accepted and the DO replaced leaves the handler holding
698
+ * an id no job was ever stored under, so its later `cancel` silently misses.
670
699
  * @param requested the caller's `RunOptions.id`, if any
700
+ * @throws LunoraError `INVALID_SCHEDULE_ID` when `requested` is supplied and is not a safe key segment
671
701
  */
672
702
  declare const resolveScheduleId: (requested: unknown) => string;
673
703
  /**
@@ -717,7 +747,7 @@ interface SchedulerEnv {
717
747
  /**
718
748
  * Base URL where the Worker is mounted. SchedulerDO uses this at dispatch
719
749
  * 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.
750
+ * request body, which carries no dispatch target at all) to prevent SSRF.
721
751
  */
722
752
  LUNORA_ORIGIN_URL?: string;
723
753
  /**
@@ -849,8 +879,8 @@ declare class SchedulerDO {
849
879
  * `dead:` key for inspection — never silently deleted.
850
880
  *
851
881
  * 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
882
+ * stored record) so a schedule request can never name where the DO calls
883
+ * back — that would be SSRF. If that env var is missing at fire time (a deploy/binding
854
884
  * regression — schedule time already enforced its presence) we return
855
885
  * `false` so the record is retried rather than silently dropped.
856
886
  */
@@ -1046,6 +1076,19 @@ declare class SchedulerDO {
1046
1076
  * at schedule time would ever have surfaced the collision.
1047
1077
  */
1048
1078
  private idConflict;
1079
+ /**
1080
+ * The id the record is stored under, or the `Response` refusing it.
1081
+ *
1082
+ * A caller id that is not a safe key segment is refused with a `400` rather
1083
+ * than minted over: `RunOptions.id` is not an idempotency key, so swapping an
1084
+ * invalid one for a random id made two calls naming it schedule two jobs
1085
+ * where the second should have answered `409`.
1086
+ *
1087
+ * Only an id the CALLER chose can collide — a minted one is 96 random bits —
1088
+ * so {@link idConflict} costs two `get`s on the deferred path and nothing on
1089
+ * the ordinary one.
1090
+ */
1091
+ private resolveId;
1049
1092
  private handleSchedule;
1050
1093
  private handleCancel;
1051
1094
  /**
@@ -1129,14 +1172,14 @@ interface SchedulerHostOptions {
1129
1172
  * the worker's own jurisdiction so scheduled state co-resides with app data.
1130
1173
  */
1131
1174
  jurisdiction?: "eu" | "fedramp" | "us";
1132
- /** The `SchedulerDO` namespace binding. */
1133
- namespace: Parameters<typeof createScheduler>[0]["namespace"];
1134
1175
  /**
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.
1176
+ * The `SchedulerDO` namespace binding.
1177
+ *
1178
+ * The origin the DO dispatches back to is not configured here — it reads
1179
+ * `env.LUNORA_ORIGIN_URL` off its own binding at fire time, so a wrong value
1180
+ * there (or none) is what makes jobs fire into nothing.
1138
1181
  */
1139
- originUrl: string;
1182
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
1140
1183
  }
1141
1184
  /**
1142
1185
  * 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-BW6wVD57.mjs";import{default as a}from"./packem_shared/createWorkpool-CQs6z3Y0.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-Dk21VyUS.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-CO6BlgVY.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 d}from"@lunora/errors";import{a as x}from"./wire-codec-TIVAE0Ds.mjs";const M=(e,t,o)=>{if(t===void 0)return{dispose:()=>{},signal:e};const a=new AbortController,n=setTimeout(()=>{a.abort(o())},t);return{dispose:()=>{clearTimeout(n)},signal:a.signal}},A=e=>{let t="";for(let o=0;o<e.length;o+=32768)t+=String.fromCharCode(...e.subarray(o,o+32768));return btoa(t)},D=e=>{const t=atob(e),o=new Uint8Array(t.length);for(let a=0;a<t.length;a+=1)o[a]=t.codePointAt(a)??0;return o},p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",U=e=>{let t="",o=0;const a=e.length-2;for(;o<a;o+=3){const r=e[o]<<16|e[o+1]<<8|e[o+2];t+=p.charAt(r>>18&63)+p.charAt(r>>12&63)+p.charAt(r>>6&63)+p.charAt(r&63)}const n=e.length-o;if(n===1){const r=e[o]<<16;t+=p.charAt(r>>18&63)+p.charAt(r>>12&63)}else if(n===2){const r=e[o]<<16|e[o+1]<<8;t+=p.charAt(r>>18&63)+p.charAt(r>>12&63)+p.charAt(r>>6&63)}return t};new TextDecoder;const j=new TextEncoder,T="=",k=e=>{for(let t=0;t<e.length;t+=1)if(e.charCodeAt(t)>255)return!1;return!0},K=e=>U(j.encode(JSON.stringify(e))),J=e=>!e.startsWith(T)&&k(e)?e:`${T}${U(j.encode(e))}`,u="$lunora.wire$",E=64,I=1024,N="__proto__",_={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},$={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},q=e=>{if(e===null||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},b=(e,t=0)=>{if(t>E)throw new RangeError(`wire-codec: value nesting exceeds the ${E}-level limit`);if(e===void 0)return[u,"undefined"];if(e===null)return null;const o=typeof e;if(o==="bigint")return[u,"bigint",e.toString()];if(o==="number"){const r=e;return Number.isNaN(r)?[u,"nan"]:r===1/0?[u,"inf"]:r===-1/0?[u,"-inf"]:r}if(o!=="object")return e;if(e instanceof Date)return[u,"date",b(e.getTime(),t+1)];if(e instanceof Error){const r=e,s={};for(const c of Object.keys(r)){if(r[c]===void 0)continue;const l=b(r[c],t+1);c===N?Object.defineProperty(s,c,{configurable:!0,enumerable:!0,value:l,writable:!0}):s[c]=l}const i=[u,"error",String(r.name),String(r.message),s];return r.cause!==void 0&&i.push(b(r.cause,t+1)),i}if(e instanceof URL)return[u,"url",e.href];if(e instanceof Map)return[u,"map",[...e.entries()].map(([r,s])=>[b(r,t+1),b(s,t+1)])];if(e instanceof Set)return[u,"set",[...e].map(r=>b(r,t+1))];if(e instanceof ArrayBuffer)return[u,"bytes",A(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const r=e,s=r.constructor.name,i=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);return s==="Uint8Array"?[u,"bytes",A(i)]:[u,"bytes",A(i),s]}if(Array.isArray(e)){const r=e.map(s=>b(s,t+1));return r.length>0&&r[0]===u?[u,"arr",r]:r}if(!q(e)){const r=e.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${r} 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 a=e,n={};for(const r of Object.keys(a)){const s=a[r];if(s===void 0)continue;const i=b(s,t+1);r===N?Object.defineProperty(n,r,{configurable:!0,enumerable:!0,value:i,writable:!0}):n[r]=i}return n},f=(e,t=0)=>{if(t>E)throw new RangeError(`wire-codec: value nesting exceeds the ${E}-level limit`);if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===u)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(n=>f(n,t+1));case"bigint":{const n=e[2];if(typeof n!="string"||n.length>I||!/^-?\d+$/.test(n))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${I} digits)`);return BigInt(n)}case"date":{const n=f(e[2],t+1);if(typeof n!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(n)}case"map":{const n=e[2];return new Map(n.map(r=>{if(!Array.isArray(r)||r.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[f(r[0],t+1),f(r[1],t+1)]}))}case"set":return new Set(e[2].map(n=>f(n,t+1)));case"url":{const n=e[2];if(typeof n!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(n)}case"error":{const n=e[2],r=e[3];if(typeof n!="string"||typeof r!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const s=(Object.hasOwn($,n)?$[n]:void 0)??Error,i=new s(r);i.name!==n&&Object.defineProperty(i,"name",{configurable:!0,value:n,writable:!0});const c=f(e[4],t+1);if(c===null||typeof c!="object"||Array.isArray(c))throw new TypeError("wire-codec: malformed error — props must be an object");for(const l of Object.keys(c))l===N?Object.defineProperty(i,l,{configurable:!0,enumerable:!0,value:c[l],writable:!0}):i[l]=c[l];return e.length>5&&Object.defineProperty(i,"cause",{configurable:!0,value:f(e[5],t+1),writable:!0}),i}case"bytes":{const n=e[2];if(typeof n!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const r=D(n);if(A(r)!==n)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const s=e[3]??"Uint8Array";if(s==="ArrayBuffer")return r.buffer.byteLength===r.byteLength?r.buffer:r.slice().buffer;const i=Object.hasOwn(_,s)?_[s]:void 0;return i?new i(r.slice().buffer):r}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(n=>f(n,t+1))}return e.map(n=>f(n,t+1))}const o=e,a={};for(const n of Object.keys(o)){const r=f(o[n],t+1);n===N?Object.defineProperty(a,n,{configurable:!0,enumerable:!0,value:r,writable:!0}):a[n]=r}return a},C=(e,t,o)=>{try{return b(o)}catch(a){throw new TypeError(`${e}: cannot encode args for '${t}' — ${a instanceof Error?a.message:String(a)}`,a instanceof Error?{cause:a}:void 0)}},Q="/_lunora/scheduler/dispatch",W=3e4,F=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)},G=Symbol("lunoraDispatchFailure"),z=Symbol("lunoraDispatchMessageId"),P=(e,t)=>(t!==void 0&&Object.defineProperty(e,z,{value:t}),e),H=(e,t)=>(Object.defineProperty(e,G,{value:!0}),P(e,t)),V=(e,t,o,a)=>{try{const n=JSON.parse(o)?.error;if(typeof n=="object"&&n!==null&&typeof n.code=="string"){const{code:r,data:s,message:i}=n;return H(new d(r,typeof i=="string"?i:void 0,{data:s,status:t}),a)}}catch{}return P(new d("INTERNAL",`${e}: function dispatch failed (${String(t)}): ${o}`,{status:t}),a)},X=(e,t,o)=>new d("INTERNAL",`${e}: function dispatch to "${t}" timed out after ${String(o)}ms`,{status:503}),Y=e=>{const{label:t}=e,o=globalThis.fetch,a=e.fetchImpl??(typeof o=="function"?o.bind(globalThis):void 0);return async(n,r,s={})=>{if(typeof a!="function")throw new TypeError(`${t}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const i=e.env.LUNORA_ORIGIN_URL;if(typeof i!="string"||i.length===0)throw new d("INTERNAL",`${t}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const c=e.env.LUNORA_ADMIN_TOKEN;if(typeof c!="string"||c.length===0)throw new d("INTERNAL",`${t}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const l=`${F(i)}${Q}`,m={authorization:`Bearer ${c}`,"content-type":"application/json"};e.traceparent!==void 0&&e.traceparent.length>0&&(m.traceparent=e.traceparent),e.identity?.userId!==void 0&&(m["x-lunora-userid"]=J(e.identity.userId)),e.identity?.claims!==void 0&&(m["x-lunora-identity"]=K(e.identity.claims));const O=s.timeoutMs??W,R=M(void 0,O,()=>new DOMException(`dispatch timed out after ${String(O)}ms`,"TimeoutError")),v=h=>{throw h instanceof Error&&h.name==="TimeoutError"?X(t,n.__lunoraRef,O):h};let y;try{try{y=await a(l,{body:JSON.stringify({args:C(t,n.__lunoraRef,r??{}),functionPath:n.__lunoraRef,id:s.dedupId,shardKey:s.shardKey}),headers:m,method:"POST",signal:R.signal})}catch(w){return v(w)}if(!y.ok){let w;try{w=await y.text()}catch(B){return v(B)}throw V(t,y.status,w,s.messageId)}let h;try{h=await y.text()}catch(w){return v(w)}if(h.length===0)return;let g;try{g=JSON.parse(h)}catch{throw new d("INTERNAL",`${t}: function dispatch returned a non-JSON body (${String(y.status)}): ${h}`,{status:y.status})}if(g===null||typeof g!="object"||Array.isArray(g)||!("result"in g))throw new d("INTERNAL",`${t}: function dispatch returned a JSON body that is not a { result } envelope (${String(y.status)}): ${h}`,{status:y.status});return f(g.result)}finally{R.dispose()}}},S=100,Z=3e5,L=e=>e===void 0?void 0:x(e),ne=e=>{if(!e.queue)throw new d("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(a,n,r={})=>{const s={args:L(n),functionPath:a.__lunoraRef,shardKey:r.shardKey},i=r.delaySeconds===void 0?void 0:{delaySeconds:r.delaySeconds};await e.queue.send(s,i)},enqueueBatch:async(a,n)=>{if(a.length>S)throw new d("VALIDATION_ERROR",`@lunora/scheduler: enqueueBatch exceeds ${String(S)} (got ${String(a.length)}) — split across calls`);const r=a.map(s=>({body:{args:L(s.args),functionPath:s.ref.__lunoraRef,shardKey:s.shardKey}}));await e.queue.sendBatch(r,n)}}},ee=e=>typeof e=="object"&&e!==null&&typeof e.functionPath=="string",oe=e=>async t=>{await Promise.all(t.messages.map(async o=>{try{if(!ee(o.body))throw new d("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await e.dispatch(o.body,o.id),o.ack()}catch{o.retry()}}))},ae=e=>{const t=Y({env:{LUNORA_ADMIN_TOKEN:e.adminToken,LUNORA_ORIGIN_URL:e.originUrl},fetchImpl:e.fetchImpl,label:"@lunora/scheduler"}),o=e.timeoutMs??Z;return async(a,n)=>{await t({__lunoraRef:a.functionPath},a.args,{dedupId:n,messageId:n,shardKey:a.shardKey,timeoutMs:o})}};export{oe as createQueueConsumer,ne as createQueueWorkpool,ae as httpDispatcher};
@@ -0,0 +1 @@
1
+ import{LunoraError as g}from"@lunora/errors";import{e as w,d as A}from"./wire-codec-TIVAE0Ds.mjs";import{a as p,c as d,g as f}from"./do-client-XQayNStI.mjs";import{isWorkflowReference as y}from"./isWorkflowReference-CT3tdefh.mjs";import R from"./assertScheduleDelay-BgA4K1WB.mjs";import b 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:A(e.args)}),K=e=>{p(e);const o=async(n,r,s,t={})=>{const l=n instanceof Date?n.getTime():n;b(l,Date.now(),"ctx.scheduler.runAt");const i={args:w("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 g("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 m=typeof r=="string"?r:r.__lunoraRef;return(await d(e,"/schedule",{...i,functionPath:m})).id},a=async(n,r,s,t={})=>(R(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{K as default};
@@ -0,0 +1 @@
1
+ import u from"./createScheduler-BW6wVD57.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{e as l}from"./wire-codec-TIVAE0Ds.mjs";import{a as o,g as m,c as u}from"./do-client-XQayNStI.mjs";import y from"./assertScheduleDelay-BgA4K1WB.mjs";const N=e=>{if(o(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:l("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()=>m(e,`/pool?name=${encodeURIComponent(a)}`)}};export{N as default};
@@ -0,0 +1 @@
1
+ import{LunoraError as o}from"@lunora/errors";const c=(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 o("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required")},i=e=>{const r=c(e.namespace,e.jurisdiction);return r.get(r.idFromName(e.instanceName??"default"))},u=(e,r,n)=>{let s,t;try{({code:s,message:t}=JSON.parse(n).error??{})}catch{}throw typeof s=="string"&&s.length>0?new o(s,typeof t=="string"&&t.length>0?t:`@lunora/scheduler: SchedulerDO ${e} failed`,{status:r}):new o("INTERNAL",`@lunora/scheduler: SchedulerDO ${e} failed (${String(r)}): ${n}`)},a=async(e,r,n)=>{const t=await i(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)=>a(e,r,{body:JSON.stringify(n),headers:{"content-type":"application/json"},method:"POST"}),p=async(e,r)=>a(e,r,{method:"GET"});export{d as a,h as c,p 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};
@@ -0,0 +1 @@
1
+ import{a as d,f as E}from"./base64-BFBvYeZM.mjs";const o="$lunora.wire$",w=64,p=1024,g="__proto__",l={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},A={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},O=e=>{if(e===null||typeof e!="object")return!1;const n=Object.getPrototypeOf(e);return n===null||n===Object.prototype},u=(e,n=0)=>{if(n>w)throw new RangeError(`wire-codec: value nesting exceeds the ${w}-level limit`);if(e===void 0)return[o,"undefined"];if(e===null)return null;const b=typeof e;if(b==="bigint")return[o,"bigint",e.toString()];if(b==="number"){const r=e;return Number.isNaN(r)?[o,"nan"]:r===1/0?[o,"inf"]:r===-1/0?[o,"-inf"]:r}if(b!=="object")return e;if(e instanceof Date)return[o,"date",u(e.getTime(),n+1)];if(e instanceof Error){const r=e,t={};for(const i of Object.keys(r)){if(r[i]===void 0)continue;const y=u(r[i],n+1);i===g?Object.defineProperty(t,i,{configurable:!0,enumerable:!0,value:y,writable:!0}):t[i]=y}const s=[o,"error",String(r.name),String(r.message),t];return r.cause!==void 0&&s.push(u(r.cause,n+1)),s}if(e instanceof URL)return[o,"url",e.href];if(e instanceof Map)return[o,"map",[...e.entries()].map(([r,t])=>[u(r,n+1),u(t,n+1)])];if(e instanceof Set)return[o,"set",[...e].map(r=>u(r,n+1))];if(e instanceof ArrayBuffer)return[o,"bytes",d(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const r=e,t=r.constructor.name,s=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);return t==="Uint8Array"?[o,"bytes",d(s)]:[o,"bytes",d(s),t]}if(Array.isArray(e)){const r=e.map(t=>u(t,n+1));return r.length>0&&r[0]===o?[o,"arr",r]:r}if(!O(e)){const r=e.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${r} 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 c=e,a={};for(const r of Object.keys(c)){const t=c[r];if(t===void 0)continue;const s=u(t,n+1);r===g?Object.defineProperty(a,r,{configurable:!0,enumerable:!0,value:s,writable:!0}):a[r]=s}return a},f=(e,n=0)=>{if(n>w)throw new RangeError(`wire-codec: value nesting exceeds the ${w}-level limit`);if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===o)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(r=>f(r,n+1));case"bigint":{const r=e[2];if(typeof r!="string"||r.length>p||!/^-?\d+$/.test(r))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${p} digits)`);return BigInt(r)}case"date":{const r=f(e[2],n+1);if(typeof r!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(r)}case"map":{const r=e[2];return new Map(r.map(t=>{if(!Array.isArray(t)||t.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[f(t[0],n+1),f(t[1],n+1)]}))}case"set":return new Set(e[2].map(r=>f(r,n+1)));case"url":{const r=e[2];if(typeof r!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(r)}case"error":{const r=e[2],t=e[3];if(typeof r!="string"||typeof t!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const s=(Object.hasOwn(A,r)?A[r]:void 0)??Error,i=new s(t);i.name!==r&&Object.defineProperty(i,"name",{configurable:!0,value:r,writable:!0});const y=f(e[4],n+1);if(y===null||typeof y!="object"||Array.isArray(y))throw new TypeError("wire-codec: malformed error — props must be an object");for(const m of Object.keys(y))m===g?Object.defineProperty(i,m,{configurable:!0,enumerable:!0,value:y[m],writable:!0}):i[m]=y[m];return e.length>5&&Object.defineProperty(i,"cause",{configurable:!0,value:f(e[5],n+1),writable:!0}),i}case"bytes":{const r=e[2];if(typeof r!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const t=E(r);if(d(t)!==r)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const s=e[3]??"Uint8Array";if(s==="ArrayBuffer")return t.buffer.byteLength===t.byteLength?t.buffer:t.slice().buffer;const i=Object.hasOwn(l,s)?l[s]:void 0;return i?new i(t.slice().buffer):t}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(r=>f(r,n+1))}return e.map(a=>f(a,n+1))}const b=e,c={};for(const a of Object.keys(b)){const r=f(b[a],n+1);a===g?Object.defineProperty(c,a,{configurable:!0,enumerable:!0,value:r,writable:!0}):c[a]=r}return c},T=(e,n,b)=>{try{return u(b)}catch(c){throw new TypeError(`${e}: cannot encode args for '${n}' — ${c instanceof Error?c.message:String(c)}`,c instanceof Error?{cause:c}:void 0)}};export{u as a,f as d,T as e};
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.56",
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 d}from"@lunora/errors";const B=(e,t,o)=>{if(t===void 0)return{dispose:()=>{},signal:e};const a=new AbortController,n=setTimeout(()=>{a.abort(o())},t);return{dispose:()=>{clearTimeout(n)},signal:a.signal}},A=e=>{let t="";for(let o=0;o<e.length;o+=32768)t+=String.fromCharCode(...e.subarray(o,o+32768));return btoa(t)},x=e=>{const t=atob(e),o=new Uint8Array(t.length);for(let a=0;a<t.length;a+=1)o[a]=t.codePointAt(a)??0;return o},p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",L=e=>{let t="",o=0;const a=e.length-2;for(;o<a;o+=3){const r=e[o]<<16|e[o+1]<<8|e[o+2];t+=p.charAt(r>>18&63)+p.charAt(r>>12&63)+p.charAt(r>>6&63)+p.charAt(r&63)}const n=e.length-o;if(n===1){const r=e[o]<<16;t+=p.charAt(r>>18&63)+p.charAt(r>>12&63)}else if(n===2){const r=e[o]<<16|e[o+1]<<8;t+=p.charAt(r>>18&63)+p.charAt(r>>12&63)+p.charAt(r>>6&63)}return t};new TextDecoder;const U=new TextEncoder,T="=",M=e=>{for(let t=0;t<e.length;t+=1)if(e.charCodeAt(t)>255)return!1;return!0},D=e=>L(U.encode(JSON.stringify(e))),k=e=>!e.startsWith(T)&&M(e)?e:`${T}${L(U.encode(e))}`,u="$lunora.wire$",E=64,I=1024,N="__proto__",_={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},$={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},K=e=>{if(e===null||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},b=(e,t=0)=>{if(t>E)throw new RangeError(`wire-codec: value nesting exceeds the ${E}-level limit`);if(e===void 0)return[u,"undefined"];if(e===null)return null;const o=typeof e;if(o==="bigint")return[u,"bigint",e.toString()];if(o==="number"){const r=e;return Number.isNaN(r)?[u,"nan"]:r===1/0?[u,"inf"]:r===-1/0?[u,"-inf"]:r}if(o!=="object")return e;if(e instanceof Date)return[u,"date",b(e.getTime(),t+1)];if(e instanceof Error){const r=e,s={};for(const c of Object.keys(r)){if(r[c]===void 0)continue;const l=b(r[c],t+1);c===N?Object.defineProperty(s,c,{configurable:!0,enumerable:!0,value:l,writable:!0}):s[c]=l}const i=[u,"error",String(r.name),String(r.message),s];return r.cause!==void 0&&i.push(b(r.cause,t+1)),i}if(e instanceof URL)return[u,"url",e.href];if(e instanceof Map)return[u,"map",[...e.entries()].map(([r,s])=>[b(r,t+1),b(s,t+1)])];if(e instanceof Set)return[u,"set",[...e].map(r=>b(r,t+1))];if(e instanceof ArrayBuffer)return[u,"bytes",A(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const r=e,s=r.constructor.name,i=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);return s==="Uint8Array"?[u,"bytes",A(i)]:[u,"bytes",A(i),s]}if(Array.isArray(e)){const r=e.map(s=>b(s,t+1));return r.length>0&&r[0]===u?[u,"arr",r]:r}if(!K(e)){const r=e.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${r} 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 a=e,n={};for(const r of Object.keys(a)){const s=a[r];if(s===void 0)continue;const i=b(s,t+1);r===N?Object.defineProperty(n,r,{configurable:!0,enumerable:!0,value:i,writable:!0}):n[r]=i}return n},f=(e,t=0)=>{if(t>E)throw new RangeError(`wire-codec: value nesting exceeds the ${E}-level limit`);if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===u)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(n=>f(n,t+1));case"bigint":{const n=e[2];if(typeof n!="string"||n.length>I||!/^-?\d+$/.test(n))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${I} digits)`);return BigInt(n)}case"date":{const n=f(e[2],t+1);if(typeof n!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(n)}case"map":{const n=e[2];return new Map(n.map(r=>{if(!Array.isArray(r)||r.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[f(r[0],t+1),f(r[1],t+1)]}))}case"set":return new Set(e[2].map(n=>f(n,t+1)));case"url":{const n=e[2];if(typeof n!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(n)}case"error":{const n=e[2],r=e[3];if(typeof n!="string"||typeof r!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const s=(Object.hasOwn($,n)?$[n]:void 0)??Error,i=new s(r);i.name!==n&&Object.defineProperty(i,"name",{configurable:!0,value:n,writable:!0});const c=f(e[4],t+1);if(c===null||typeof c!="object"||Array.isArray(c))throw new TypeError("wire-codec: malformed error — props must be an object");for(const l of Object.keys(c))l===N?Object.defineProperty(i,l,{configurable:!0,enumerable:!0,value:c[l],writable:!0}):i[l]=c[l];return e.length>5&&Object.defineProperty(i,"cause",{configurable:!0,value:f(e[5],t+1),writable:!0}),i}case"bytes":{const n=e[2];if(typeof n!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const r=x(n);if(A(r)!==n)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const s=e[3]??"Uint8Array";if(s==="ArrayBuffer")return r.buffer.byteLength===r.byteLength?r.buffer:r.slice().buffer;const i=Object.hasOwn(_,s)?_[s]:void 0;return i?new i(r.slice().buffer):r}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(n=>f(n,t+1))}return e.map(n=>f(n,t+1))}const o=e,a={};for(const n of Object.keys(o)){const r=f(o[n],t+1);n===N?Object.defineProperty(a,n,{configurable:!0,enumerable:!0,value:r,writable:!0}):a[n]=r}return a},J=(e,t,o)=>{try{return b(o)}catch(a){throw new TypeError(`${e}: cannot encode args for '${t}' — ${a instanceof Error?a.message:String(a)}`,a instanceof Error?{cause:a}:void 0)}},q="/_lunora/scheduler/dispatch",C=3e4,Q=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)},F=Symbol("lunoraDispatchFailure"),W=Symbol("lunoraDispatchMessageId"),j=(e,t)=>(t!==void 0&&Object.defineProperty(e,W,{value:t}),e),G=(e,t)=>(Object.defineProperty(e,F,{value:!0}),j(e,t)),z=(e,t,o,a)=>{try{const n=JSON.parse(o)?.error;if(typeof n=="object"&&n!==null&&typeof n.code=="string"){const{code:r,data:s,message:i}=n;return G(new d(r,typeof i=="string"?i:void 0,{data:s,status:t}),a)}}catch{}return j(new d("INTERNAL",`${e}: function dispatch failed (${String(t)}): ${o}`,{status:t}),a)},H=(e,t,o)=>new d("INTERNAL",`${e}: function dispatch to "${t}" timed out after ${String(o)}ms`,{status:503}),V=e=>{const{label:t}=e,o=globalThis.fetch,a=e.fetchImpl??(typeof o=="function"?o.bind(globalThis):void 0);return async(n,r,s={})=>{if(typeof a!="function")throw new TypeError(`${t}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);const i=e.env.LUNORA_ORIGIN_URL;if(typeof i!="string"||i.length===0)throw new d("INTERNAL",`${t}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);const c=e.env.LUNORA_ADMIN_TOKEN;if(typeof c!="string"||c.length===0)throw new d("INTERNAL",`${t}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);const l=`${Q(i)}${q}`,m={authorization:`Bearer ${c}`,"content-type":"application/json"};e.traceparent!==void 0&&e.traceparent.length>0&&(m.traceparent=e.traceparent),e.identity?.userId!==void 0&&(m["x-lunora-userid"]=k(e.identity.userId)),e.identity?.claims!==void 0&&(m["x-lunora-identity"]=D(e.identity.claims));const O=s.timeoutMs??C,R=B(void 0,O,()=>new DOMException(`dispatch timed out after ${String(O)}ms`,"TimeoutError")),v=h=>{throw h instanceof Error&&h.name==="TimeoutError"?H(t,n.__lunoraRef,O):h};let y;try{try{y=await a(l,{body:JSON.stringify({args:J(t,n.__lunoraRef,r??{}),functionPath:n.__lunoraRef,id:s.dedupId,shardKey:s.shardKey}),headers:m,method:"POST",signal:R.signal})}catch(g){return v(g)}if(!y.ok){let g;try{g=await y.text()}catch(P){return v(P)}throw z(t,y.status,g,s.messageId)}let h;try{h=await y.text()}catch(g){return v(g)}if(h.length===0)return;let w;try{w=JSON.parse(h)}catch{throw new d("INTERNAL",`${t}: function dispatch returned a non-JSON body (${String(y.status)}): ${h}`,{status:y.status})}if(w===null||typeof w!="object"||Array.isArray(w)||!("result"in w))throw new d("INTERNAL",`${t}: function dispatch returned a JSON body that is not a { result } envelope (${String(y.status)}): ${h}`,{status:y.status});return f(w.result)}finally{R.dispose()}}},S=100,X=3e5,ee=e=>{if(!e.queue)throw new d("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(a,n,r={})=>{const s={args:n,functionPath:a.__lunoraRef,shardKey:r.shardKey},i=r.delaySeconds===void 0?void 0:{delaySeconds:r.delaySeconds};await e.queue.send(s,i)},enqueueBatch:async(a,n)=>{if(a.length>S)throw new d("VALIDATION_ERROR",`@lunora/scheduler: enqueueBatch exceeds ${String(S)} (got ${String(a.length)}) — split across calls`);const r=a.map(s=>({body:{args:s.args,functionPath:s.ref.__lunoraRef,shardKey:s.shardKey}}));await e.queue.sendBatch(r,n)}}},Y=e=>typeof e=="object"&&e!==null&&typeof e.functionPath=="string",te=e=>async t=>{await Promise.all(t.messages.map(async o=>{try{if(!Y(o.body))throw new d("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await e.dispatch(o.body,o.id),o.ack()}catch{o.retry()}}))},re=e=>{const t=V({env:{LUNORA_ADMIN_TOKEN:e.adminToken,LUNORA_ORIGIN_URL:e.originUrl},fetchImpl:e.fetchImpl,label:"@lunora/scheduler"}),o=e.timeoutMs??X;return async(a,n)=>{await t({__lunoraRef:a.functionPath},a.args,{dedupId:n,messageId:n,shardKey:a.shardKey,timeoutMs:o})}};export{te as createQueueConsumer,ee as createQueueWorkpool,re as httpDispatcher};
@@ -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{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 +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};