@lunora/scheduler 1.0.0-alpha.12 → 1.0.0-alpha.14

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
@@ -1,3 +1,4 @@
1
+ import { SchedulerHost } from '@lunora/platform';
1
2
  /**
2
3
  * Opaque reference to a Lunora function. Mirrors the `FunctionReference` shape
3
4
  * emitted by `@lunora/codegen` (and consumed by `@lunora/client`). We avoid a
@@ -140,6 +141,19 @@ interface Scheduler {
140
141
  cancel: (id: string) => Promise<{
141
142
  cancelled: boolean;
142
143
  }>;
144
+ /**
145
+ * Jobs that exhausted their retry budget and were parked under `dead:`
146
+ * (the DO's `/dead` view). Deliberately absent from {@link Scheduler.list} —
147
+ * the park deletes the `id:` header — so this is the only view of a job
148
+ * that failed permanently rather than being silently dropped.
149
+ */
150
+ dead: () => Promise<ScheduleRecord[]>;
151
+ /**
152
+ * Resurrect a parked job with a fresh attempt budget (the DO's
153
+ * `POST /dead/retry`). `false` when the id is not parked; a racing double
154
+ * recover is a no-op rather than an error.
155
+ */
156
+ deadRetry: (id: string) => Promise<boolean>;
143
157
  /** Resolve a single pending job by id, or `null` when absent (derived from {@link Scheduler.list}). */
144
158
  get: (id: string) => Promise<ScheduleRecord | null>;
145
159
  /** All pending scheduled jobs (the DO's `/list` view). */
@@ -842,6 +856,33 @@ declare class SchedulerDO {
842
856
  private armAlarmIfEarlier;
843
857
  private rescheduleAlarm;
844
858
  }
859
+ /** What the Cloudflare scheduler host needs from the Worker's environment. */
860
+ interface SchedulerHostOptions {
861
+ /**
862
+ * Named scheduler instance — one `SchedulerDO` per name, useful for tenant
863
+ * isolation. Defaults to `"default"`.
864
+ */
865
+ instanceName?: string;
866
+ /**
867
+ * Data-residency jurisdiction for the `SchedulerDO`. Pass the same value as
868
+ * the worker's own jurisdiction so scheduled state co-resides with app data.
869
+ */
870
+ jurisdiction?: "eu" | "fedramp" | "us";
871
+ /** The `SchedulerDO` namespace binding. */
872
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
873
+ /**
874
+ * Public origin the Worker is mounted at. `SchedulerDO` dispatches back to
875
+ * this base URL when an alarm fires, so a wrong value means jobs fire into
876
+ * nothing.
877
+ */
878
+ originUrl: string;
879
+ }
880
+ /**
881
+ * Build the Cloudflare {@link SchedulerHost}.
882
+ *
883
+ * The returned host has no `cron` member — see the module docstring.
884
+ */
885
+ declare const createSchedulerHost: (options: SchedulerHostOptions) => SchedulerHost;
845
886
  /** Standard cron expression (5- or 6-field) or a supported `@macro`, per `cron-parser`. */
846
887
  declare const isValidCronExpression: (schedule: string) => boolean;
847
888
  /**
@@ -850,4 +891,4 @@ declare const isValidCronExpression: (schedule: string) => boolean;
850
891
  * job (`cron job "send digest"`) vs. the bare trigger.
851
892
  */
852
893
  declare const assertValidCronExpression: (schedule: string, context?: string) => void;
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 };
894
+ 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 SchedulerHostOptions, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createSchedulerHost, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SchedulerHost } from '@lunora/platform';
1
2
  /**
2
3
  * Opaque reference to a Lunora function. Mirrors the `FunctionReference` shape
3
4
  * emitted by `@lunora/codegen` (and consumed by `@lunora/client`). We avoid a
@@ -140,6 +141,19 @@ interface Scheduler {
140
141
  cancel: (id: string) => Promise<{
141
142
  cancelled: boolean;
142
143
  }>;
144
+ /**
145
+ * Jobs that exhausted their retry budget and were parked under `dead:`
146
+ * (the DO's `/dead` view). Deliberately absent from {@link Scheduler.list} —
147
+ * the park deletes the `id:` header — so this is the only view of a job
148
+ * that failed permanently rather than being silently dropped.
149
+ */
150
+ dead: () => Promise<ScheduleRecord[]>;
151
+ /**
152
+ * Resurrect a parked job with a fresh attempt budget (the DO's
153
+ * `POST /dead/retry`). `false` when the id is not parked; a racing double
154
+ * recover is a no-op rather than an error.
155
+ */
156
+ deadRetry: (id: string) => Promise<boolean>;
143
157
  /** Resolve a single pending job by id, or `null` when absent (derived from {@link Scheduler.list}). */
