@lunora/scheduler 1.0.0-alpha.1 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -45,6 +45,15 @@ interface WorkflowReference<Params = Record<string, unknown>> {
45
45
  type CronTarget = FunctionReference | WorkflowReference;
46
46
  /** The arguments a cron's target accepts: a workflow's inferred `params`, else an open record (function args aren't inferred). */
47
47
  type CronTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : Record<string, unknown>;
48
+ /**
49
+ * The arguments a one-shot schedule target ({@link Scheduler.runAfter} /
50
+ * {@link Scheduler.runAt}) accepts. Unlike {@link CronTargetArgs} it preserves a
51
+ * {@link FunctionReference}'s inferred `args` (via {@link ArgsOf}) as well as a
52
+ * {@link WorkflowReference}'s inferred `params`, so scheduling a plain function
53
+ * keeps its today's arg checking while scheduling a workflow/agent infers its
54
+ * `params`.
55
+ */
56
+ type ScheduleTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : T extends FunctionReference ? ArgsOf<T> : Record<string, unknown>;
48
57
  /** Narrow a {@link CronTarget} to a {@link WorkflowReference} by its runtime brand. */
49
58
  declare const isWorkflowReference: (target: unknown) => target is WorkflowReference;
50
59
  /**
@@ -93,7 +102,12 @@ interface ScheduleRecord {
93
102
  */
94
103
  attempts?: number;
95
104
  enqueuedAt: number;
96
- functionPath: string;
105
+ /**
106
+ * The `ns:fn` path of the function to dispatch on fire. Absent when the job
107
+ * targets a durable workflow/agent instead — see {@link ScheduleRecord.workflow}.
108
+ * Exactly one of `functionPath` / `workflow` is set.
109
+ */
110
+ functionPath?: string;
97
111
  id: string;
98
112
  /**
99
113
  * Scheduler/workpool instance name the job was enqueued through. Echoed in
@@ -113,6 +127,14 @@ interface ScheduleRecord {
113
127
  retry?: RetryPolicy;
114
128
  scheduledFor: number;
115
129
  shardKey?: string;
130
+ /**
131
+ * The `WORKFLOW_*`/`AGENT_*` binding name to start a fresh durable instance
132
+ * of on fire (the {@link ScheduleRecord.args} become its `params`). Set
133
+ * instead of {@link ScheduleRecord.functionPath} when the job targets a
134
+ * workflow/agent {@link WorkflowReference}. The runtime — not the DO — owns
135
+ * the binding, so the dispatch payload carries this through to the Worker.
136
+ */
137
+ workflow?: string;
116
138
  }
