@lunora/scheduler 1.0.0-alpha.30 → 1.0.0-alpha.32

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.mjs CHANGED
@@ -1 +1 @@
1
- import{default as o}from"./packem_shared/createScheduler-w8KwN6aR.mjs";import{default as a}from"./packem_shared/createWorkpool-CqhYmK3r.mjs";import{createCronTrigger as c}from"./packem_shared/createCronTrigger-C4iUW_zz.mjs";import{CRON_SCHEDULE_KINDS as f,compileCronSchedule as l,cronJobs as n}from"./packem_shared/CRON_SCHEDULE_KINDS-Bi-0IEbF.mjs";import{createQueueConsumer as u,createQueueWorkpool as x,httpDispatcher as d}from"./packem_shared/createQueueConsumer-CGFKlYj_.mjs";import{SchedulerDO as C}from"./packem_shared/SchedulerDO-UqT2X44b.mjs";import{createSchedulerHost as h}from"./packem_shared/createSchedulerHost-yZW681fB.mjs";import{isWorkflowReference as E}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as k,isValidCronExpression as W,warnIfSecondsLeading as w}from"./packem_shared/assertValidCronExpression-B2lN8n3O.mjs";export{f as CRON_SCHEDULE_KINDS,C as SchedulerDO,k as assertValidCronExpression,l as compileCronSchedule,c as createCronTrigger,u as createQueueConsumer,x as createQueueWorkpool,o as createScheduler,h as createSchedulerHost,a as createWorkpool,n as cronJobs,d as httpDispatcher,W as isValidCronExpression,E as isWorkflowReference,w as warnIfSecondsLeading};
1
+ import{default as o}from"./packem_shared/createScheduler-DrXa1iXV.mjs";import{default as a}from"./packem_shared/createWorkpool-d3THj5gc.mjs";import{createCronTrigger as c}from"./packem_shared/createCronTrigger-DSuDZtqj.mjs";import{CRON_SCHEDULE_KINDS as f,compileCronSchedule as l,cronJobs as n}from"./packem_shared/CRON_SCHEDULE_KINDS-xyFk800s.mjs";import{createQueueConsumer as u,createQueueWorkpool as x,httpDispatcher as d}from"./packem_shared/createQueueConsumer-CNhmGnLG.mjs";import{SchedulerDO as C}from"./packem_shared/SchedulerDO-DE5vJpCd.mjs";import{createSchedulerHost as h}from"./packem_shared/createSchedulerHost-C4HUMyFs.mjs";import{isWorkflowReference as E}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as k,isValidCronExpression as W,warnIfSecondsLeading as w}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";export{f as CRON_SCHEDULE_KINDS,C as SchedulerDO,k as assertValidCronExpression,l as compileCronSchedule,c as createCronTrigger,u as createQueueConsumer,x as createQueueWorkpool,o as createScheduler,h as createSchedulerHost,a as createWorkpool,n as cronJobs,d as httpDispatcher,W as isValidCronExpression,E as isWorkflowReference,w as warnIfSecondsLeading};
@@ -0,0 +1 @@
1
+ import{LunoraError as l}from"@lunora/errors";import{isWorkflowReference as m}from"./isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as f}from"./assertValidCronExpression-DnBtukpq.mjs";const w={friday:5,monday:1,saturday:6,sunday:0,thursday:4,tuesday:2,wednesday:3},T=24,h=" For a daily or longer schedule use crons.daily(name, { hourUTC, minuteUTC }, …), crons.weekly(name, { dayOfWeek, hourUTC, minuteUTC }, …) or crons.monthly(name, { day, hourUTC, minuteUTC }, …).",a=(e,o,r,i)=>{if(!Number.isInteger(e)||e<r||e>i)throw new l("INTERNAL",`@lunora/scheduler: cronJobs ${o} must be an integer in [${r.toFixed(0)}, ${i.toFixed(0)}], got ${String(e)}`);return e.toFixed(0)},y=(e,o,r)=>{const i=a(e,o,1,r-1);if(r%e!==0)throw new l("INTERNAL",`@lunora/scheduler: ${o} must evenly divide ${r.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 ${r.toFixed(0)}`);return i},$=(e,o)=>{const r=["seconds","minutes","hours"].filter(s=>e[s]!==void 0);if(r.length!==1){const s=Object.entries(e).filter(([,t])=>t!==void 0).map(([t])=>t);throw new l("INTERNAL",`@lunora/scheduler: interval schedule must specify exactly one of { seconds, minutes, hours }, got { ${s.join(", ")} }.${h}`)}const i=r[0],n=e[i];if(i==="hours"&&n>=T)throw new l("INTERNAL",`@lunora/scheduler: interval.hours is capped at 23, got ${String(n)} — an interval repeats WITHIN a day rather than spanning one.${h}`);if(i==="seconds")throw new l("INTERNAL",`@lunora/scheduler: cron job${o?` "${o}"`:""} uses interval.seconds (${String(n)}) — Cloudflare Cron Triggers have a one-minute floor and cannot run sub-minute schedules; \`wrangler deploy\` would reject the resulting 6-field cron expression. Use ctx.scheduler.runAfter/runAt (optionally via a workpool for bounded concurrency) for sub-minute recurrence, or crons.interval(name, { minutes: 1 }, …) for the fastest cron-native cadence.`);return i==="minutes"?`*/${y(n,"interval.minutes",60)} * * * *`:`0 */${y(n,"interval.hours",24)} * * *`},p=e=>`${a(e.minuteUTC,"hourly.minuteUTC",0,59)} * * * *`,C=e=>{const o=a(e.minuteUTC,"daily.minuteUTC",0,59),r=a(e.hourUTC,"daily.hourUTC",0,23);return`${o} ${r} * * *`},N=e=>{const o=w[e.dayOfWeek];if(o===void 0)throw new l("INTERNAL",`@lunora/scheduler: weekly schedule has invalid dayOfWeek "${e.dayOfWeek}"`);const r=a(e.minuteUTC,"weekly.minuteUTC",0,59),i=a(e.hourUTC,"weekly.hourUTC",0,23);return`${r} ${i} * * ${o.toFixed(0)}`},U=e=>{const o=a(e.day,"monthly.day",1,31),r=a(e.minuteUTC,"monthly.minuteUTC",0,59),i=a(e.hourUTC,"monthly.hourUTC",0,23);return`${r} ${i} ${o} * *`},b=new Set(["daily","hourly","interval","monthly","weekly"]),c=(e,o,r)=>{switch(e){case"daily":return C(o);case"hourly":return p(o);case"interval":return $(o,r);case"monthly":return U(o);case"weekly":return N(o);default:throw new l("INTERNAL",`@lunora/scheduler: unknown cron schedule kind "${String(e)}"`)}},k=()=>{const e=[],o=new Set,r=(n,s,t,u)=>{if(typeof n!="string"||n.trim()==="")throw new l("INTERNAL","@lunora/scheduler: cron job name must be a non-empty string");if(o.has(n))throw new l("INTERNAL",`@lunora/scheduler: duplicate cron job name "${n}" — names must be unique within one cronJobs()`);let d;if(m(t))d={workflow:typeof t.name=="string"?t.name:""};else if(t&&typeof t.__lunoraRef=="string")d={functionPath:t.__lunoraRef};else throw new l("INTERNAL",`@lunora/scheduler: cron job "${n}" requires a function reference (e.g. internal.email.digest) or a workflow reference`);f(s,`cron expression for job "${n}"`),o.add(n),e.push({args:u??{},cron:s,name:n,...d})},i={cron(n,s,t,u){return r(n,s,t,u),i},daily(n,s,t,u){return r(n,c("daily",s),t,u),i},hourly(n,s,t,u){return r(n,c("hourly",s),t,u),i},interval(n,s,t,u){return r(n,c("interval",s,n),t,u),i},jobs:()=>[...e],monthly(n,s,t,u){return r(n,c("monthly",s),t,u),i},weekly(n,s,t,u){return r(n,c("weekly",s),t,u),i}};return i};export{b as CRON_SCHEDULE_KINDS,c as compileCronSchedule,k as cronJobs};
@@ -1 +1 @@
1
- const f=(u,t=200,e)=>Response.json(u,{headers:{"content-type":"application/json",...e},status:t}),b="lunora-ping",I="lunora-pong";const m="retry:",g="dead:",p="pool:";const y=u=>String(u).padStart(15,"0"),w=u=>{let t="";for(const e of u)t+=String.fromCodePoint(e);return btoa(t).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},v=()=>w(crypto.getRandomValues(new Uint8Array(12)));class r{static indexKey(t,e){return`t:${y(t)}:${e}`}static json(t,e=200){return f(t,e)}static error(t,e,s){return r.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:5,a=typeof e?.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0?e.baseMs:3e4,n=e?.backoff==="linear"?"linear":"exponential",i=typeof e?.maxMs=="number"&&Number.isFinite(e.maxMs)&&e.maxMs>=0?e.maxMs:void 0;return{backoff:n,baseMs:a,maxAttempts:s,maxMs:i}}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;constructor(t,e){this.state=t,this.env=e,this.armWebSocketKeepalive()}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 f({error:{code:"NOT_FOUND"}},404)}async alarm(){const t=Date.now(),e=[],s=await this.state.storage.list({end:`t:${y(t)}:~`,limit:100,prefix:"t:"});for(const[a,n]of s.entries()){const i=Number.parseInt(a.slice(2,a.indexOf(":",2)),10);if(Number.isFinite(i)&&i<=t){const o=await this.state.storage.get(`id:${n}`);o?e.push(o):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(b,I))}async drainRecordGuarded(t){try{await this.state.storage.delete(r.indexKey(t.scheduledFor,t.id)),await this.drainRecord(t)}catch{try{await this.state.storage.put(r.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 s=await this.loadPool(t.pool),a=r.releaseSlot(s,t.id);await this.savePool(t.pool,a)}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 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 r.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.listRecords();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.listRecords(),a=JSON.stringify({records:e,truncated:s,type:"jobs"});for(const n of t)try{n.send(a)}catch{}}async listRecords(t=100){const e=[...(await this.state.storage.list({limit:t+1,prefix:"id:"})).values()],s=e.length>t;return{records:s?e.slice(0,t):e,truncated:s}}async countHeaders(t,e=100){let s;for(;;){const a=await this.state.storage.list(s===void 0?{limit:e,prefix:"id:"}:{limit:e,prefix:"id:",startAfter:s});if(a.size===0)break;for(const n of a.values())t(n);if(s=[...a.keys()].at(-1),a.size<e)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 w(new Uint8Array(n))}async recordRetry(t){const e=(t.attempts??0)+1,{backoff:s,baseMs:a,maxAttempts:n,maxMs:i}=r.resolveRetry(t);if(e>n){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=s==="linear"?a*e:a*2**(e-1),l=i===void 0?o:Math.min(o,i),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(r.indexKey(c,t.id),t.id)}async loadPool(t,e){const s=await this.state.storage.get(`${p}${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:r.normalizeConcurrency(e,1)}}async savePool(t,e){await this.state.storage.put(`${p}${t}`,e)}async requeuePooled(t){const e=Date.now()+1e3,s={...t,scheduledFor:e};await this.state.storage.put(`id:${t.id}`,s),await this.state.storage.put(r.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 r.error(400,"INVALID_INPUT","pool is required");const n=await this.loadPool(s),i=a===void 0?r.releaseFirstSlot(n):r.releaseSlot(n,a);return await this.savePool(s,i),await this.armAlarmIfEarlier(Date.now()),r.json({inFlight:i.inFlight})}async handlePoolStatus(t){const e=t.searchParams.get("name");if(e===null||e.length===0)return r.error(400,"INVALID_INPUT","name is required");const s=await this.loadPool(e);let a=0;return await this.countHeaders(n=>{n.pool===e&&(a+=1)}),r.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const t=await this.state.storage.list({prefix:p}),e=new Map;await this.countHeaders(o=>{o.pool!==void 0&&e.set(o.pool,(e.get(o.pool)??0)+1)});const s=[];let a=0,n=0;for(const[o,l]of t.entries()){const c=o.slice(p.length),d=Math.max(0,l.inFlight),h=e.get(c)??0;s.push({inFlight:d,maxConcurrency:l.maxConcurrency,name:c,queued:h}),a+=h,n+=d}const i={backlog:a,inFlight:n,pools:s};return r.json(i)}async handleSchedule(t){const e=await t.json().catch(()=>{}),s=r.resolveScheduleTarget(e);if(!e||s===void 0)return r.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:a,workflow:n}=s;if(typeof e.scheduledFor!="number"||!Number.isInteger(e.scheduledFor)||e.scheduledFor<=0||e.scheduledFor>999999999999999)return r.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 r.error(500,"ORIGIN_NOT_CONFIGURED","LUNORA_ORIGIN_URL env binding must be set on the SchedulerDO");const i=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=r.normalizeRetry(e.retry),c=v(),d={args:e.args??{},enqueuedAt:Date.now(),id:c,...a===void 0?{}:{functionPath:a},...o===void 0?{}:{instanceName:o},...i===void 0?{}:{pool:i},...l===void 0?{}:{retry:l},scheduledFor:e.scheduledFor,shardKey:e.shardKey,...n===void 0?{}:{workflow:n}};if(i!==void 0){const h=await this.loadPool(i,e.maxConcurrency);await this.savePool(i,{inFlight:h.inFlight,...h.inFlightIds===void 0?{}:{inFlightIds:h.inFlightIds},maxConcurrency:r.normalizeConcurrency(e.maxConcurrency,h.maxConcurrency)})}return await this.state.storage.put(`id:${c}`,d),await this.state.storage.put(r.indexKey(d.scheduledFor,c),c),await this.armAlarmIfEarlier(d.scheduledFor),await this.broadcastChange(),r.json({id:c,scheduledFor:d.scheduledFor})}async handleCancel(t){const e=await t.json().catch(()=>{});if(!e?.id)return r.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${e.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),r.json({cancelled:!0})):r.json({cancelled:!1})}async handleList(){const{records:t,truncated:e}=await this.listRecords();return r.json({records:t,truncated:e})}async handleDeadList(){const t=await this.state.storage.list({prefix:g});return r.json({records:[...t.values()]})}async handleDeadRetry(t){const e=await t.json().catch(()=>{});if(typeof e?.id!="string"||e.id.length===0)return r.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${g}${e.id}`);if(s===void 0)return r.json({retried:!1});const a=Date.now(),n={...s,attempts:0,scheduledFor:a};return await this.state.storage.put(`id:${s.id}`,n),await this.state.storage.put(r.indexKey(a,s.id),s.id),await this.state.storage.delete(`${g}${s.id}`),await this.armAlarmIfEarlier(a),await this.broadcastChange(),r.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 r.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.delete(`${g}${e.id}`);return r.json({removed:!!s})}async handleGet(t){const e=t.searchParams.get("id");if(e===null||e.length===0)return r.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${e}`);return r.json(s===void 0?{}:{record:s})}async removeRecord(t){await this.state.storage.delete([`id:${t.id}`,r.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,s=Number.parseInt(e.slice(2,e.indexOf(":",2)),10);Number.isFinite(s)&&await this.state.storage.setAlarm(s)}}export{r as SchedulerDO};
1
+ const m=(u,t=200,e)=>Response.json(u,{headers:{"content-type":"application/json",...e},status:t}),R="lunora-ping",I="lunora-pong";const f="retry:",g="dead:",p="pool:";const y=u=>String(u).padStart(15,"0"),w=u=>{let t="";for(const e of u)t+=String.fromCodePoint(e);return btoa(t).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},E=()=>w(crypto.getRandomValues(new Uint8Array(12)));class n{static indexKey(t,e){return`t:${y(t)}:${e}`}static json(t,e=200){return m(t,e)}static error(t,e,s){return n.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:5,a=typeof e?.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0?e.baseMs:3e4,i=e?.backoff==="linear"?"linear":"exponential",o=typeof e?.maxMs=="number"&&Number.isFinite(e.maxMs)&&e.maxMs>=0?e.maxMs:void 0;return{backoff:i,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;constructor(t,e){this.state=t,this.env=e,this.armWebSocketKeepalive()}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 m({error:{code:"NOT_FOUND"}},404)}async alarm(){const t=Date.now(),e=[],s=await this.state.storage.list({end:`t:${y(t)}:~`,limit:100,prefix:"t:"});for(const[a,i]of s.entries()){const o=Number.parseInt(a.slice(2,a.indexOf(":",2)),10);if(Number.isFinite(o)&&o<=t){const r=await this.state.storage.get(`id:${i}`);r?e.push(r):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"},i=await this.signDispatch(s);return i!==void 0?a["x-lunora-scheduler-signature"]=i: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(R,I))}async drainRecordGuarded(t){try{await this.state.storage.delete(n.indexKey(t.scheduledFor,t.id)),await this.drainRecord(t)}catch{try{await this.state.storage.put(n.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),i=n.releaseSlot(a,t.id);await this.savePool(t.pool,i)}if(s){try{await this.state.storage.delete([`id:${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 n.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.listRecords();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.listRecords(),a=JSON.stringify({records:e,truncated:s,type:"jobs"});for(const i of t)try{i.send(a)}catch{}}async listRecords(t=100){const s=[...(await this.state.storage.list({limit:t+1,prefix:"id:"})).values()],a=s.length>t;return{records:a?s.slice(0,t):s,truncated:a}}async countHeaders(t,e=100){let s;for(;;){const a=await this.state.storage.list(s===void 0?{limit:e,prefix:"id:"}:{limit:e,prefix:"id:",startAfter:s});if(a.size===0)break;for(const o of a.values())t(o);if(s=[...a.keys()].at(-1),a.size<e)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"]),i=await crypto.subtle.sign("HMAC",a,s.encode(t));return w(new Uint8Array(i))}async recordRetry(t){const e=(t.attempts??0)+1,{backoff:s,baseMs:a,maxAttempts:i,maxMs:o}=n.resolveRetry(t);if(e>i){await this.state.storage.put(`${g}${t.id}`,{...t,attempts:e}),await this.state.storage.delete([`${f}${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 r=s==="linear"?a*e:a*2**(e-1),d=o===void 0?r:Math.min(r,o),c=Date.now()+d,l={...t,attempts:e,scheduledFor:c};await this.state.storage.put(`${f}${t.id}`,l),await this.state.storage.put(`id:${t.id}`,l),await this.state.storage.put(n.indexKey(c,t.id),t.id)}async loadPool(t,e){const s=await this.state.storage.get(`${p}${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:n.normalizeConcurrency(e,1)}}async savePool(t,e){await this.state.storage.put(`${p}${t}`,e)}async requeuePooled(t){const e=Date.now()+1e3,s={...t,scheduledFor:e};await this.state.storage.put(`id:${t.id}`,s),await this.state.storage.put(n.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 n.error(400,"INVALID_INPUT","pool is required");const i=await this.loadPool(s),o=a===void 0?n.releaseFirstSlot(i):n.releaseSlot(i,a);return await this.savePool(s,o),await this.armAlarmIfEarlier(Date.now()),n.json({inFlight:o.inFlight})}async handlePoolStatus(t){const e=t.searchParams.get("name");if(e===null||e.length===0)return n.error(400,"INVALID_INPUT","name is required");const s=await this.loadPool(e);let a=0;return await this.countHeaders(i=>{i.pool===e&&(a+=1)}),n.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const t=await this.state.storage.list({prefix:p}),e=new Map;await this.countHeaders(r=>{r.pool!==void 0&&e.set(r.pool,(e.get(r.pool)??0)+1)});const s=[];let a=0,i=0;for(const[r,d]of t.entries()){const c=r.slice(p.length),l=Math.max(0,d.inFlight),h=e.get(c)??0;s.push({inFlight:l,maxConcurrency:d.maxConcurrency,name:c,queued:h}),a+=h,i+=l}const o={backlog:a,inFlight:i,pools:s};return n.json(o)}async handleSchedule(t){const e=await t.json().catch(()=>{}),s=n.resolveScheduleTarget(e);if(!e||s===void 0)return n.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:a,workflow:i}=s;if(typeof e.scheduledFor!="number"||!Number.isInteger(e.scheduledFor)||e.scheduledFor<=0||e.scheduledFor>999999999999999)return n.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 n.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,r=typeof e.instanceName=="string"&&e.instanceName.length>0?e.instanceName:void 0,d=n.normalizeRetry(e.retry),c=E(),l={args:e.args??{},enqueuedAt:Date.now(),id:c,...a===void 0?{}:{functionPath:a},...r===void 0?{}:{instanceName:r},...o===void 0?{}:{pool:o},...d===void 0?{}:{retry:d},scheduledFor:e.scheduledFor,shardKey:e.shardKey,...i===void 0?{}:{workflow:i}};if(o!==void 0){const h=await this.loadPool(o,e.maxConcurrency);await this.savePool(o,{inFlight:h.inFlight,...h.inFlightIds===void 0?{}:{inFlightIds:h.inFlightIds},maxConcurrency:n.normalizeConcurrency(e.maxConcurrency,h.maxConcurrency)})}return await this.state.storage.put(`id:${c}`,l),await this.state.storage.put(n.indexKey(l.scheduledFor,c),c),await this.armAlarmIfEarlier(l.scheduledFor),await this.broadcastChange(),n.json({id:c,scheduledFor:l.scheduledFor})}async handleCancel(t){const e=await t.json().catch(()=>{});if(!e?.id)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${e.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),n.json({cancelled:!0})):n.json({cancelled:!1})}async handleList(){const{records:t,truncated:e}=await this.listRecords();return n.json({records:t,truncated:e})}async handleDeadList(){const t=await this.state.storage.list({prefix:g});return n.json({records:[...t.values()]})}async handleDeadRetry(t){const e=await t.json().catch(()=>{});if(typeof e?.id!="string"||e.id.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${g}${e.id}`);if(s===void 0)return n.json({retried:!1});const a=Date.now(),i={...s,attempts:0,scheduledFor:a};return await this.state.storage.put(`id:${s.id}`,i),await this.state.storage.put(n.indexKey(a,s.id),s.id),await this.state.storage.delete(`${g}${s.id}`),await this.armAlarmIfEarlier(a),await this.broadcastChange(),n.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 n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.delete(`${g}${e.id}`);return n.json({removed:!!s})}async handleGet(t){const e=t.searchParams.get("id");if(e===null||e.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${e}`);return n.json(s===void 0?{}:{record:s})}async removeRecord(t){await this.state.storage.delete([`id:${t.id}`,n.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 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{n as SchedulerDO};
@@ -1 +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}},t=/\s+/u,i=(r,e)=>{r.trim().split(t).length===6&&console.warn(`@lunora/scheduler: ${e} "${r}" is a 6-field (seconds-leading) cron expression — Cloudflare Cron Triggers only support the standard 5-field form and will reject this at \`wrangler deploy\`. Drop the seconds field, or use ctx.scheduler.runAfter/runAt for sub-minute work.`)},d=(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 * * * *")`);i(r,e)};export{d as assertValidCronExpression,n as isValidCronExpression,i as warnIfSecondsLeading};
1
+ import{LunoraError as n}from"@lunora/errors";import{CronExpressionParser as o}from"cron-parser";const s=r=>{if(typeof r!="string"||r.trim()==="")return!1;try{return o.parse(r.trim()),!0}catch{return!1}},t=/\s+/u,i=(r,e)=>{r.trim().split(t).length===6&&console.warn(`@lunora/scheduler: ${e} "${r}" is a 6-field (seconds-leading) cron expression — Cloudflare Cron Triggers only support the standard 5-field form and will reject this at \`wrangler deploy\`. Drop the seconds field, or use ctx.scheduler.runAfter/runAt for sub-minute work.`)},d=(r,e="cron expression")=>{if(!s(r))throw new n("INTERNAL",`@lunora/scheduler: invalid ${e} "${r}" — expected a standard 5- or 6-field cron expression (e.g. "0 * * * *")`);i(r,e)};export{d as assertValidCronExpression,s as isValidCronExpression,i as warnIfSecondsLeading};
@@ -0,0 +1 @@
1
+ import{LunoraError as n}from"@lunora/errors";import{assertValidCronExpression as s}from"./assertValidCronExpression-DnBtukpq.mjs";const u=r=>{if(!r.schedule||!r.fn)throw new n("INTERNAL","@lunora/scheduler: createCronTrigger() requires `schedule` and `fn`");s(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{u as createCronTrigger};
@@ -0,0 +1 @@
1
+ import{LunoraError as c}from"@lunora/errors";const u=100,h=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)},f=e=>{if(!e.queue)throw new c("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(n,r,o={})=>{const s={args:r,functionPath:n.__lunoraRef,shardKey:o.shardKey},i=o.delaySeconds===void 0?void 0:{delaySeconds:o.delaySeconds};await e.queue.send(s,i)},enqueueBatch:async(n,r)=>{if(n.length>u)throw new c("VALIDATION_ERROR",`@lunora/scheduler: enqueueBatch exceeds ${String(u)} (got ${String(n.length)}) — split across calls`);const o=n.map(s=>({body:{args:s.args,functionPath:s.ref.__lunoraRef,shardKey:s.shardKey}}));await e.queue.sendBatch(o,r)}}},l=e=>typeof e=="object"&&e!==null&&typeof e.functionPath=="string",y=e=>async t=>{await Promise.all(t.messages.map(async a=>{try{if(!l(a.body))throw new c("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await e.dispatch(a.body),a.ack()}catch{a.retry()}}))},p=e=>{const t=e.fetchImpl??globalThis.fetch;if(typeof t!="function")throw new TypeError("@lunora/scheduler: no fetch implementation available — pass fetchImpl or run on a platform with global fetch");const a=`${h(e.originUrl)}/_lunora/scheduler/dispatch`;return async n=>{const r=await t(a,{body:JSON.stringify({args:n.args??{},functionPath:n.functionPath,shardKey:n.shardKey}),headers:{authorization:`Bearer ${e.adminToken}`,"content-type":"application/json"},method:"POST"});if(!r.ok)throw new c("INTERNAL",`@lunora/scheduler: queue dispatch failed (${r.status.toString()}): ${await r.text()}`)}};export{y as createQueueConsumer,f as createQueueWorkpool,p as httpDispatcher};
@@ -0,0 +1 @@
1
+ import{LunoraError as i}from"@lunora/errors";import{a as y,c as s,g as a}from"./do-client-BKEFA9pM.mjs";import{isWorkflowReference as f}from"./isWorkflowReference-CT3tdefh.mjs";const D=n=>{y(n);const o=async(e,r,c,t={})=>{const l=e instanceof Date?e.getTime():e,d={args:c,originUrl:n.originUrl,pool:t.pool,retry:t.retry,scheduledFor:l,shardKey:t.shardKey};if(f(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 s(n,"/schedule",{...d,workflow:r.binding})}const u=typeof r=="string"?r:r.__lunoraRef;return s(n,"/schedule",{...d,functionPath:u})};return{cancel:async e=>s(n,"/cancel",{id:e}),dead:async()=>{const e=await a(n,"/dead");return Array.isArray(e.records)?e.records:[]},deadRetry:async e=>{const{retried:r}=await s(n,"/dead/retry",{id:e});return r===!0},get:async e=>(await a(n,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>{const e=await a(n,"/list");return Array.isArray(e.records)?e.records:[]},runAfter:async(e,r,c,t={})=>{if(!Number.isFinite(e)||e<0)throw new i("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return o(Date.now()+e,r,c,t)},runAt:o}};export{D as default};
@@ -0,0 +1 @@
1
+ import d from"./createScheduler-DrXa1iXV.mjs";const u=t=>t?.at!==void 0?typeof t.at=="number"?t.at:t.at.getTime():Date.now()+(t?.delayMs??0),l=t=>{const a=d({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 s=a.runAt;return s(u(c),e,r,{retry:c?.retry,shardKey:c?.shardKey})}}};export{l as createSchedulerHost};
@@ -0,0 +1 @@
1
+ import{LunoraError as u}from"@lunora/errors";import{a as s,g as m,c as t}from"./do-client-BKEFA9pM.mjs";const i=e=>{if(s(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new u("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=>t(e,"/cancel",{id:a}),enqueue:async(a,l,n={})=>{const c=n.delayMs??0;if(!Number.isFinite(c)||c<0)throw new u("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return t(e,"/schedule",{args:l,functionPath:a.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,originUrl:e.originUrl,pool:r,retry:n.retry,scheduledFor:Date.now()+c,shardKey:n.shardKey})},name:r,status:async()=>m(e,`/pool?name=${encodeURIComponent(r)}`)}};export{i as default};
@@ -0,0 +1 @@
1
+ import{LunoraError as o}from"@lunora/errors";const i=(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)},l=e=>{if(!e.namespace)throw new o("INTERNAL","@lunora/scheduler: `namespace` (SchedulerDO binding) is required");if(!e.originUrl)throw new o("INTERNAL","@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker")},c=e=>{const r=i(e.namespace,e.jurisdiction);return r.get(r.idFromName(e.instanceName??"default"))},s=async(e,r,n)=>{const t=await c(e).fetch(`https://scheduler.internal${r}`,n);if(!t.ok){const a=await t.text();throw new o("INTERNAL",`@lunora/scheduler: SchedulerDO ${r} failed (${String(t.status)}): ${a}`)}return await t.json()},h=async(e,r,n)=>s(e,r,{body:JSON.stringify(n),headers:{"content-type":"application/json"},method:"POST"}),p=async(e,r)=>s(e,r,{method:"GET"});export{l as a,h as c,p as g};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "1.0.0-alpha.30",
3
+ "version": "1.0.0-alpha.32",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,9 +46,9 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.21",
50
- "@lunora/platform": "1.0.0-alpha.10",
51
- "cron-parser": "5.6.2"
49
+ "@lunora/errors": "1.0.0-alpha.22",
50
+ "@lunora/platform": "1.0.0-alpha.12",
51
+ "cron-parser": "5.8.1"
52
52
  },
53
53
  "engines": {
54
54
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
1
- import{LunoraError as a}from"@lunora/errors";import{isWorkflowReference as m}from"./isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as f}from"./assertValidCronExpression-B2lN8n3O.mjs";const w={friday:5,monday:1,saturday:6,sunday:0,thursday:4,tuesday:2,wednesday:3},$=24,h=" For a daily or longer schedule use crons.daily(name, { hourUTC, minuteUTC }, …), crons.weekly(name, { dayOfWeek, hourUTC, minuteUTC }, …) or crons.monthly(name, { day, hourUTC, minuteUTC }, …).",l=(e,o,n,i)=>{if(!Number.isInteger(e)||e<n||e>i)throw new a("INTERNAL",`@lunora/scheduler: cronJobs ${o} must be an integer in [${n.toFixed(0)}, ${i.toFixed(0)}], got ${String(e)}`);return e.toFixed(0)},y=(e,o,n)=>{const i=l(e,o,1,n-1);if(n%e!==0)throw new a("INTERNAL",`@lunora/scheduler: ${o} 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 i},T=(e,o)=>{const n=["seconds","minutes","hours"].filter(s=>e[s]!==void 0);if(n.length!==1){const s=Object.entries(e).filter(([,t])=>t!==void 0).map(([t])=>t);throw new a("INTERNAL",`@lunora/scheduler: interval schedule must specify exactly one of { seconds, minutes, hours }, got { ${s.join(", ")} }.${h}`)}const i=n[0],r=e[i];if(i==="hours"&&r>=$)throw new a("INTERNAL",`@lunora/scheduler: interval.hours is capped at 23, got ${String(r)} — an interval repeats WITHIN a day rather than spanning one.${h}`);if(i==="seconds")throw new a("INTERNAL",`@lunora/scheduler: cron job${o?` "${o}"`:""} uses interval.seconds (${String(r)}) — Cloudflare Cron Triggers have a one-minute floor and cannot run sub-minute schedules; \`wrangler deploy\` would reject the resulting 6-field cron expression. Use ctx.scheduler.runAfter/runAt (optionally via a workpool for bounded concurrency) for sub-minute recurrence, or crons.interval(name, { minutes: 1 }, …) for the fastest cron-native cadence.`);return i==="minutes"?`*/${y(r,"interval.minutes",60)} * * * *`:`0 */${y(r,"interval.hours",24)} * * *`},C=e=>`${l(e.minuteUTC,"hourly.minuteUTC",0,59)} * * * *`,p=e=>{const o=l(e.minuteUTC,"daily.minuteUTC",0,59),n=l(e.hourUTC,"daily.hourUTC",0,23);return`${o} ${n} * * *`},v=e=>{const o=w[e.dayOfWeek];if(o===void 0)throw new a("INTERNAL",`@lunora/scheduler: weekly schedule has invalid dayOfWeek "${e.dayOfWeek}"`);const n=l(e.minuteUTC,"weekly.minuteUTC",0,59),i=l(e.hourUTC,"weekly.hourUTC",0,23);return`${n} ${i} * * ${o.toFixed(0)}`},N=e=>{const o=l(e.day,"monthly.day",1,31),n=l(e.minuteUTC,"monthly.minuteUTC",0,59),i=l(e.hourUTC,"monthly.hourUTC",0,23);return`${n} ${i} ${o} * *`},k=new Set(["daily","hourly","interval","monthly","weekly"]),d=(e,o,n)=>{switch(e){case"daily":return p(o);case"hourly":return C(o);case"interval":return T(o,n);case"monthly":return N(o);case"weekly":return v(o);default:throw new a("INTERNAL",`@lunora/scheduler: unknown cron schedule kind "${String(e)}"`)}},x=()=>{const e=[],o=new Set,n=(r,s,t,u)=>{if(typeof r!="string"||r.trim()==="")throw new a("INTERNAL","@lunora/scheduler: cron job name must be a non-empty string");if(o.has(r))throw new a("INTERNAL",`@lunora/scheduler: duplicate cron job name "${r}" — names must be unique within one cronJobs()`);let c;if(m(t))c={workflow:typeof t.name=="string"?t.name:""};else if(t&&typeof t.__lunoraRef=="string")c={functionPath:t.__lunoraRef};else throw new a("INTERNAL",`@lunora/scheduler: cron job "${r}" requires a function reference (e.g. internal.email.digest) or a workflow reference`);f(s,`cron expression for job "${r}"`),o.add(r),e.push({args:u??{},cron:s,name:r,...c})},i={cron(r,s,t,u){return n(r,s,t,u),i},daily(r,s,t,u){return n(r,d("daily",s),t,u),i},hourly(r,s,t,u){return n(r,d("hourly",s),t,u),i},interval(r,s,t,u){return n(r,d("interval",s,r),t,u),i},jobs:()=>[...e],monthly(r,s,t,u){return n(r,d("monthly",s),t,u),i},weekly(r,s,t,u){return n(r,d("weekly",s),t,u),i}};return i};export{k as CRON_SCHEDULE_KINDS,d as compileCronSchedule,x as cronJobs};
@@ -1 +0,0 @@
1
- import{LunoraError as s}from"@lunora/errors";import{assertValidCronExpression as n}from"./assertValidCronExpression-B2lN8n3O.mjs";const t=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{t as createCronTrigger};
@@ -1 +0,0 @@
1
- import{LunoraError as o}from"@lunora/errors";const s=100,i=e=>{let a=e.length;for(;a>0&&e[a-1]==="/";)a-=1;return e.slice(0,a)},l=e=>{if(!e.queue)throw new o("INTERNAL","@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");return{enqueue:async(a,t,n={})=>{const r={args:t,functionPath:a.__lunoraRef,shardKey:n.shardKey},u=n.delaySeconds===void 0?void 0:{delaySeconds:n.delaySeconds};await e.queue.send(r,u)},enqueueBatch:async(a,t)=>{if(a.length>s)throw new o("VALIDATION_ERROR",`@lunora/scheduler: enqueueBatch exceeds ${String(s)} (got ${String(a.length)}) — split across calls`);const n=a.map(r=>({body:{args:r.args,functionPath:r.ref.__lunoraRef,shardKey:r.shardKey}}));await e.queue.sendBatch(n,t)}}},c=e=>typeof e=="object"&&e!==null&&typeof e.functionPath=="string",d=e=>async a=>{await Promise.all(a.messages.map(async t=>{try{if(!c(t.body))throw new o("INTERNAL","@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");await e.dispatch(t.body),t.ack()}catch{t.retry()}}))},f=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=`${i(e.originUrl)}/_lunora/scheduler/dispatch`;return async n=>{const r=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(!r.ok)throw new o("INTERNAL",`@lunora/scheduler: queue dispatch failed (${r.status.toString()}): ${await r.text()}`)}};export{d as createQueueConsumer,l as createQueueWorkpool,f as httpDispatcher};
@@ -1 +0,0 @@
1
- import{LunoraError as c}from"@lunora/errors";import{u as f,l as i,d as a}from"./do-client-DvdRxZzc.mjs";import{isWorkflowReference as y}from"./isWorkflowReference-CT3tdefh.mjs";const h=t=>{f(t);const s=async(e,r,o,n={})=>{const l=e instanceof Date?e.getTime():e,d={args:o,originUrl:t.originUrl,pool:n.pool,retry:n.retry,scheduledFor:l,shardKey:n.shardKey};if(y(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new c("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return a(t,"/schedule",{...d,workflow:r.binding})}const u=typeof r=="string"?r:r.__lunoraRef;return a(t,"/schedule",{...d,functionPath:u})};return{cancel:async e=>a(t,"/cancel",{id:e}),dead:async()=>{const e=await i(t,"/dead");return Array.isArray(e.records)?e.records:[]},deadRetry:async e=>{const{retried:r}=await a(t,"/dead/retry",{id:e});return r===!0},get:async e=>(await i(t,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>{const e=await i(t,"/list");return Array.isArray(e.records)?e.records:[]},runAfter:async(e,r,o,n={})=>{if(!Number.isFinite(e)||e<0)throw new c("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return s(Date.now()+e,r,o,n)},runAt:s}};export{h as default};
@@ -1 +0,0 @@
1
- import i from"./createScheduler-w8KwN6aR.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};
@@ -1 +0,0 @@
1
- import{LunoraError as t}from"@lunora/errors";import{u,l as i,d as c}from"./do-client-DvdRxZzc.mjs";const d=n=>{if(u(n),!Number.isInteger(n.maxConcurrency)||n.maxConcurrency<=0)throw new t("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const e=typeof n.name=="string"&&n.name.length>0?n.name:"default";return{cancel:async r=>c(n,"/cancel",{id:r}),enqueue:async(r,s,a={})=>{const o=a.delayMs??0;if(!Number.isFinite(o)||o<0)throw new t("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return c(n,"/schedule",{args:s,functionPath:r.__lunoraRef,instanceName:n.instanceName??"default",maxConcurrency:n.maxConcurrency,originUrl:n.originUrl,pool:e,retry:a.retry,scheduledFor:Date.now()+o,shardKey:a.shardKey})},name:e,status:async()=>i(n,`/pool?name=${encodeURIComponent(e)}`)}};export{d as default};
@@ -1 +0,0 @@
1
- import{LunoraError as n}from"@lunora/errors";const s=(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 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")},c=e=>{const r=s(e.namespace,e.jurisdiction);return r.get(r.idFromName(e.instanceName??"default"))},i=async(e,r,o)=>{const t=await c(e).fetch(`https://scheduler.internal${r}`,o);if(!t.ok){const a=await t.text();throw new n("INTERNAL",`@lunora/scheduler: SchedulerDO ${r} failed (${String(t.status)}): ${a}`)}return await t.json()},l=async(e,r,o)=>i(e,r,{body:JSON.stringify(o),headers:{"content-type":"application/json"},method:"POST"}),h=async(e,r)=>i(e,r,{method:"GET"});export{l as d,h as l,d as u};