144
158
  get: (id: string) => Promise<ScheduleRecord | null>;
145
159
  /** All pending scheduled jobs (the DO's `/list` view). */
@@ -842,6 +856,33 @@ declare class SchedulerDO {
842
856
  private armAlarmIfEarlier;
843
857
  private rescheduleAlarm;
844
858
  }
859
+ /** What the Cloudflare scheduler host needs from the Worker's environment. */
860
+ interface SchedulerHostOptions {
861
+ /**
862
+ * Named scheduler instance — one `SchedulerDO` per name, useful for tenant
863
+ * isolation. Defaults to `"default"`.
864
+ */
865
+ instanceName?: string;
866
+ /**
867
+ * Data-residency jurisdiction for the `SchedulerDO`. Pass the same value as
868
+ * the worker's own jurisdiction so scheduled state co-resides with app data.
869
+ */
870
+ jurisdiction?: "eu" | "fedramp" | "us";
871
+ /** The `SchedulerDO` namespace binding. */
872
+ namespace: Parameters<typeof createScheduler>[0]["namespace"];
873
+ /**
874
+ * Public origin the Worker is mounted at. `SchedulerDO` dispatches back to
875
+ * this base URL when an alarm fires, so a wrong value means jobs fire into
876
+ * nothing.
877
+ */
878
+ originUrl: string;
879
+ }
880
+ /**
881
+ * Build the Cloudflare {@link SchedulerHost}.
882
+ *
883
+ * The returned host has no `cron` member — see the module docstring.
884
+ */
885
+ declare const createSchedulerHost: (options: SchedulerHostOptions) => SchedulerHost;
845
886
  /** Standard cron expression (5- or 6-field) or a supported `@macro`, per `cron-parser`. */
846
887
  declare const isValidCronExpression: (schedule: string) => boolean;
847
888
  /**
@@ -850,4 +891,4 @@ declare const isValidCronExpression: (schedule: string) => boolean;
850
891
  * job (`cron job "send digest"`) vs. the bare trigger.
851
892
  */
