@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 +1 -1
- package/dist/index.d.mts +70 -27
- package/dist/index.d.ts +70 -27
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/{MAX_RETRY_ATTEMPTS-D941CspE.mjs → MAX_RETRY_ATTEMPTS-CoEnaemn.mjs} +1 -1
- package/dist/packem_shared/createQueueConsumer-Dk21VyUS.mjs +1 -0
- package/dist/packem_shared/createScheduler-BW6wVD57.mjs +1 -0
- package/dist/packem_shared/createSchedulerHost-CO6BlgVY.mjs +1 -0
- package/dist/packem_shared/createWorkpool-CQs6z3Y0.mjs +1 -0
- package/dist/packem_shared/do-client-XQayNStI.mjs +1 -0
- package/dist/packem_shared/resolveScheduleId-DgysKSrN.mjs +1 -0
- package/dist/packem_shared/wire-codec-TIVAE0Ds.mjs +1 -0
- package/package.json +2 -2
- package/dist/packem_shared/createQueueConsumer-BqnY49Ex.mjs +0 -1
- package/dist/packem_shared/createScheduler-D0Z1KGvI.mjs +0 -1
- package/dist/packem_shared/createSchedulerHost-w58TU4G2.mjs +0 -1
- package/dist/packem_shared/createWorkpool-66-2C7VQ.mjs +0 -1
- package/dist/packem_shared/do-client-dmDTFkkC.mjs +0 -1
- package/dist/packem_shared/resolveScheduleId-BrT1GGNR.mjs +0 -1
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
|
|
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.
|
|
132
|
-
*
|
|
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
|
-
*
|
|
288
|
-
*
|
|
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
|
-
|
|
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` / `
|
|
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
|
-
* `
|
|
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,
|
|
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,
|
|
661
|
-
*
|
|
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
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
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
|
|
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)
|
|
853
|
-
*
|
|
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
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
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
|
-
|
|
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.
|
|
132
|
-
*
|
|
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
|
-
*
|
|
288
|
-
*
|
|
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
|
-
|
|
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` / `
|
|
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
|
-
* `
|
|
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,
|
|
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,
|
|
661
|
-
*
|
|
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
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
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
|
|
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)
|
|
853
|
-
*
|
|
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
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
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
|
-
|
|
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-
|
|
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};
|
package/dist/packem_shared/{MAX_RETRY_ATTEMPTS-D941CspE.mjs → MAX_RETRY_ATTEMPTS-CoEnaemn.mjs}
RENAMED
|
@@ -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.
|
|
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.
|
|
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};
|