117
139
  interface Scheduler {
118
140
  cancel: (id: string) => Promise<{
@@ -122,19 +144,39 @@ interface Scheduler {
122
144
  get: (id: string) => Promise<ScheduleRecord | null>;
123
145
  /** All pending scheduled jobs (the DO's `/list` view). */
124
146
  list: () => Promise<ScheduleRecord[]>;
125
- runAfter: <F extends FunctionReference>(delayMs: number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
147
+ /**
148
+ * Schedule `target` to run once, `delayMs` from now. `target` is a function
149
+ * {@link FunctionReference} (dispatched as a one-shot) or a durable
150
+ * {@link WorkflowReference} — the generated `workflows.&lt;name>` /
151
+ * `agents.&lt;name>` ref — which starts a fresh instance on fire (args become
152
+ * its `params`). {@link ScheduleTargetArgs} infers the accepted args from
153
+ * whichever target was passed.
154
+ */
155
+ runAfter: <T extends CronTarget>(delayMs: number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
126
156
  id: string;
127
157
  scheduledFor: number;
128
158
  }>;
129
- runAt: <F extends FunctionReference>(date: Date | number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
159
+ /** Like {@link Scheduler.runAfter} but fires at an absolute `date`/timestamp. */
160
+ runAt: <T extends CronTarget>(date: Date | number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
130
161
  id: string;
131
162
  scheduledFor: number;
132
163
  }>;
133
164
  }
165
+ /**
166
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
167
+ * Cloudflare adds values over time.
168
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
169
+ */
170
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
134
171
  /** Subset of `DurableObjectNamespace` the package consumes. */
135
172
  interface DurableObjectNamespaceLike {
136
173
  get: (id: DurableObjectIdLike) => DurableObjectStubLike;
137
174
  idFromName: (name: string) => DurableObjectIdLike;
175
+ /**
176
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
177
+ * workers-types releases (and test doubles) may not expose it.
178
+ */
179
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => DurableObjectNamespaceLike;
138
180
  }
139
181
  interface DurableObjectIdLike {
140
182
  toString: () => string;
@@ -145,6 +187,13 @@ interface DurableObjectStubLike {
145
187
  interface LunoraSchedulerOptions {
146
188
  /** Optional named instance — useful for tenant isolation. Default `default`. */
147
189
  instanceName?: string;
190
+ /**
191
+ * Pin the SchedulerDO (durable timers + cron state) to a Cloudflare
192
+ * data-residency jurisdiction. Pass the same value as the worker's
193
+ * `jurisdiction` so scheduled state co-resides with app data. Omit for the
194
+ * un-pinned global namespace.
195
+ */
196
+ jurisdiction?: DurableObjectJurisdiction;
148
197
  /** Binding to the `SchedulerDO` durable object namespace. */
149
198
  namespace: DurableObjectNamespaceLike;
150
199
  /**
@@ -599,6 +648,13 @@ declare class SchedulerDO {
599
648
  * compatibility shim, not the hot path.
600
649
  */
601
650
  private static releaseFirstSlot;
651
+ /**
652
+ * Normalize the mutually-exclusive dispatch target off an untrusted body: a
653
+ * one-shot function path (`functionPath`) or a durable workflow/agent
654
+ * instance (`workflow`, a `WORKFLOW_*`/`AGENT_*` binding). Returns `undefined`
655
+ * when neither is present so the caller can reject the schedule.
656
+ */
657
+ private static resolveScheduleTarget;
602
658
  protected readonly state: SchedulerDOState;
603
659
  protected readonly env: SchedulerEnv;
604
660
  constructor(state: SchedulerDOState, env: SchedulerEnv);
@@ -630,15 +686,17 @@ declare class SchedulerDO {
630
686
  *
631
687
  * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
632
688
  * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
633
- * thrown storage op decides whether the job stays re-fireable.
689
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
634
690
  *
635
- * When the record was NOT successfully dispatched, re-assert the time-index
636
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
637
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
638
- * before throwing, and re-inserting the same key is idempotent, so a
639
- * surviving claim is simply rewritten to its prior value. When the record WAS
640
- * dispatched (the throw came from post-dispatch cleanup), leave the index
641
- * deleted so the already-kicked, idempotent job is not re-fired.
691
+ * A throw reaching here always means the job was NOT dispatched:
692
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
693
+ * returns instead of throwing once a kick succeeds, so every escaping throw
694
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore always
695
+ * re-assert the time-index claim so a later alarm re-attempts it
696
+ * (at-least-once): the claim delete may have removed it and
697
+ * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
698
+ * re-inserting the same key is idempotent, so a surviving claim is simply
699
+ * rewritten to its prior value.
642
700
  */
643
701
  private drainRecordGuarded;
644
702
  /**
@@ -648,16 +706,18 @@ declare class SchedulerDO {
648
706
  * free slot is reserved durably before dispatch and released immediately if
649
707
  * the kick fails (success holds it until the runtime reports completion).
650
708
  * Success clears the `id:`/`retry:` rows; failure routes to
651
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
652
- * lifetime of the drain so the budget decrements without re-reading storage.
709
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
710
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
711
+ * concurrent /complete landing mid-dispatch can't be clobbered.
712
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
713
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
714
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
715
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
716
+ * is still re-fireable and the guard safely re-claims the time index.
653
717
  * @returns `true` only when the record was successfully dispatched (a 2xx
654
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
655
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
656
- * later throws) must NOT have its time-index claim restored, since re-firing
657
- * an already-kicked job would break idempotency. A `false` return (pool
658
- * backpressure or a failed dispatch) means the job is still re-fireable —
659
- * either already re-armed here, or, if a throw escapes, re-claimed by the
660
- * guard's catch.
718
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
719
+ * re-fireable — already re-armed here). The value is informational (the guard
720
+ * branches on throw/no-throw, not on this boolean).
661
721
  */
662
722
  private drainRecord;
663
723
  /**
@@ -665,6 +725,14 @@ declare class SchedulerDO {
665
725
  * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
666
726
  * otherwise reserves a slot durably and returns `true`. Non-pooled records
667
727
  * always return `true` without touching any pool state.
728
+ *
729
+ * The pool row is read FRESH from storage on every call — never cached
730
+ * across the drain. Each reservation durably `savePool()`s before the next
731
+ * record runs, so a same-pass reservation is still visible to the next
732
+ * record's fresh read (the budget carries forward); and because dispatch()
733
+ * awaits an outbound fetch between records, a concurrent /complete that
734
+ * decrements the row mid-drain IS reflected here instead of being clobbered
735
+ * by a stale in-memory copy (which would leak a slot permanently).
668
736
  */
669
737
  private reservePoolSlot;
670
738
  /**
@@ -782,4 +850,4 @@ declare const isValidCronExpression: (schedule: string) => boolean;
782
850
  * job (`cron job "send digest"`) vs. the bare trigger.
783
851
  */
784
852
  declare const assertValidCronExpression: (schedule: string, context?: string) => void;
785
- export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, type DurableObjectNamespaceLike, type DurableObjectStubLike, type EnqueueOptions, type FunctionReference, type HttpDispatcherOptions, type IntervalSchedule, type LunoraSchedulerOptions, type MessageBatchLike, type MonthlySchedule, type QueueConsumerOptions, type QueueDispatch, type QueueEnqueueOptions, type QueueJob, type QueueLike, type QueueMessageLike, type QueueSendOptionsLike, type QueueSendRequestLike, type QueueWorkpool, type QueueWorkpoolOptions, type RetryPolicy, type RunOptions, type ScheduleRecord, type Scheduler, SchedulerDO, type SchedulerDOState, type SchedulerEnv, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
853
+ export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, type DurableObjectJurisdiction, type DurableObjectNamespaceLike, type DurableObjectStubLike, type EnqueueOptions, type FunctionReference, type HttpDispatcherOptions, type IntervalSchedule, type LunoraSchedulerOptions, type MessageBatchLike, type MonthlySchedule, type QueueConsumerOptions, type QueueDispatch, type QueueEnqueueOptions, type QueueJob, type QueueLike, type QueueMessageLike, type QueueSendOptionsLike, type QueueSendRequestLike, type QueueWorkpool, type QueueWorkpoolOptions, type RetryPolicy, type RunOptions, type ScheduleRecord, type Scheduler, SchedulerDO, type SchedulerDOState, type SchedulerEnv, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
package/dist/index.d.ts CHANGED
@@ -45,6 +45,15 @@ interface WorkflowReference<Params = Record<string, unknown>> {
45
45
  type CronTarget = FunctionReference | WorkflowReference;
46
46
  /** The arguments a cron's target accepts: a workflow's inferred `params`, else an open record (function args aren't inferred). */
47
47
  type CronTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : Record<string, unknown>;
48
+ /**
49
+ * The arguments a one-shot schedule target ({@link Scheduler.runAfter} /
50
+ * {@link Scheduler.runAt}) accepts. Unlike {@link CronTargetArgs} it preserves a
51
+ * {@link FunctionReference}'s inferred `args` (via {@link ArgsOf}) as well as a
52
+ * {@link WorkflowReference}'s inferred `params`, so scheduling a plain function
53
+ * keeps its today's arg checking while scheduling a workflow/agent infers its
54
+ * `params`.
55
+ */
56
+ type ScheduleTargetArgs<T extends CronTarget> = T extends WorkflowReference<infer Params> ? Params : T extends FunctionReference ? ArgsOf<T> : Record<string, unknown>;
48
57
  /** Narrow a {@link CronTarget} to a {@link WorkflowReference} by its runtime brand. */
49
58
  declare const isWorkflowReference: (target: unknown) => target is WorkflowReference;
50
59
  /**
@@ -93,7 +102,12 @@ interface ScheduleRecord {
93
102
  */
94
103
  attempts?: number;
95
104
  enqueuedAt: number;
96
- functionPath: string;
105
+ /**
106
+ * The `ns:fn` path of the function to dispatch on fire. Absent when the job
107
+ * targets a durable workflow/agent instead — see {@link ScheduleRecord.workflow}.
108
+ * Exactly one of `functionPath` / `workflow` is set.
109
+ */
110
+ functionPath?: string;
97
111
  id: string;
98
112
  /**
99
113
  * Scheduler/workpool instance name the job was enqueued through. Echoed in
@@ -113,6 +127,14 @@ interface ScheduleRecord {
113
127
  retry?: RetryPolicy;
114
128
  scheduledFor: number;
115
129
  shardKey?: string;
130
+ /**
131
+ * The `WORKFLOW_*`/`AGENT_*` binding name to start a fresh durable instance
132
+ * of on fire (the {@link ScheduleRecord.args} become its `params`). Set
133
+ * instead of {@link ScheduleRecord.functionPath} when the job targets a
134
+ * workflow/agent {@link WorkflowReference}. The runtime — not the DO — owns
135
+ * the binding, so the dispatch payload carries this through to the Worker.
136
+ */
137
+ workflow?: string;
116
138
  }
117
139
  interface Scheduler {
118
140
  cancel: (id: string) => Promise<{
@@ -122,19 +144,39 @@ interface Scheduler {
122
144
  get: (id: string) => Promise<ScheduleRecord | null>;
123
145
  /** All pending scheduled jobs (the DO's `/list` view). */
124
146
  list: () => Promise<ScheduleRecord[]>;
125
- runAfter: <F extends FunctionReference>(delayMs: number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
147
+ /**
148
+ * Schedule `target` to run once, `delayMs` from now. `target` is a function
149
+ * {@link FunctionReference} (dispatched as a one-shot) or a durable
150
+ * {@link WorkflowReference} — the generated `workflows.&lt;name>` /
151
+ * `agents.&lt;name>` ref — which starts a fresh instance on fire (args become
152
+ * its `params`). {@link ScheduleTargetArgs} infers the accepted args from
153
+ * whichever target was passed.
154
+ */
155
+ runAfter: <T extends CronTarget>(delayMs: number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
126
156
  id: string;
127
157
  scheduledFor: number;
128
158
  }>;
129
- runAt: <F extends FunctionReference>(date: Date | number, function_: F, args: ArgsOf<F>, options?: RunOptions) => Promise<{
159
+ /** Like {@link Scheduler.runAfter} but fires at an absolute `date`/timestamp. */
160
+ runAt: <T extends CronTarget>(date: Date | number, target: T, args: ScheduleTargetArgs<T>, options?: RunOptions) => Promise<{
130
161
  id: string;
131
162
  scheduledFor: number;
132
163
  }>;
133
164
  }
165
+ /**
166
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
167
+ * Cloudflare adds values over time.
168
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
169
+ */
170
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
134
171
  /** Subset of `DurableObjectNamespace` the package consumes. */
135
172
  interface DurableObjectNamespaceLike {
136
173
  get: (id: DurableObjectIdLike) => DurableObjectStubLike;
137
174
  idFromName: (name: string) => DurableObjectIdLike;
175
+ /**
176
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
177
+ * workers-types releases (and test doubles) may not expose it.
178
+ */
179
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => DurableObjectNamespaceLike;
138
180
  }
139
181
  interface DurableObjectIdLike {
140
182
  toString: () => string;
@@ -145,6 +187,13 @@ interface DurableObjectStubLike {
145
187
  interface LunoraSchedulerOptions {
146
188
  /** Optional named instance — useful for tenant isolation. Default `default`. */
147
189
  instanceName?: string;
190
+ /**
191
+ * Pin the SchedulerDO (durable timers + cron state) to a Cloudflare
192
+ * data-residency jurisdiction. Pass the same value as the worker's
193
+ * `jurisdiction` so scheduled state co-resides with app data. Omit for the
194
+ * un-pinned global namespace.
195
+ */
196
+ jurisdiction?: DurableObjectJurisdiction;
148
197
  /** Binding to the `SchedulerDO` durable object namespace. */
149
198
  namespace: DurableObjectNamespaceLike;
150
199
  /**
@@ -599,6 +648,13 @@ declare class SchedulerDO {
599
648
  * compatibility shim, not the hot path.
600
649
  */
601
650
  private static releaseFirstSlot;
651
+ /**
652
+ * Normalize the mutually-exclusive dispatch target off an untrusted body: a
653
+ * one-shot function path (`functionPath`) or a durable workflow/agent
654
+ * instance (`workflow`, a `WORKFLOW_*`/`AGENT_*` binding). Returns `undefined`
655
+ * when neither is present so the caller can reject the schedule.
656
+ */
657
+ private static resolveScheduleTarget;
602
658
  protected readonly state: SchedulerDOState;
603
659
  protected readonly env: SchedulerEnv;
604
660
  constructor(state: SchedulerDOState, env: SchedulerEnv);
@@ -630,15 +686,17 @@ declare class SchedulerDO {
630
686
  *
631
687
  * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
632
688
  * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
633
- * thrown storage op decides whether the job stays re-fireable.
689
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
634
690
  *
635
- * When the record was NOT successfully dispatched, re-assert the time-index
636
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
637
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
638
- * before throwing, and re-inserting the same key is idempotent, so a
639
- * surviving claim is simply rewritten to its prior value. When the record WAS
640
- * dispatched (the throw came from post-dispatch cleanup), leave the index
641
- * deleted so the already-kicked, idempotent job is not re-fired.
691
+ * A throw reaching here always means the job was NOT dispatched:
692
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
693
+ * returns instead of throwing once a kick succeeds, so every escaping throw
694
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore always
695
+ * re-assert the time-index claim so a later alarm re-attempts it
696
+ * (at-least-once): the claim delete may have removed it and
697
+ * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
698
+ * re-inserting the same key is idempotent, so a surviving claim is simply
699
+ * rewritten to its prior value.
642
700
  */
643
701
  private drainRecordGuarded;
644
702
  /**
@@ -648,16 +706,18 @@ declare class SchedulerDO {
648
706
  * free slot is reserved durably before dispatch and released immediately if
649
707
  * the kick fails (success holds it until the runtime reports completion).
650
708
  * Success clears the `id:`/`retry:` rows; failure routes to
651
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
652
- * lifetime of the drain so the budget decrements without re-reading storage.
709
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
710
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
711
+ * concurrent /complete landing mid-dispatch can't be clobbered.
712
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
713
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
714
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
715
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
716
+ * is still re-fireable and the guard safely re-claims the time index.
653
717
  * @returns `true` only when the record was successfully dispatched (a 2xx
654
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
655
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
656
- * later throws) must NOT have its time-index claim restored, since re-firing
657
- * an already-kicked job would break idempotency. A `false` return (pool
658
- * backpressure or a failed dispatch) means the job is still re-fireable —
659
- * either already re-armed here, or, if a throw escapes, re-claimed by the
660
- * guard's catch.
718
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
719
+ * re-fireable — already re-armed here). The value is informational (the guard
720
+ * branches on throw/no-throw, not on this boolean).
661
721
  */
662
722
  private drainRecord;
663
723
  /**
@@ -665,6 +725,14 @@ declare class SchedulerDO {
665
725
  * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
666
726
  * otherwise reserves a slot durably and returns `true`. Non-pooled records
667
727
  * always return `true` without touching any pool state.
728
+ *
729
+ * The pool row is read FRESH from storage on every call — never cached
730
+ * across the drain. Each reservation durably `savePool()`s before the next
731
+ * record runs, so a same-pass reservation is still visible to the next
732
+ * record's fresh read (the budget carries forward); and because dispatch()
733
+ * awaits an outbound fetch between records, a concurrent /complete that
734
+ * decrements the row mid-drain IS reflected here instead of being clobbered
735
+ * by a stale in-memory copy (which would leak a slot permanently).
668
736
  */
669
737
  private reservePoolSlot;
670
738
  /**
@@ -782,4 +850,4 @@ declare const isValidCronExpression: (schedule: string) => boolean;
782
850
  * job (`cron job "send digest"`) vs. the bare trigger.
783
851
  */
784
852
  declare const assertValidCronExpression: (schedule: string, context?: string) => void;
785
- export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, type DurableObjectNamespaceLike, type DurableObjectStubLike, type EnqueueOptions, type FunctionReference, type HttpDispatcherOptions, type IntervalSchedule, type LunoraSchedulerOptions, type MessageBatchLike, type MonthlySchedule, type QueueConsumerOptions, type QueueDispatch, type QueueEnqueueOptions, type QueueJob, type QueueLike, type QueueMessageLike, type QueueSendOptionsLike, type QueueSendRequestLike, type QueueWorkpool, type QueueWorkpoolOptions, type RetryPolicy, type RunOptions, type ScheduleRecord, type Scheduler, SchedulerDO, type SchedulerDOState, type SchedulerEnv, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
853
+ export { type ArgsOf, CRON_SCHEDULE_KINDS, type CronJob, type CronJobsBuilder, type CronScheduleKind, type CronTarget, type CronTriggerOptions, type CronTriggerSnippet, type DailySchedule, type DurableObjectIdLike, type DurableObjectJurisdiction, type DurableObjectNamespaceLike, type DurableObjectStubLike, type EnqueueOptions, type FunctionReference, type HttpDispatcherOptions, type IntervalSchedule, type LunoraSchedulerOptions, type MessageBatchLike, type MonthlySchedule, type QueueConsumerOptions, type QueueDispatch, type QueueEnqueueOptions, type QueueJob, type QueueLike, type QueueMessageLike, type QueueSendOptionsLike, type QueueSendRequestLike, type QueueWorkpool, type QueueWorkpoolOptions, type RetryPolicy, type RunOptions, type ScheduleRecord, type Scheduler, SchedulerDO, type SchedulerDOState, type SchedulerEnv, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- export { default as createScheduler } from './packem_shared/createScheduler-KCso4_at.mjs';
2
- export { default as createWorkpool } from './packem_shared/createWorkpool-D5cWPQmH.mjs';
3
- export { createCronTrigger } from './packem_shared/createCronTrigger-Cq9IBcWQ.mjs';
4
- export { CRON_SCHEDULE_KINDS, compileCronSchedule, cronJobs } from './packem_shared/compileCronSchedule-BaLlXJiN.mjs';
5
- export { createQueueConsumer, createQueueWorkpool, httpDispatcher } from './packem_shared/createQueueConsumer-DWahNPfz.mjs';
6
- export { SchedulerDO } from './packem_shared/SchedulerDO-BNzXNnS4.mjs';
1
+ export { default as createScheduler } from './packem_shared/createScheduler-Df9JUjQb.mjs';
2
+ export { default as createWorkpool } from './packem_shared/createWorkpool-Ce0kNM2p.mjs';
3
+ export { createCronTrigger } from './packem_shared/createCronTrigger-Dh4VLxD9.mjs';
4
+ export { CRON_SCHEDULE_KINDS, compileCronSchedule, cronJobs } from './packem_shared/CRON_SCHEDULE_KINDS-C2FflEnq.mjs';
5
+ export { createQueueConsumer, createQueueWorkpool, httpDispatcher } from './packem_shared/createQueueConsumer-Cy-Mp-El.mjs';
6
+ export { SchedulerDO } from './packem_shared/SchedulerDO-DeJHI379.mjs';
7
7
  export { isWorkflowReference } from './packem_shared/isWorkflowReference-C9mQkMXt.mjs';
8
- export { assertValidCronExpression, isValidCronExpression } from './packem_shared/assertValidCronExpression-BLfrDgmK.mjs';
8
+ export { assertValidCronExpression, isValidCronExpression } from './packem_shared/assertValidCronExpression-B9m75qU0.mjs';
@@ -1,5 +1,6 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { isWorkflowReference } from './isWorkflowReference-C9mQkMXt.mjs';
2
- import { assertValidCronExpression } from './assertValidCronExpression-BLfrDgmK.mjs';
3
+ import { assertValidCronExpression } from './assertValidCronExpression-B9m75qU0.mjs';
3
4
 
4
5
  const WEEKDAY_INDEX = {
5
6
  friday: 5,
@@ -12,24 +13,37 @@ const WEEKDAY_INDEX = {
12
13
  };
13
14
  const field = (value, label, min, max) => {
14
15
  if (!Number.isInteger(value) || value < min || value > max) {
15
- throw new Error(`@lunora/scheduler: cronJobs ${label} must be an integer in [${min.toFixed(0)}, ${max.toFixed(0)}], got ${String(value)}`);
16
+ throw new LunoraError(
17
+ "INTERNAL",
18
+ `@lunora/scheduler: cronJobs ${label} must be an integer in [${min.toFixed(0)}, ${max.toFixed(0)}], got ${String(value)}`
19
+ );
16
20
  }
17
21
  return value.toFixed(0);
18
22
  };
23
+ const stepField = (value, label, period) => {
24
+ const rendered = field(value, label, 1, period - 1);
25
+ if (period % value !== 0) {
26
+ throw new LunoraError(
27
+ "INTERNAL",
28
+ `@lunora/scheduler: ${label} must evenly divide ${period.toFixed(0)} for a fixed "every ${value.toFixed(0)}" interval — cron "*/${value.toFixed(0)}" means "at values divisible by ${value.toFixed(0)}", which wraps unevenly; pick a divisor of ${period.toFixed(0)}`
29
+ );
30
+ }
31
+ return rendered;
32
+ };
19
33
  const compileInterval = (schedule) => {
20
34
  const units = ["seconds", "minutes", "hours"].filter((unit2) => schedule[unit2] !== void 0);
21
35
  if (units.length !== 1) {
22
- throw new Error(`@lunora/scheduler: interval schedule must specify exactly one of { seconds, minutes, hours }`);
36
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: interval schedule must specify exactly one of { seconds, minutes, hours }`);
23
37
  }
24
38
  const unit = units[0];
25
39
  const value = schedule[unit];
26
40
  if (unit === "seconds") {
27
- return `*/${field(value, "interval.seconds", 1, 59)} * * * * *`;
41
+ return `*/${stepField(value, "interval.seconds", 60)} * * * * *`;
28
42
  }
29
43
  if (unit === "minutes") {
30
- return `*/${field(value, "interval.minutes", 1, 59)} * * * *`;
44
+ return `*/${stepField(value, "interval.minutes", 60)} * * * *`;
31
45
  }
32
- return `0 */${field(value, "interval.hours", 1, 23)} * * *`;
46
+ return `0 */${stepField(value, "interval.hours", 24)} * * *`;
33
47
  };
34
48
  const compileDaily = (schedule) => {
35
49
  const minute = field(schedule.minuteUTC, "daily.minuteUTC", 0, 59);
@@ -39,7 +53,7 @@ const compileDaily = (schedule) => {
39
53
  const compileWeekly = (schedule) => {
40
54
  const index = WEEKDAY_INDEX[schedule.dayOfWeek];
41
55
  if (index === void 0) {
42
- throw new Error(`@lunora/scheduler: weekly schedule has invalid dayOfWeek "${schedule.dayOfWeek}"`);
56
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: weekly schedule has invalid dayOfWeek "${schedule.dayOfWeek}"`);
43
57
  }
44
58
  const minute = field(schedule.minuteUTC, "weekly.minuteUTC", 0, 59);
45
59
  const hour = field(schedule.hourUTC, "weekly.hourUTC", 0, 23);
@@ -67,7 +81,7 @@ const compileCronSchedule = (kind, schedule) => {
67
81
  return compileWeekly(schedule);
68
82
  }
69
83
  default: {
70
- throw new Error(`@lunora/scheduler: unknown cron schedule kind "${String(kind)}"`);
84
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: unknown cron schedule kind "${String(kind)}"`);
71
85
  }
72
86
  }
73
87
  };
@@ -76,10 +90,10 @@ const cronJobs = () => {
76
90
  const seen = /* @__PURE__ */ new Set();
77
91
  const register = (name, cron, target, args) => {
78
92
  if (typeof name !== "string" || name.trim() === "") {
79
- throw new Error(`@lunora/scheduler: cron job name must be a non-empty string`);
93
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: cron job name must be a non-empty string`);
80
94
  }
81
95
  if (seen.has(name)) {
82
- throw new Error(`@lunora/scheduler: duplicate cron job name "${name}" — names must be unique within one cronJobs()`);
96
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: duplicate cron job name "${name}" — names must be unique within one cronJobs()`);
83
97
  }
84
98
  if (isWorkflowReference(target)) {
85
99
  assertValidCronExpression(cron, `cron expression for job "${name}"`);
@@ -88,7 +102,10 @@ const cronJobs = () => {
88
102
  return;
89
103
  }
90
104
  if (!target || typeof target.__lunoraRef !== "string") {
91
- throw new Error(`@lunora/scheduler: cron job "${name}" requires a function reference (e.g. internal.email.digest) or a workflow reference`);
105
+ throw new LunoraError(
106
+ "INTERNAL",
107
+ `@lunora/scheduler: cron job "${name}" requires a function reference (e.g. internal.email.digest) or a workflow reference`
108
+ );
92
109
  }
93
110
  assertValidCronExpression(cron, `cron expression for job "${name}"`);
94
111
  seen.add(name);