852
893
  declare const assertValidCronExpression: (schedule: string, context?: string) => void;
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 };
894
+ 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 SchedulerHostOptions, type SchedulerPoolStatus, type SchedulerStatus, type WeeklySchedule, type WorkflowReference, type Workpool, type WorkpoolOptions, assertValidCronExpression, compileCronSchedule, createCronTrigger, createQueueConsumer, createQueueWorkpool, createScheduler, createSchedulerHost, createWorkpool, cronJobs, httpDispatcher, isValidCronExpression, isWorkflowReference };
package/dist/index.mjs CHANGED
@@ -1,8 +1 @@
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
- export { isWorkflowReference } from './packem_shared/isWorkflowReference-C9mQkMXt.mjs';
8
- export { assertValidCronExpression, isValidCronExpression } from './packem_shared/assertValidCronExpression-B9m75qU0.mjs';
1
+ import{default as o}from"./packem_shared/createScheduler-CsRAEtdb.mjs";import{default as p}from"./packem_shared/createWorkpool-DyVdhB6o.mjs";import{createCronTrigger as c}from"./packem_shared/createCronTrigger-CyPdVKNF.mjs";import{CRON_SCHEDULE_KINDS as f,compileCronSchedule as l,cronJobs as m}from"./packem_shared/CRON_SCHEDULE_KINDS-CZFw97XW.mjs";import{createQueueConsumer as x,createQueueWorkpool as i,httpDispatcher as n}from"./packem_shared/createQueueConsumer-B0qNXRUJ.mjs";import{SchedulerDO as C}from"./packem_shared/SchedulerDO-DGgeZ-uI.mjs";import{createSchedulerHost as S}from"./packem_shared/createSchedulerHost-C5XacwXv.mjs";import{isWorkflowReference as E}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as W,isValidCronExpression as g}from"./packem_shared/assertValidCronExpression-DHvO7Vm9.mjs";export{f as CRON_SCHEDULE_KINDS,C as SchedulerDO,W as assertValidCronExpression,l as compileCronSchedule,c as createCronTrigger,x as createQueueConsumer,i as createQueueWorkpool,o as createScheduler,S as createSchedulerHost,p as createWorkpool,m as cronJobs,n as httpDispatcher,g as isValidCronExpression,E as isWorkflowReference};
@@ -0,0 +1 @@
1
+ import{LunoraError as u}from"@lunora/errors";import{isWorkflowReference as m}from"./isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as h}from"./assertValidCronExpression-DHvO7Vm9.mjs";const y={friday:5,monday:1,saturday:6,sunday:0,thursday:4,tuesday:2,wednesday:3},l=(e,r,n,t)=>{if(!Number.isInteger(e)||e<n||e>t)throw new u("INTERNAL",`@lunora/scheduler: cronJobs ${r} must be an integer in [${n.toFixed(0)}, ${t.toFixed(0)}], got ${String(e)}`);return e.toFixed(0)},c=(e,r,n)=>{const t=l(e,r,1,n-1);if(n%e!==0)throw new u("INTERNAL",`@lunora/scheduler: ${r} must evenly divide ${n.toFixed(0)} for a fixed "every ${e.toFixed(0)}" interval — cron "*/${e.toFixed(0)}" means "at values divisible by ${e.toFixed(0)}", which wraps unevenly; pick a divisor of ${n.toFixed(0)}`);return t},f=e=>{const r=["seconds","minutes","hours"].filter(o=>e[o]!==void 0);if(r.length!==1)throw new u("INTERNAL","@lunora/scheduler: interval schedule must specify exactly one of { seconds, minutes, hours }");const n=r[0],t=e[n];return n==="seconds"?`*/${c(t,"interval.seconds",60)} * * * * *`:n==="minutes"?`*/${c(t,"interval.minutes",60)} * * * *`:`0 */${c(t,"interval.hours",24)} * * *`},w=e=>{const r=l(e.minuteUTC,"daily.minuteUTC",0,59),n=l(e.hourUTC,"daily.hourUTC",0,23);return`${r} ${n} * * *`},$=e=>{const r=y[e.dayOfWeek];if(r===void 0)throw new u("INTERNAL",`@lunora/scheduler: weekly schedule has invalid dayOfWeek "${e.dayOfWeek}"`);const n=l(e.minuteUTC,"weekly.minuteUTC",0,59),t=l(e.hourUTC,"weekly.hourUTC",0,23);return`${n} ${t} * * ${r.toFixed(0)}`},T=e=>{const r=l(e.day,"monthly.day",1,31),n=l(e.minuteUTC,"monthly.minuteUTC",0,59),t=l(e.hourUTC,"monthly.hourUTC",0,23);return`${n} ${t} ${r} * *`},C=new Set(["daily","interval","monthly","weekly"]),d=(e,r)=>{switch(e){case"daily":return w(r);case"interval":return f(r);case"monthly":return T(r);case"weekly":return $(r);default:throw new u("INTERNAL",`@lunora/scheduler: unknown cron schedule kind "${String(e)}"`)}},b=()=>{const e=[],r=new Set,n=(o,s,i,a)=>{if(typeof o!="string"||o.trim()==="")throw new u("INTERNAL","@lunora/scheduler: cron job name must be a non-empty string");if(r.has(o))throw new u("INTERNAL",`@lunora/scheduler: duplicate cron job name "${o}" — names must be unique within one cronJobs()`);if(m(i)){h(s,`cron expression for job "${o}"`),r.add(o),e.push({args:a??{},cron:s,name:o,workflow:typeof i.name=="string"?i.name:""});return}if(!i||typeof i.__lunoraRef!="string")throw new u("INTERNAL",`@lunora/scheduler: cron job "${o}" requires a function reference (e.g. internal.email.digest) or a workflow reference`);h(s,`cron expression for job "${o}"`),r.add(o),e.push({args:a??{},cron:s,functionPath:i.__lunoraRef,name:o})},t={cron(o,s,i,a){return n(o,s,i,a),t},daily(o,s,i,a){return n(o,d("daily",s),i,a),t},interval(o,s,i,a){return n(o,d("interval",s),i,a),t},jobs:()=>[...e],monthly(o,s,i,a){return n(o,d("monthly",s),i,a),t},weekly(o,s,i,a){return n(o,d("weekly",s),i,a),t}};return t};export{C as CRON_SCHEDULE_KINDS,d as compileCronSchedule,b as cronJobs};
@@ -0,0 +1 @@
1
+ const y=(u,t=200,e)=>Response.json(u,{headers:{"content-type":"application/json",...e},status:t});const m="retry:",g="dead:",p="pool:";const w=u=>String(u).padStart(15,"0"),I=()=>{const u=crypto.getRandomValues(new Uint8Array(12));let t="";for(const e of u)t+=String.fromCodePoint(e);return btoa(t).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")};class i{static indexKey(t,e){return`t:${w(t)}:${e}`}static json(t,e=200){return y(t,e)}static error(t,e,a){return i.json({error:{code:e,message:a}},t)}static resolveRetry(t){const e=t.retry,a=typeof e?.maxAttempts=="number"&&Number.isInteger(e.maxAttempts)&&e.maxAttempts>0?e.maxAttempts:5,r=typeof e?.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0?e.baseMs:3e4,s=e?.backoff==="linear"?"linear":"exponential",n=typeof e?.maxMs=="number"&&Number.isFinite(e.maxMs)&&e.maxMs>=0?e.maxMs:void 0;return{backoff:s,baseMs:r,maxAttempts:a,maxMs:n}}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,a={};return typeof e.maxAttempts=="number"&&Number.isInteger(e.maxAttempts)&&e.maxAttempts>0&&(a.maxAttempts=e.maxAttempts),typeof e.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0&&(a.baseMs=e.baseMs),(e.backoff==="exponential"||e.backoff==="linear")&&(a.backoff=e.backoff),typeof e.maxMs=="number"&&Number.isFinite(e.maxMs)&&e.maxMs>=0&&(a.maxMs=e.maxMs),Object.keys(a).length===0?void 0:a}static releaseSlot(t,e){if(t.inFlightIds===void 0)return{...t,inFlight:Math.max(0,t.inFlight-1)};const a=t.inFlightIds.filter(r=>r!==e);return{...t,inFlight:a.length,inFlightIds:a}}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,a=typeof t?.workflow=="string"&&t.workflow.length>0?t.workflow:void 0;if(!(e===void 0&&a===void 0))return{functionPath:e,workflow:a}}state;env;constructor(t,e){this.state=t,this.env=e}async fetch(t){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();case"GET /get":return this.handleGet(e);case"GET /list":return this.handleList();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 y({error:{code:"NOT_FOUND"}},404)}async alarm(){const t=Date.now(),e=[],a=await this.state.storage.list({end:`t:${w(t)}:~`,limit:100,prefix:"t:"});for(const[r,s]of a.entries()){const n=Number.parseInt(r.slice(2,r.indexOf(":",2)),10);if(Number.isFinite(n)&&n<=t){const o=await this.state.storage.get(`id:${s}`);o?e.push(o):await this.state.storage.delete(r)}}try{for(const r of e)await this.drainRecordGuarded(r)}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 a=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 r={"content-type":"application/json"},s=await this.signDispatch(a);return s!==void 0?r["x-lunora-scheduler-signature"]=s:typeof this.env.LUNORA_ADMIN_TOKEN=="string"&&this.env.LUNORA_ADMIN_TOKEN.length>0&&(r.authorization=`Bearer ${this.env.LUNORA_ADMIN_TOKEN}`),(await fetch(`${e}/_lunora/scheduler/dispatch`,{body:a,headers:r,method:"POST"})).ok}catch{return!1}}async drainRecordGuarded(t){try{await this.state.storage.delete(i.indexKey(t.scheduledFor,t.id)),await this.drainRecord(t)}catch{try{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 e=await this.dispatch(t);if(!e&&t.pool!==void 0){const a=await this.loadPool(t.pool),r=i.releaseSlot(a,t.id);await this.savePool(t.pool,r)}if(e){try{await this.state.storage.delete([`id:${t.id}`,`${m}${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 a=e.inFlightIds??[];return a.includes(t.id)||a.push(t.id),e.inFlightIds=a,e.inFlight=a.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],a=t[1];return this.state.acceptWebSocket(a),a.send(JSON.stringify({records:await this.listRecords(),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 e=JSON.stringify({records:await this.listRecords(),type:"jobs"});for(const a of t)try{a.send(e)}catch{}}async listRecords(){return[...(await this.state.storage.list({prefix:"id:"})).values()]}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 a=new TextEncoder,r=await crypto.subtle.importKey("raw",a.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),s=await crypto.subtle.sign("HMAC",r,a.encode(t)),n=new Uint8Array(s);let o="";for(const l of n)o+=String.fromCodePoint(l);return btoa(o).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")}async recordRetry(t){const e=(t.attempts??0)+1,{backoff:a,baseMs:r,maxAttempts:s,maxMs:n}=i.resolveRetry(t);if(e>s){await this.state.storage.put(`${g}${t.id}`,{...t,attempts:e}),await this.state.storage.delete([`${m}${t.id}`,`id:${t.id}`]),console.warn(`@lunora/scheduler: job "${t.id}" (${t.functionPath??t.workflow??"unknown"}) parked in dead-letter after ${String(e)} attempts`);return}const o=a==="linear"?r*e:r*2**(e-1),l=n===void 0?o:Math.min(o,n),c=Date.now()+l,d={...t,attempts:e,scheduledFor:c};await this.state.storage.put(`${m}${t.id}`,d),await this.state.storage.put(`id:${t.id}`,d),await this.state.storage.put(i.indexKey(c,t.id),t.id)}async loadPool(t,e){const a=await this.state.storage.get(`${p}${t}`);return a!==void 0?Array.isArray(a.inFlightIds)?{inFlight:a.inFlightIds.length,inFlightIds:[...a.inFlightIds],maxConcurrency:a.maxConcurrency}:{inFlight:Math.max(0,a.inFlight),maxConcurrency:a.maxConcurrency}:{inFlight:0,inFlightIds:[],maxConcurrency:i.normalizeConcurrency(e,1)}}async savePool(t,e){await this.state.storage.put(`${p}${t}`,e)}async requeuePooled(t){const e=Date.now()+1e3,a={...t,scheduledFor:e};await this.state.storage.put(`id:${t.id}`,a),await this.state.storage.put(i.indexKey(e,t.id),t.id)}async handleComplete(t){const e=await t.json().catch(()=>{}),a=typeof e?.pool=="string"&&e.pool.length>0?e.pool:void 0,r=typeof e?.id=="string"&&e.id.length>0?e.id:void 0;if(a===void 0)return i.error(400,"INVALID_INPUT","pool is required");const s=await this.loadPool(a),n=r===void 0?i.releaseFirstSlot(s):i.releaseSlot(s,r);return await this.savePool(a,n),await this.armAlarmIfEarlier(Date.now()),i.json({inFlight:n.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 a=await this.loadPool(e),r=await this.state.storage.list({prefix:"id:"});let s=0;for(const n of r.values())n.pool===e&&(s+=1);return i.json({inFlight:a.inFlight,maxConcurrency:a.maxConcurrency,queued:s})}async handleStatus(){const t=await this.state.storage.list({prefix:p}),e=await this.state.storage.list({prefix:"id:"}),a=new Map;for(const l of e.values())l.pool!==void 0&&a.set(l.pool,(a.get(l.pool)??0)+1);const r=[];let s=0,n=0;for(const[l,c]of t.entries()){const d=l.slice(p.length),h=Math.max(0,c.inFlight),f=a.get(d)??0;r.push({inFlight:h,maxConcurrency:c.maxConcurrency,name:d,queued:f}),s+=f,n+=h}const o={backlog:s,inFlight:n,pools:r};return i.json(o)}async handleSchedule(t){const e=await t.json().catch(()=>{}),a=i.resolveScheduleTarget(e);if(!e||a===void 0)return i.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:r,workflow:s}=a;if(typeof e.scheduledFor!="number"||!Number.isInteger(e.scheduledFor)||e.scheduledFor<=0||e.scheduledFor>999999999999999)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 n=typeof e.pool=="string"&&e.pool.length>0?e.pool:void 0,o=typeof e.instanceName=="string"&&e.instanceName.length>0?e.instanceName:void 0,l=i.normalizeRetry(e.retry),c=I(),d={args:e.args??{},enqueuedAt:Date.now(),id:c,...r===void 0?{}:{functionPath:r},...o===void 0?{}:{instanceName:o},...n===void 0?{}:{pool:n},...l===void 0?{}:{retry:l},scheduledFor:e.scheduledFor,shardKey:e.shardKey,...s===void 0?{}:{workflow:s}};if(n!==void 0){const h=await this.loadPool(n,e.maxConcurrency);await this.savePool(n,{inFlight:h.inFlight,...h.inFlightIds===void 0?{}:{inFlightIds:h.inFlightIds},maxConcurrency:i.normalizeConcurrency(e.maxConcurrency,h.maxConcurrency)})}return await this.state.storage.put(`id:${c}`,d),await this.state.storage.put(i.indexKey(d.scheduledFor,c),c),await this.armAlarmIfEarlier(d.scheduledFor),await this.broadcastChange(),i.json({id:c,scheduledFor:d.scheduledFor})}async handleCancel(t){const e=await t.json().catch(()=>{});if(!e?.id)return i.error(400,"INVALID_INPUT","id is required");const a=await this.state.storage.get(`id:${e.id}`);return a?(await this.removeRecord(a),await this.rescheduleAlarm(),await this.broadcastChange(),i.json({cancelled:!0})):i.json({cancelled:!1})}async handleList(){return i.json({records:await this.listRecords()})}async handleDeadList(){const t=await this.state.storage.list({prefix:g});return i.json({records:[...t.values()]})}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 a=await this.state.storage.get(`${g}${e.id}`);if(a===void 0)return i.json({retried:!1});const r=Date.now(),s={...a,attempts:0,scheduledFor:r};return await this.state.storage.put(`id:${a.id}`,s),await this.state.storage.put(i.indexKey(r,a.id),a.id),await this.state.storage.delete(`${g}${a.id}`),await this.armAlarmIfEarlier(r),await this.broadcastChange(),i.json({id:a.id,retried:!0,scheduledFor:r})}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 a=await this.state.storage.delete(`${g}${e.id}`);return i.json({removed:!!a})}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 a=await this.state.storage.get(`id:${e}`);return i.json(a===void 0?{}:{record:a})}async removeRecord(t){await this.state.storage.delete([`id:${t.id}`,i.indexKey(t.scheduledFor,t.id),`${m}${t.id}`])}async armAlarmIfEarlier(t){const e=await this.state.storage.getAlarm();(e===null||t<e)&&await this.state.storage.setAlarm(t)}async rescheduleAlarm(){const t=(await this.state.storage.list({limit:1,prefix:"t:"})).entries().next();if(t.done){await this.state.storage.deleteAlarm();return}const[e]=t.value,a=Number.parseInt(e.slice(2,e.indexOf(":",2)),10);Number.isFinite(a)&&await this.state.storage.setAlarm(a)}}export{i as SchedulerDO};
@@ -0,0 +1 @@
1
+ import{LunoraError as s}from"@lunora/errors";import{CronExpressionParser as o}from"cron-parser";const n=r=>{if(typeof r!="string"||r.trim()==="")return!1;try{return o.parse(r.trim()),!0}catch{return!1}},a=(r,e="cron expression")=>{if(!n(r))throw new s("INTERNAL",`@lunora/scheduler: invalid ${e} "${r}" — expected a standard 5- or 6-field cron expression (e.g. "0 * * * *")`)};export{a as assertValidCronExpression,n as isValidCronExpression};
@@ -0,0 +1 @@
1
+ import{LunoraError as s}from"@lunora/errors";import{assertValidCronExpression as n}from"./assertValidCronExpression-DHvO7Vm9.mjs";const a=r=>{if(!r.schedule||!r.fn)throw new s("INTERNAL","@lunora/scheduler: createCronTrigger() requires `schedule` and `fn`");n(r.schedule);const e=JSON.stringify({triggers:{crons:[r.schedule]}},void 0,2);return{crons:[r.schedule],dispatcher:{args:r.args??{},functionPath:r.fn.__lunoraRef},wranglerJsonc:e}};export{a as createCronTrigger};
@@ -0,0 +1 @@
1
+ import{LunoraError as r}from"@lunora/errors";const u=e=>{let a=e.length;for(;a>0&&e[a-1]==="/";)a-=1;return e.slice(0,a)},h=e=>{if(!e.queue)throw new r("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(a,t,n={})=>{const o={args:t,functionPath:a.__lunoraRef,shardKey:n.shardKey},s=n.delaySeconds===void 0?void 0:{delaySeconds:n.delaySeconds};await e.queue.send(o,s)},enqueueBatch:async(a,t)=>{const n=a.map(o=>({body:{args:o.args,functionPath:o.ref.__lunoraRef,shardKey:o.shardKey}}));await e.queue.sendBatch(n,t)}}},i=e=>typeof e=="object"&&e!==null&&typeof e.functionPath=="string",l=e=>async a=>{await Promise.all(a.messages.map(async t=>{try{if(!i(t.body))throw new r("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await e.dispatch(t.body),t.ack()}catch{t.retry()}}))},d=e=>{const a=e.fetchImpl??globalThis.fetch;if(typeof a!="function")throw new TypeError("@lunora/scheduler: no fetch implementation available — pass fetchImpl or run on a platform with global fetch");const t=`${u(e.originUrl)}/_lunora/scheduler/dispatch`;return async n=>{const o=await a(t,{body:JSON.stringify({args:n.args??{},functionPath:n.functionPath,shardKey:n.shardKey}),headers:{authorization:`Bearer ${e.adminToken}`,"content-type":"application/json"},method:"POST"});if(!o.ok)throw new r("INTERNAL",`@lunora/scheduler: queue dispatch failed (${o.status.toString()}): ${await o.text()}`)}};export{l as createQueueConsumer,h as createQueueWorkpool,d as httpDispatcher};
@@ -0,0 +1 @@
1
+ import{LunoraError as i}from"@lunora/errors";import{l as s,u as a}from"./do-client-BFps7NIj.mjs";import{isWorkflowReference as g}from"./isWorkflowReference-CT3tdefh.mjs";const y=n=>{if(!n.namespace)throw new i("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required");if(!n.originUrl)throw new i("INTERNAL","@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker");const c=async(e,r,o,t={})=>{const l=e instanceof Date?e.getTime():e,d={args:o,originUrl:n.originUrl,pool:t.pool,retry:t.retry,scheduledFor:l,shardKey:t.shardKey};if(g(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new i("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return a(n,"/schedule",{...d,workflow:r.binding})}const u=typeof r=="string"?r:r.__lunoraRef;return a(n,"/schedule",{...d,functionPath:u})};return{cancel:async e=>a(n,"/cancel",{id:e}),dead:async()=>{const e=await s(n,"/dead");return Array.isArray(e.records)?e.records:[]},deadRetry:async e=>{const{retried:r}=await a(n,"/dead/retry",{id:e});return r===!0},get:async e=>(await s(n,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>{const e=await s(n,"/list");return Array.isArray(e.records)?e.records:[]},runAfter:async(e,r,o,t={})=>{if(!Number.isFinite(e)||e<0)throw new i("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return c(Date.now()+e,r,o,t)},runAt:c}};export{y as default};
@@ -0,0 +1 @@
1
+ import i from"./createScheduler-CsRAEtdb.mjs";const o=a=>a?.at!==void 0?typeof a.at=="number"?a.at:a.at.getTime():Date.now()+(a?.delayMs??0),u=a=>{const t=i({instanceName:a.instanceName,jurisdiction:a.jurisdiction,namespace:a.namespace,originUrl:a.originUrl}),c=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:r}=await t.cancel(e);return r},deadLetter:{list:async()=>(await t.dead()).map(e=>c(e)),requeue:async e=>t.deadRetry(e)},list:async()=>(await t.list()).map(e=>c(e)),schedule:async(e,r,n)=>{const s=t.runAt;return s(o(n),e,r,{retry:n?.retry,shardKey:n?.shardKey})}}};export{u as createSchedulerHost};
@@ -0,0 +1 @@
1
+ import{LunoraError as n}from"@lunora/errors";import{l as c,u as i}from"./do-client-BFps7NIj.mjs";const m=e=>{if(!e.namespace)throw new n("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required");if(!e.originUrl)throw new n("INTERNAL","@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker");if(!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new n("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const r=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async a=>i(e,"/cancel",{id:a}),enqueue:async(a,u,o={})=>{const t=o.delayMs??0;if(!Number.isFinite(t)||t<0)throw new n("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return i(e,"/schedule",{args:u,functionPath:a.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,originUrl:e.originUrl,pool:r,retry:o.retry,scheduledFor:Date.now()+t,shardKey:o.shardKey})},name:r,status:async()=>c(e,`/pool?name=${encodeURIComponent(r)}`)}};export{m as default};
@@ -0,0 +1 @@
1
+ import{LunoraError as o}from"@lunora/errors";const s=(e,t)=>{if(t===void 0)return e;if(typeof e.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 e.jurisdiction(t)},i=e=>{const t=s(e.namespace,e.jurisdiction);return t.get(t.idFromName(e.instanceName??"default"))},u=async(e,t,r)=>{const n=await i(e).fetch(`https://scheduler.internal${t}`,{body:JSON.stringify(r),headers:{"content-type":"application/json"},method:"POST"});if(!n.ok){const a=await n.text();throw new o("INTERNAL",`@lunora/scheduler: SchedulerDO ${t} failed (${String(n.status)}): ${a}`)}return await n.json()},d=async(e,t)=>{const r=await i(e).fetch(`https://scheduler.internal${t}`,{method:"GET"});if(!r.ok){const n=await r.text();throw new o("INTERNAL",`@lunora/scheduler: SchedulerDO ${t} failed (${String(r.status)}): ${n}`)}return await r.json()};export{d as l,u};
@@ -0,0 +1 @@
1
+ const e=o=>typeof o=="object"&&o!==null&&o.isLunoraWorkflow===!0;export{e as isWorkflowReference};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "1.0.0-alpha.12",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.8",
49
+ "@lunora/errors": "1.0.0-alpha.9",
50
+ "@lunora/platform": "1.0.0-alpha.1",
50
51
  "cron-parser": "5.6.2"
51
52
  },
52
53
  "engines": {
@@ -1,140 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { isWorkflowReference } from './isWorkflowReference-C9mQkMXt.mjs';
3
- import { assertValidCronExpression } from './assertValidCronExpression-B9m75qU0.mjs';
4
-
5
- const WEEKDAY_INDEX = {
6
- friday: 5,
7
- monday: 1,
8
- saturday: 6,
9
- sunday: 0,
10
- thursday: 4,
11
- tuesday: 2,
12
- wednesday: 3
13
- };
14
- const field = (value, label, min, max) => {
15
- if (!Number.isInteger(value) || value < min || value > max) {
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
- );
20
- }
21
- return value.toFixed(0);
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
- };
33
- const compileInterval = (schedule) => {
34
- const units = ["seconds", "minutes", "hours"].filter((unit2) => schedule[unit2] !== void 0);
35
- if (units.length !== 1) {
36
- throw new LunoraError("INTERNAL", `@lunora/scheduler: interval schedule must specify exactly one of { seconds, minutes, hours }`);
37
- }
38
- const unit = units[0];
39
- const value = schedule[unit];
40
- if (unit === "seconds") {
41
- return `*/${stepField(value, "interval.seconds", 60)} * * * * *`;
42
- }
43
- if (unit === "minutes") {
44
- return `*/${stepField(value, "interval.minutes", 60)} * * * *`;
45
- }
46
- return `0 */${stepField(value, "interval.hours", 24)} * * *`;
47
- };
48
- const compileDaily = (schedule) => {
49
- const minute = field(schedule.minuteUTC, "daily.minuteUTC", 0, 59);
50
- const hour = field(schedule.hourUTC, "daily.hourUTC", 0, 23);
51
- return `${minute} ${hour} * * *`;
52
- };
53
- const compileWeekly = (schedule) => {
54
- const index = WEEKDAY_INDEX[schedule.dayOfWeek];
55
- if (index === void 0) {
56
- throw new LunoraError("INTERNAL", `@lunora/scheduler: weekly schedule has invalid dayOfWeek "${schedule.dayOfWeek}"`);
57
- }
58
- const minute = field(schedule.minuteUTC, "weekly.minuteUTC", 0, 59);
59
- const hour = field(schedule.hourUTC, "weekly.hourUTC", 0, 23);
60
- return `${minute} ${hour} * * ${index.toFixed(0)}`;
61
- };
62
- const compileMonthly = (schedule) => {
63
- const day = field(schedule.day, "monthly.day", 1, 31);
64
- const minute = field(schedule.minuteUTC, "monthly.minuteUTC", 0, 59);
65
- const hour = field(schedule.hourUTC, "monthly.hourUTC", 0, 23);
66
- return `${minute} ${hour} ${day} * *`;
67
- };
68
- const CRON_SCHEDULE_KINDS = /* @__PURE__ */ new Set(["daily", "interval", "monthly", "weekly"]);
69
- const compileCronSchedule = (kind, schedule) => {
70
- switch (kind) {
71
- case "daily": {
72
- return compileDaily(schedule);
73
- }
74
- case "interval": {
75
- return compileInterval(schedule);
76
- }
77
- case "monthly": {
78
- return compileMonthly(schedule);
79
- }
80
- case "weekly": {
81
- return compileWeekly(schedule);
82
- }
83
- default: {
84
- throw new LunoraError("INTERNAL", `@lunora/scheduler: unknown cron schedule kind "${String(kind)}"`);
85
- }
86
- }
87
- };
88
- const cronJobs = () => {
89
- const jobs = [];
90
- const seen = /* @__PURE__ */ new Set();
91
- const register = (name, cron, target, args) => {
92
- if (typeof name !== "string" || name.trim() === "") {
93
- throw new LunoraError("INTERNAL", `@lunora/scheduler: cron job name must be a non-empty string`);
94
- }
95
- if (seen.has(name)) {
96
- throw new LunoraError("INTERNAL", `@lunora/scheduler: duplicate cron job name "${name}" — names must be unique within one cronJobs()`);
97
- }
98
- if (isWorkflowReference(target)) {
99
- assertValidCronExpression(cron, `cron expression for job "${name}"`);
100
- seen.add(name);
101
- jobs.push({ args: args ?? {}, cron, name, workflow: typeof target.name === "string" ? target.name : "" });
102
- return;
103
- }
104
- if (!target || typeof target.__lunoraRef !== "string") {
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
- );
109
- }
110
- assertValidCronExpression(cron, `cron expression for job "${name}"`);
111
- seen.add(name);
112
- jobs.push({ args: args ?? {}, cron, functionPath: target.__lunoraRef, name });
113
- };
114
- const builder = {
115
- cron(name, cronExpr, function_, args) {
116
- register(name, cronExpr, function_, args);
117
- return builder;
118
- },
119
- daily(name, schedule, function_, args) {
120
- register(name, compileCronSchedule("daily", schedule), function_, args);
121
- return builder;
122
- },
123
- interval(name, schedule, function_, args) {
124
- register(name, compileCronSchedule("interval", schedule), function_, args);
125
- return builder;
126
- },
127
- jobs: () => [...jobs],
128
- monthly(name, schedule, function_, args) {
129
- register(name, compileCronSchedule("monthly", schedule), function_, args);
130
- return builder;
131
- },
132
- weekly(name, schedule, function_, args) {
133
- register(name, compileCronSchedule("weekly", schedule), function_, args);
134
- return builder;
135
- }
136
- };
137
- return builder;
138
- };
139
-
140
- export { CRON_SCHEDULE_KINDS, compileCronSchedule, cronJobs };