@lunora/scheduler 1.0.0-alpha.124 → 1.0.0-alpha.125

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-CL46fdGy.mjs";import{default as a}from"./packem_shared/createWorkpool-DRPzIlo3.mjs";import{createCronTrigger as f}from"./packem_shared/createCronTrigger-DSuDZtqj.mjs";import{CRON_SCHEDULE_KINDS as p,compileCronSchedule as c,cronJobs as d}from"./packem_shared/CRON_SCHEDULE_KINDS-xyFk800s.mjs";import{createQueueConsumer as m,createQueueWorkpool as n,httpDispatcher as x}from"./packem_shared/createQueueConsumer-B-2nHgJ1.mjs";import{default as i}from"./packem_shared/resolveScheduleId-DuLCFI_l.mjs";import{MAX_RETRY_ATTEMPTS as E,RETRY_BASE_DELAY_MS as C,SchedulerDO as _}from"./packem_shared/MAX_RETRY_ATTEMPTS-Q3JXDtb1.mjs";import{createSchedulerHost as R}from"./packem_shared/createSchedulerHost-PFABEAa3.mjs";import{isWorkflowReference as A}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as g,isValidCronExpression as k,warnIfSecondsLeading as L}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";import{default as W}from"./packem_shared/assertScheduleDelay-Cz5e-UVl.mjs";import{default as w}from"./packem_shared/assertScheduleInstant-CMN9XCFm.mjs";export{p as CRON_SCHEDULE_KINDS,E as MAX_RETRY_ATTEMPTS,C as RETRY_BASE_DELAY_MS,_ as SchedulerDO,W as assertScheduleDelay,w as assertScheduleInstant,g as assertValidCronExpression,c as compileCronSchedule,f as createCronTrigger,m as createQueueConsumer,n as createQueueWorkpool,o as createScheduler,R as createSchedulerHost,a as createWorkpool,d as cronJobs,x as httpDispatcher,k as isValidCronExpression,A as isWorkflowReference,i as resolveScheduleId,L as warnIfSecondsLeading};
1
+ import{default as o}from"./packem_shared/createScheduler-DpLHXye6.mjs";import{default as a}from"./packem_shared/createWorkpool-BtS_ttVJ.mjs";import{createCronTrigger as f}from"./packem_shared/createCronTrigger-DSuDZtqj.mjs";import{CRON_SCHEDULE_KINDS as p,compileCronSchedule as c,cronJobs as d}from"./packem_shared/CRON_SCHEDULE_KINDS-xyFk800s.mjs";import{createQueueConsumer as m,createQueueWorkpool as n,httpDispatcher as x}from"./packem_shared/createQueueConsumer-B-2nHgJ1.mjs";import{default as i}from"./packem_shared/resolveScheduleId-DuLCFI_l.mjs";import{MAX_RETRY_ATTEMPTS as E,RETRY_BASE_DELAY_MS as C,SchedulerDO as _}from"./packem_shared/MAX_RETRY_ATTEMPTS-BnrPhYes.mjs";import{createSchedulerHost as R}from"./packem_shared/createSchedulerHost-cdArZpNX.mjs";import{isWorkflowReference as A}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as g,isValidCronExpression as k,warnIfSecondsLeading as L}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";import{default as W}from"./packem_shared/assertScheduleDelay-CVTqeF04.mjs";import{default as w}from"./packem_shared/assertScheduleInstant-DCqHdiVt.mjs";export{p as CRON_SCHEDULE_KINDS,E as MAX_RETRY_ATTEMPTS,C as RETRY_BASE_DELAY_MS,_ as SchedulerDO,W as assertScheduleDelay,w as assertScheduleInstant,g as assertValidCronExpression,c as compileCronSchedule,f as createCronTrigger,m as createQueueConsumer,n as createQueueWorkpool,o as createScheduler,R as createSchedulerHost,a as createWorkpool,d as cronJobs,x as httpDispatcher,k as isValidCronExpression,A as isWorkflowReference,i as resolveScheduleId,L as warnIfSecondsLeading};
@@ -0,0 +1 @@
1
+ import{a as N}from"./base64-junLSktH.mjs";import R from"./resolveScheduleId-DuLCFI_l.mjs";const x="DISPATCH_IN_PROGRESS",E="x-lunora-dispatch-declined",P=(u,t=200,e)=>{const s=new Headers({"content-type":"application/json"});for(const[a,i]of Object.entries({}))s.set(a,i);return Response.json(u,{headers:s,status:t})},F="lunora-ping",L="lunora-pong",c="id:",m="retry:",g="dead:",y="pool:",w=100,$=5,b=3e4,k=1e3,T=6,C=9e5,M=999999999999999,U=15,A=u=>String(u).padStart(U,"0"),I=u=>Number.isInteger(u)&&u>0&&u<=M;class n{static indexKey(t,e){return`t:${A(t)}:${e}`}static json(t,e=200){return P(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:$,a=typeof e?.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0?e.baseMs:b,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;reindexed=!1;poolLock=Promise.resolve();activeLeases=new Map;cancelledLeases=new Set;constructor(t,e){this.state=t,this.env=e,this.armWebSocketKeepalive()}async fetch(t){await this.reindexOrphanedRecords();const e=new URL(t.url);if(e.pathname==="/ws"&&t.headers.get("Upgrade")==="websocket")return this.handleWebSocketUpgrade();switch(`${t.method} ${e.pathname}`){case"GET /dead":return this.handleDeadList(e);case"GET /get":return this.handleGet(e);case"GET /list":return this.handleList(e);case"GET /pool":return this.handlePoolStatus(e);case"GET /status":return this.handleStatus();case"POST /cancel":return this.handleCancel(t);case"POST /complete":return this.handleComplete(t);case"POST /dead/cancel":return this.handleDeadCancel(t);case"POST /dead/retry":return this.handleDeadRetry(t);case"POST /schedule":return this.handleSchedule(t)}return P({error:{code:"NOT_FOUND"}},404)}async alarm(){await this.reindexOrphanedRecords();const t=Date.now(),e=[],s=new Set,a=await this.state.storage.list({end:`t:${A(t)}:~`,limit:100,prefix:"t:"}),i=[];for(const[r,d]of a.entries()){const l=Number.parseInt(r.slice(2,r.indexOf(":",2)),10);Number.isFinite(l)&&l<=t&&i.push({indexKey:r,recordId:d})}const o=await Promise.all(i.map(r=>this.state.storage.get(`${c}${r.recordId}`)));for(const[r,{indexKey:d,recordId:l}]of i.entries()){const h=o[r];h&&s.has(l)?await this.state.storage.delete(d):h?(s.add(l),e.push({claimKey:d,record:h})):await this.state.storage.delete(d)}try{await this.drainDue(e)}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);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}`);const o=await fetch(`${e}/_lunora/scheduler/dispatch`,{body:s,headers:a,method:"POST"});return o.status===409&&o.headers.get(E)==="1"&&await o.json().then(d=>d?.error?.code===x,()=>!1)?"in-progress":o.ok}catch{return!1}}armWebSocketKeepalive(){const t=this.state.setWebSocketAutoResponse;typeof t!="function"||typeof WebSocketRequestResponsePair>"u"||t.call(this.state,new WebSocketRequestResponsePair(F,L))}async drainDue(t){const e=[...t],s=[],a=Math.min(T,e.length);for(let i=0;i<a;i+=1)s.push((async()=>{for(let o=e.shift();o!==void 0;o=e.shift())await this.drainRecordGuarded(o.record,o.claimKey)})());await Promise.all(s)}async withPoolLock(t){const e=this.poolLock.then(t,t);return this.poolLock=e.then(()=>{},()=>{}),e}async drainRecordGuarded(t,e){const s=n.indexKey(Date.now()+C,t.id);try{const a=await this.state.storage.get(`${c}${t.id}`);if(a===void 0){await this.state.storage.delete(e);return}if(JSON.stringify(a)!==JSON.stringify(t))return;await this.state.storage.delete(e),await this.state.storage.put(s,t.id),this.activeLeases.set(t.id,s),await this.drainRecord(t)}catch{try{if(this.cancelledLeases.has(t.id)){await this.state.storage.delete(s);return}if(await this.state.storage.get(`${g}${t.id}`)!==void 0){await this.state.storage.delete(s),await this.state.storage.delete([`${m}${t.id}`,`${c}${t.id}`]);return}await this.state.storage.put(e,t.id),await this.state.storage.delete(s)}catch{}return}finally{this.activeLeases.delete(t.id),this.cancelledLeases.delete(t.id)}try{await this.state.storage.delete(s)}catch{}}async drainRecord(t){if(!await this.reservePoolSlot(t))return!1;const s=await this.dispatch(t),a=s===!0,i=t.pool;if(!a&&i!==void 0&&await this.withPoolLock(async()=>{const o=await this.loadPool(i);await this.savePool(i,n.releaseSlot(o,t.id))}),this.cancelledLeases.has(t.id))return a;if(a){try{await this.state.storage.delete([`${c}${t.id}`,`${m}${t.id}`])}catch{}return!0}return await this.recordRetry(t,s==="in-progress"),!1}async reservePoolSlot(t){const e=t.pool;if(e===void 0)return!0;const s=await this.withPoolLock(async()=>{const a=await this.loadPool(e);if(a.inFlight>=a.maxConcurrency)return!1;const i=a.inFlightIds??[];return i.includes(t.id)||i.push(t.id),a.inFlightIds=i,a.inFlight=i.length,await this.savePool(e,a),!0});return s||await this.requeuePooled(t),s}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.listPage(c,w);return s.send(JSON.stringify({records:a.records,truncated:a.truncated,type:"jobs"})),new Response(null,{status:101,webSocket:e})}async broadcastChange(){const t=this.state.getWebSockets?.();if(t===void 0||t.length===0)return;const{records:e,truncated:s}=await this.listPage(c,w),a=JSON.stringify({records:e,truncated:s,type:"jobs"});for(const i of t)try{i.send(a)}catch{}}async listPage(t,e,s){const a=await this.state.storage.list({limit:e+1,prefix:t,...s===void 0?{}:{startAfter:s}}),i=[...a.keys()],o=[...a.values()],r=o.length>e;return r?{cursor:i[e-1],records:o.slice(0,e),truncated:r}:{records:o,truncated:r}}async forEachPage(t,e,s=w){let a;for(;;){const i=await this.state.storage.list(a===void 0?{limit:s,prefix:t}:{limit:s,prefix:t,startAfter:a});if(i.size===0)break;for(const[r,d]of i.entries())e(d,r);if(a=[...i.keys()].at(-1),i.size<s)break}}async signDispatch(t){const e=typeof this.env.LUNORA_SCHEDULER_SECRET=="string"?this.env.LUNORA_SCHEDULER_SECRET:void 0;if(!e||e.length===0)return;const s=new TextEncoder,a=await crypto.subtle.importKey("raw",s.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",a,s.encode(t));return N(new Uint8Array(i))}async recordRetry(t,e=!1){if(this.cancelledLeases.has(t.id))return;const s=(t.attempts??0)+(e?0:1),a=(t.attempts??0)+1,{backoff:i,baseMs:o,maxAttempts:r,maxMs:d}=n.resolveRetry(t),l=i==="linear"?o*a:o*2**(a-1),h=d===void 0?l:Math.min(l,d),f=e?Math.max(h,b):h,p=Math.round(Date.now()+f);if(s>r){await this.parkDead(t,s,`after ${String(s)} attempts`);return}if(!I(p)){await this.parkDead(t,s,`at attempt ${String(s)}: the retry backoff exceeded the largest schedulable time`);return}const v={...t,attempts:s,scheduledFor:p};await this.state.storage.put({[`${c}${t.id}`]:v,[`${m}${t.id}`]:v,[n.indexKey(p,t.id)]:t.id})}async parkDead(t,e,s){await this.state.storage.put(`${g}${t.id}`,{...t,attempts:e}),await this.state.storage.delete([`${m}${t.id}`,`${c}${t.id}`]),console.warn(`@lunora/scheduler: job "${t.id}" (${t.functionPath??t.workflow??"unknown"}) parked in dead-letter ${s}`)}async loadPool(t,e){const s=await this.state.storage.get(`${y}${t}`);return s!==void 0?Array.isArray(s.inFlightIds)?{inFlight:s.inFlightIds.length,inFlightIds:[...s.inFlightIds],maxConcurrency:s.maxConcurrency}:{inFlight:Math.max(0,s.inFlight),maxConcurrency:s.maxConcurrency}:{inFlight:0,inFlightIds:[],maxConcurrency:n.normalizeConcurrency(e,1)}}async savePool(t,e){await this.state.storage.put(`${y}${t}`,e)}async requeuePooled(t){if(this.cancelledLeases.has(t.id))return;const e=Date.now()+k,s={...t,scheduledFor:e};await this.state.storage.put({[`${c}${t.id}`]:s,[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.withPoolLock(async()=>{const o=await this.loadPool(s),r=a===void 0?n.releaseFirstSlot(o):n.releaseSlot(o,a);return await this.savePool(s,r),r});return await this.armAlarmIfEarlier(Date.now()),n.json({inFlight:i.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.forEachPage(c,i=>{i.pool===e&&(a+=1)}),n.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const t=new Map;await this.forEachPage(c,o=>{o.pool!==void 0&&t.set(o.pool,(t.get(o.pool)??0)+1)});const e=[];let s=0,a=0;await this.forEachPage(y,(o,r)=>{const d=r.slice(y.length),l=Math.max(0,o.inFlight),h=t.get(d)??0;e.push({inFlight:l,maxConcurrency:o.maxConcurrency,name:d,queued:h}),s+=h,a+=l});const i={backlog:s,inFlight:a,pools:e};return n.json(i)}async persistPoolCap(t,e){const s=await this.loadPool(t,e);await this.savePool(t,{inFlight:s.inFlight,...s.inFlightIds===void 0?{}:{inFlightIds:s.inFlightIds},maxConcurrency:n.normalizeConcurrency(e,s.maxConcurrency)})}async idConflict(t){if(await this.state.storage.get(`${c}${t}`)!==void 0)return n.error(409,"DUPLICATE_SCHEDULE_ID",`a job with id "${t}" is already scheduled — cancel it first, or schedule under a different id`);if(await this.state.storage.get(`${g}${t}`)!==void 0)return n.error(409,"DUPLICATE_SCHEDULE_ID",`id "${t}" is held by a dead-letter record — retry or cancel it (POST /dead/retry, POST /dead/cancel) first, or schedule under a different id`)}async resolveId(t){let e;try{e=R(t)}catch(s){return n.error(400,"INVALID_SCHEDULE_ID",s instanceof Error?s.message:"invalid `id`")}return t===void 0?e:await this.idConflict(e)??e}async handleSchedule(t){const e=await t.json().catch(()=>{}),s=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"||!I(e.scheduledFor))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),l=await this.resolveId(e.id);if(l instanceof Response)return l;const h=l,f={args:e.args??{},enqueuedAt:Date.now(),id:h,...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}};return o!==void 0&&await this.persistPoolCap(o,e.maxConcurrency),await this.state.storage.put(`${c}${h}`,f),await this.state.storage.put(n.indexKey(f.scheduledFor,h),h),await this.armAlarmIfEarlier(f.scheduledFor),await this.broadcastChange(),n.json({id:h,scheduledFor:f.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(`${c}${e.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),n.json({cancelled:!0})):n.json({cancelled:!1})}async handleList(t){const{cursor:e,records:s,truncated:a}=await this.listPage(c,w,t.searchParams.get("cursor")??void 0);return n.json({cursor:e,records:s,truncated:a})}async handleDeadList(t){const{cursor:e,records:s,truncated:a}=await this.listPage(g,w,t.searchParams.get("cursor")??void 0);return n.json({cursor:e,records:s,truncated:a})}async handleDeadRetry(t){const e=await t.json().catch(()=>{});if(typeof e?.id!="string"||e.id.length===0)return 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(`${c}${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(`${c}${e}`);return n.json(s===void 0?{}:{record:s})}async removeRecord(t){const e=[`${c}${t.id}`,n.indexKey(t.scheduledFor,t.id),`${m}${t.id}`],s=this.activeLeases.get(t.id);s!==void 0&&(e.push(s),this.cancelledLeases.add(t.id)),await this.state.storage.delete(e)}async armAlarmIfEarlier(t){const e=await this.state.storage.getAlarm();(e===null||t<e)&&await this.state.storage.setAlarm(t)}async reindexOrphanedRecords(){if(this.reindexed)return;this.reindexed=!0;const t=new Set;await this.forEachPage("t:",s=>{t.add(s)});const e=[];await this.forEachPage(c,s=>{!t.has(s.id)&&I(s.scheduledFor)&&e.push(s)});for(const s of e)await this.state.storage.put(n.indexKey(s.scheduledFor,s.id),s.id),await this.armAlarmIfEarlier(s.scheduledFor);if(await this.state.storage.getAlarm()===null){const s=await this.earliestPendingTime();s!==void 0&&Number.isFinite(s)&&await this.state.storage.setAlarm(s)}}async earliestPendingTime(){const e=(await this.state.storage.list({limit:1,prefix:"t:"})).entries().next();if(e.done)return;const[s]=e.value;return Number.parseInt(s.slice(2,s.indexOf(":",2)),10)}async rescheduleAlarm(){const t=await this.earliestPendingTime();if(t===void 0){await this.state.storage.deleteAlarm();return}Number.isFinite(t)&&await this.state.storage.setAlarm(t)}}export{C as DISPATCH_LEASE_MS,T as MAX_CONCURRENT_DISPATCHES,$ as MAX_RETRY_ATTEMPTS,M as MAX_SCHEDULED_FOR_MS,b as RETRY_BASE_DELAY_MS,n as SchedulerDO};
@@ -1 +1 @@
1
- import{LunoraError as r}from"@lunora/errors";import{MAX_SCHEDULED_FOR_MS as i}from"./MAX_RETRY_ATTEMPTS-Q3JXDtb1.mjs";const s=(t,e,n="delayMs")=>{if(!Number.isFinite(t)||t<0)throw new r("INVALID_INPUT",`${e}: \`${n}\` must be a non-negative finite number`);if(Date.now()+t>i)throw new r("INVALID_INPUT",`${e}: \`${n}\` is later than the latest schedulable instant (${String(i)} ms)`)};export{s as default};
1
+ import{LunoraError as r}from"@lunora/errors";import{MAX_SCHEDULED_FOR_MS as i}from"./MAX_RETRY_ATTEMPTS-BnrPhYes.mjs";const s=(t,e,n="delayMs")=>{if(!Number.isFinite(t)||t<0)throw new r("INVALID_INPUT",`${e}: \`${n}\` must be a non-negative finite number`);if(Date.now()+t>i)throw new r("INVALID_INPUT",`${e}: \`${n}\` is later than the latest schedulable instant (${String(i)} ms)`)};export{s as default};
@@ -1 +1 @@
1
- import r from"./assertScheduleDelay-Cz5e-UVl.mjs";const l=(t,s,a)=>{const e=t-s;r(Number.isFinite(e)&&e<0?0:e,a,"date")};export{l as default};
1
+ import r from"./assertScheduleDelay-CVTqeF04.mjs";const l=(t,s,a)=>{const e=t-s;r(Number.isFinite(e)&&e<0?0:e,a,"date")};export{l as default};
@@ -1 +1 @@
1
- import{LunoraError as w}from"@lunora/errors";import{e as g,d as A}from"./wire-codec-BqRJjIKh.mjs";import{a as p,c as d,g as f}from"./do-client-XQayNStI.mjs";import{isWorkflowReference as h}from"./isWorkflowReference-CT3tdefh.mjs";import N from"./assertScheduleDelay-Cz5e-UVl.mjs";import R from"./assertScheduleInstant-CMN9XCFm.mjs";const v=async e=>{const a=[];let o;for(;;){const c=await e(o);if(a.push(...Array.isArray(c.records)?c.records:[]),c.truncated!==!0||typeof c.cursor!="string"||c.cursor.length===0)return a;if(c.cursor===o)throw new Error("collectPages: the list did not advance its cursor — refusing to page forever");o=c.cursor}},x=e=>typeof e=="string"?e:(h(e)?e.className:e.__lunoraRef)??"<unknown>",m=e=>({...e,args:A(e.args)}),K=e=>{p(e);const a=async(s,r,n,t={})=>{const u=Math.ceil(s instanceof Date?s.getTime():s);R(u,Date.now(),"ctx.scheduler.runAt");const i={args:g("ctx.scheduler.runAt",x(r),n),id:t.id,instanceName:e.instanceName??"default",maxConcurrency:t.pool===void 0?void 0:t.maxConcurrency,pool:t.pool,retry:t.retry,scheduledFor:u,shardKey:t.shardKey};if(h(r)){if(typeof r.className!="string"||r.className.length===0)throw new w("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `className` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await d(e,"/schedule",{...i,workflow:r.className})).id}const y=typeof r=="string"?r:r.__lunoraRef;return(await d(e,"/schedule",{...i,functionPath:y})).id},o=async(s,r,n,t={})=>(N(s,"ctx.scheduler.runAfter"),a(Date.now()+s,r,n,t)),c=async s=>d(e,"/cancel",{id:s}),l=async s=>(await v(async n=>f(e,n===void 0?s:`${s}?cursor=${encodeURIComponent(n)}`))).map(n=>m(n));return{cancel:c,dead:async()=>l("/dead"),deadRetry:async s=>{const{retried:r}=await d(e,"/dead/retry",{id:s});return r===!0},get:async s=>{const r=await f(e,`/get?id=${encodeURIComponent(s)}`);return r.record===void 0?null:m(r.record)},list:async()=>l("/list"),runAfter:o,runAt:a}};export{K as default};
1
+ import{LunoraError as w}from"@lunora/errors";import{e as g,d as A}from"./wire-codec-BqRJjIKh.mjs";import{a as p,c as d,g as f}from"./do-client-XQayNStI.mjs";import{isWorkflowReference as h}from"./isWorkflowReference-CT3tdefh.mjs";import N from"./assertScheduleDelay-CVTqeF04.mjs";import R from"./assertScheduleInstant-DCqHdiVt.mjs";const v=async e=>{const a=[];let o;for(;;){const c=await e(o);if(a.push(...Array.isArray(c.records)?c.records:[]),c.truncated!==!0||typeof c.cursor!="string"||c.cursor.length===0)return a;if(c.cursor===o)throw new Error("collectPages: the list did not advance its cursor — refusing to page forever");o=c.cursor}},x=e=>typeof e=="string"?e:(h(e)?e.className:e.__lunoraRef)??"<unknown>",m=e=>({...e,args:A(e.args)}),K=e=>{p(e);const a=async(s,r,n,t={})=>{const u=Math.ceil(s instanceof Date?s.getTime():s);R(u,Date.now(),"ctx.scheduler.runAt");const i={args:g("ctx.scheduler.runAt",x(r),n),id:t.id,instanceName:e.instanceName??"default",maxConcurrency:t.pool===void 0?void 0:t.maxConcurrency,pool:t.pool,retry:t.retry,scheduledFor:u,shardKey:t.shardKey};if(h(r)){if(typeof r.className!="string"||r.className.length===0)throw new w("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `className` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await d(e,"/schedule",{...i,workflow:r.className})).id}const y=typeof r=="string"?r:r.__lunoraRef;return(await d(e,"/schedule",{...i,functionPath:y})).id},o=async(s,r,n,t={})=>(N(s,"ctx.scheduler.runAfter"),a(Date.now()+s,r,n,t)),c=async s=>d(e,"/cancel",{id:s}),l=async s=>(await v(async n=>f(e,n===void 0?s:`${s}?cursor=${encodeURIComponent(n)}`))).map(n=>m(n));return{cancel:c,dead:async()=>l("/dead"),deadRetry:async s=>{const{retried:r}=await d(e,"/dead/retry",{id:s});return r===!0},get:async s=>{const r=await f(e,`/get?id=${encodeURIComponent(s)}`);return r.record===void 0?null:m(r.record)},list:async()=>l("/list"),runAfter:o,runAt:a}};export{K as default};
@@ -1 +1 @@
1
- import u from"./createScheduler-CL46fdGy.mjs";const i=t=>t?.at!==void 0?typeof t.at=="number"?t.at:t.at.getTime():Date.now()+(t?.delayMs??0),m=t=>{const r=u({instanceName:t.instanceName,jurisdiction:t.jurisdiction,namespace:t.namespace}),n=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:a}=await r.cancel(e);return a},deadLetter:{list:async()=>(await r.dead()).map(a=>n(a)),requeue:async e=>r.deadRetry(e)},list:async()=>(await r.list()).map(a=>n(a)),schedule:async(e,a,c)=>{const d=r.runAt,s=i(c);return{id:await d(s,e,a,{retry:c?.retry,shardKey:c?.shardKey}),scheduledFor:s}}}};export{m as createSchedulerHost};
1
+ import u from"./createScheduler-DpLHXye6.mjs";const i=t=>t?.at!==void 0?typeof t.at=="number"?t.at:t.at.getTime():Date.now()+(t?.delayMs??0),m=t=>{const r=u({instanceName:t.instanceName,jurisdiction:t.jurisdiction,namespace:t.namespace}),n=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:a}=await r.cancel(e);return a},deadLetter:{list:async()=>(await r.dead()).map(a=>n(a)),requeue:async e=>r.deadRetry(e)},list:async()=>(await r.list()).map(a=>n(a)),schedule:async(e,a,c)=>{const d=r.runAt,s=i(c);return{id:await d(s,e,a,{retry:c?.retry,shardKey:c?.shardKey}),scheduledFor:s}}}};export{m as createSchedulerHost};
@@ -1 +1 @@
1
- import{LunoraError as l}from"@lunora/errors";import{e as s}from"./wire-codec-BqRJjIKh.mjs";import{a as o,g as m,c as u}from"./do-client-XQayNStI.mjs";import y from"./assertScheduleDelay-Cz5e-UVl.mjs";const N=e=>{if(o(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new l("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const a=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async r=>u(e,"/cancel",{id:r}),enqueue:async(r,t,n={})=>{const c=n.delayMs??0;return y(c,"workpool.enqueue"),u(e,"/schedule",{args:s("workpool.enqueue",r.__lunoraRef,t),functionPath:r.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,pool:a,retry:n.retry,scheduledFor:Math.ceil(Date.now()+c),shardKey:n.shardKey})},name:a,status:async()=>m(e,`/pool?name=${encodeURIComponent(a)}`)}};export{N as default};
1
+ import{LunoraError as l}from"@lunora/errors";import{e as s}from"./wire-codec-BqRJjIKh.mjs";import{a as o,g as m,c as u}from"./do-client-XQayNStI.mjs";import y from"./assertScheduleDelay-CVTqeF04.mjs";const N=e=>{if(o(e),!Number.isInteger(e.maxConcurrency)||e.maxConcurrency<=0)throw new l("INTERNAL","@lunora/scheduler: `maxConcurrency` must be a positive integer");const a=typeof e.name=="string"&&e.name.length>0?e.name:"default";return{cancel:async r=>u(e,"/cancel",{id:r}),enqueue:async(r,t,n={})=>{const c=n.delayMs??0;return y(c,"workpool.enqueue"),u(e,"/schedule",{args:s("workpool.enqueue",r.__lunoraRef,t),functionPath:r.__lunoraRef,instanceName:e.instanceName??"default",maxConcurrency:e.maxConcurrency,pool:a,retry:n.retry,scheduledFor:Math.ceil(Date.now()+c),shardKey:n.shardKey})},name:a,status:async()=>m(e,`/pool?name=${encodeURIComponent(a)}`)}};export{N as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "1.0.0-alpha.124",
3
+ "version": "1.0.0-alpha.125",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- import{a as N}from"./base64-junLSktH.mjs";import R from"./resolveScheduleId-DuLCFI_l.mjs";const E="DISPATCH_IN_PROGRESS",x="x-lunora-dispatch-declined",P=(u,t=200,e)=>{const s=new Headers({"content-type":"application/json"});for(const[a,i]of Object.entries({}))s.set(a,i);return Response.json(u,{headers:s,status:t})},F="lunora-ping",L="lunora-pong",c="id:",m="retry:",g="dead:",y="pool:",w=100,$=5,b=3e4,k=1e3,T=6,C=9e5,M=999999999999999,U=15,A=u=>String(u).padStart(U,"0"),v=u=>Number.isInteger(u)&&u>0&&u<=M;class n{static indexKey(t,e){return`t:${A(t)}:${e}`}static json(t,e=200){return P(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:$,a=typeof e?.baseMs=="number"&&Number.isFinite(e.baseMs)&&e.baseMs>=0?e.baseMs:b,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;reindexed=!1;poolLock=Promise.resolve();activeLeases=new Map;cancelledLeases=new Set;constructor(t,e){this.state=t,this.env=e,this.armWebSocketKeepalive()}async fetch(t){await this.reindexOrphanedRecords();const e=new URL(t.url);if(e.pathname==="/ws"&&t.headers.get("Upgrade")==="websocket")return this.handleWebSocketUpgrade();switch(`${t.method} ${e.pathname}`){case"GET /dead":return this.handleDeadList(e);case"GET /get":return this.handleGet(e);case"GET /list":return this.handleList(e);case"GET /pool":return this.handlePoolStatus(e);case"GET /status":return this.handleStatus();case"POST /cancel":return this.handleCancel(t);case"POST /complete":return this.handleComplete(t);case"POST /dead/cancel":return this.handleDeadCancel(t);case"POST /dead/retry":return this.handleDeadRetry(t);case"POST /schedule":return this.handleSchedule(t)}return P({error:{code:"NOT_FOUND"}},404)}async alarm(){await this.reindexOrphanedRecords();const t=Date.now(),e=[],s=new Set,a=await this.state.storage.list({end:`t:${A(t)}:~`,limit:100,prefix:"t:"});for(const[i,o]of a.entries()){const r=Number.parseInt(i.slice(2,i.indexOf(":",2)),10);if(Number.isFinite(r)&&r<=t){const d=await this.state.storage.get(`${c}${o}`);d&&s.has(o)?await this.state.storage.delete(i):d?(s.add(o),e.push({claimKey:i,record:d})):await this.state.storage.delete(i)}}try{await this.drainDue(e)}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);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}`);const o=await fetch(`${e}/_lunora/scheduler/dispatch`,{body:s,headers:a,method:"POST"});return o.status===409&&o.headers.get(x)==="1"&&await o.json().then(d=>d?.error?.code===E,()=>!1)?"in-progress":o.ok}catch{return!1}}armWebSocketKeepalive(){const t=this.state.setWebSocketAutoResponse;typeof t!="function"||typeof WebSocketRequestResponsePair>"u"||t.call(this.state,new WebSocketRequestResponsePair(F,L))}async drainDue(t){const e=[...t],s=[],a=Math.min(T,e.length);for(let i=0;i<a;i+=1)s.push((async()=>{for(let o=e.shift();o!==void 0;o=e.shift())await this.drainRecordGuarded(o.record,o.claimKey)})());await Promise.all(s)}async withPoolLock(t){const e=this.poolLock.then(t,t);return this.poolLock=e.then(()=>{},()=>{}),e}async drainRecordGuarded(t,e){const s=n.indexKey(Date.now()+C,t.id);try{const a=await this.state.storage.get(`${c}${t.id}`);if(a===void 0){await this.state.storage.delete(e);return}if(JSON.stringify(a)!==JSON.stringify(t))return;await this.state.storage.delete(e),await this.state.storage.put(s,t.id),this.activeLeases.set(t.id,s),await this.drainRecord(t)}catch{try{if(this.cancelledLeases.has(t.id)){await this.state.storage.delete(s);return}if(await this.state.storage.get(`${g}${t.id}`)!==void 0){await this.state.storage.delete(s),await this.state.storage.delete([`${m}${t.id}`,`${c}${t.id}`]);return}await this.state.storage.put(e,t.id),await this.state.storage.delete(s)}catch{}return}finally{this.activeLeases.delete(t.id),this.cancelledLeases.delete(t.id)}try{await this.state.storage.delete(s)}catch{}}async drainRecord(t){if(!await this.reservePoolSlot(t))return!1;const s=await this.dispatch(t),a=s===!0,i=t.pool;if(!a&&i!==void 0&&await this.withPoolLock(async()=>{const o=await this.loadPool(i);await this.savePool(i,n.releaseSlot(o,t.id))}),this.cancelledLeases.has(t.id))return a;if(a){try{await this.state.storage.delete([`${c}${t.id}`,`${m}${t.id}`])}catch{}return!0}return await this.recordRetry(t,s==="in-progress"),!1}async reservePoolSlot(t){const e=t.pool;if(e===void 0)return!0;const s=await this.withPoolLock(async()=>{const a=await this.loadPool(e);if(a.inFlight>=a.maxConcurrency)return!1;const i=a.inFlightIds??[];return i.includes(t.id)||i.push(t.id),a.inFlightIds=i,a.inFlight=i.length,await this.savePool(e,a),!0});return s||await this.requeuePooled(t),s}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.listPage(c,w);return s.send(JSON.stringify({records:a.records,truncated:a.truncated,type:"jobs"})),new Response(null,{status:101,webSocket:e})}async broadcastChange(){const t=this.state.getWebSockets?.();if(t===void 0||t.length===0)return;const{records:e,truncated:s}=await this.listPage(c,w),a=JSON.stringify({records:e,truncated:s,type:"jobs"});for(const i of t)try{i.send(a)}catch{}}async listPage(t,e,s){const a=await this.state.storage.list({limit:e+1,prefix:t,...s===void 0?{}:{startAfter:s}}),i=[...a.keys()],o=[...a.values()],r=o.length>e;return r?{cursor:i[e-1],records:o.slice(0,e),truncated:r}:{records:o,truncated:r}}async forEachPage(t,e,s=w){let a;for(;;){const i=await this.state.storage.list(a===void 0?{limit:s,prefix:t}:{limit:s,prefix:t,startAfter:a});if(i.size===0)break;for(const[r,d]of i.entries())e(d,r);if(a=[...i.keys()].at(-1),i.size<s)break}}async signDispatch(t){const e=typeof this.env.LUNORA_SCHEDULER_SECRET=="string"?this.env.LUNORA_SCHEDULER_SECRET:void 0;if(!e||e.length===0)return;const s=new TextEncoder,a=await crypto.subtle.importKey("raw",s.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",a,s.encode(t));return N(new Uint8Array(i))}async recordRetry(t,e=!1){if(this.cancelledLeases.has(t.id))return;const s=(t.attempts??0)+(e?0:1),a=(t.attempts??0)+1,{backoff:i,baseMs:o,maxAttempts:r,maxMs:d}=n.resolveRetry(t),h=i==="linear"?o*a:o*2**(a-1),l=d===void 0?h:Math.min(h,d),f=e?Math.max(l,b):l,p=Math.round(Date.now()+f);if(s>r){await this.parkDead(t,s,`after ${String(s)} attempts`);return}if(!v(p)){await this.parkDead(t,s,`at attempt ${String(s)}: the retry backoff exceeded the largest schedulable time`);return}const I={...t,attempts:s,scheduledFor:p};await this.state.storage.put({[`${c}${t.id}`]:I,[`${m}${t.id}`]:I,[n.indexKey(p,t.id)]:t.id})}async parkDead(t,e,s){await this.state.storage.put(`${g}${t.id}`,{...t,attempts:e}),await this.state.storage.delete([`${m}${t.id}`,`${c}${t.id}`]),console.warn(`@lunora/scheduler: job "${t.id}" (${t.functionPath??t.workflow??"unknown"}) parked in dead-letter ${s}`)}async loadPool(t,e){const s=await this.state.storage.get(`${y}${t}`);return s!==void 0?Array.isArray(s.inFlightIds)?{inFlight:s.inFlightIds.length,inFlightIds:[...s.inFlightIds],maxConcurrency:s.maxConcurrency}:{inFlight:Math.max(0,s.inFlight),maxConcurrency:s.maxConcurrency}:{inFlight:0,inFlightIds:[],maxConcurrency:n.normalizeConcurrency(e,1)}}async savePool(t,e){await this.state.storage.put(`${y}${t}`,e)}async requeuePooled(t){if(this.cancelledLeases.has(t.id))return;const e=Date.now()+k,s={...t,scheduledFor:e};await this.state.storage.put({[`${c}${t.id}`]:s,[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.withPoolLock(async()=>{const o=await this.loadPool(s),r=a===void 0?n.releaseFirstSlot(o):n.releaseSlot(o,a);return await this.savePool(s,r),r});return await this.armAlarmIfEarlier(Date.now()),n.json({inFlight:i.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.forEachPage(c,i=>{i.pool===e&&(a+=1)}),n.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const t=new Map;await this.forEachPage(c,o=>{o.pool!==void 0&&t.set(o.pool,(t.get(o.pool)??0)+1)});const e=[];let s=0,a=0;await this.forEachPage(y,(o,r)=>{const d=r.slice(y.length),h=Math.max(0,o.inFlight),l=t.get(d)??0;e.push({inFlight:h,maxConcurrency:o.maxConcurrency,name:d,queued:l}),s+=l,a+=h});const i={backlog:s,inFlight:a,pools:e};return n.json(i)}async persistPoolCap(t,e){const s=await this.loadPool(t,e);await this.savePool(t,{inFlight:s.inFlight,...s.inFlightIds===void 0?{}:{inFlightIds:s.inFlightIds},maxConcurrency:n.normalizeConcurrency(e,s.maxConcurrency)})}async idConflict(t){if(await this.state.storage.get(`${c}${t}`)!==void 0)return n.error(409,"DUPLICATE_SCHEDULE_ID",`a job with id "${t}" is already scheduled — cancel it first, or schedule under a different id`);if(await this.state.storage.get(`${g}${t}`)!==void 0)return n.error(409,"DUPLICATE_SCHEDULE_ID",`id "${t}" is held by a dead-letter record — retry or cancel it (POST /dead/retry, POST /dead/cancel) first, or schedule under a different id`)}async resolveId(t){let e;try{e=R(t)}catch(s){return n.error(400,"INVALID_SCHEDULE_ID",s instanceof Error?s.message:"invalid `id`")}return t===void 0?e:await this.idConflict(e)??e}async handleSchedule(t){const e=await t.json().catch(()=>{}),s=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"||!v(e.scheduledFor))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),h=await this.resolveId(e.id);if(h instanceof Response)return h;const l=h,f={args:e.args??{},enqueuedAt:Date.now(),id:l,...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}};return o!==void 0&&await this.persistPoolCap(o,e.maxConcurrency),await this.state.storage.put(`${c}${l}`,f),await this.state.storage.put(n.indexKey(f.scheduledFor,l),l),await this.armAlarmIfEarlier(f.scheduledFor),await this.broadcastChange(),n.json({id:l,scheduledFor:f.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(`${c}${e.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),n.json({cancelled:!0})):n.json({cancelled:!1})}async handleList(t){const{cursor:e,records:s,truncated:a}=await this.listPage(c,w,t.searchParams.get("cursor")??void 0);return n.json({cursor:e,records:s,truncated:a})}async handleDeadList(t){const{cursor:e,records:s,truncated:a}=await this.listPage(g,w,t.searchParams.get("cursor")??void 0);return n.json({cursor:e,records:s,truncated:a})}async handleDeadRetry(t){const e=await t.json().catch(()=>{});if(typeof e?.id!="string"||e.id.length===0)return 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(`${c}${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(`${c}${e}`);return n.json(s===void 0?{}:{record:s})}async removeRecord(t){const e=[`${c}${t.id}`,n.indexKey(t.scheduledFor,t.id),`${m}${t.id}`],s=this.activeLeases.get(t.id);s!==void 0&&(e.push(s),this.cancelledLeases.add(t.id)),await this.state.storage.delete(e)}async armAlarmIfEarlier(t){const e=await this.state.storage.getAlarm();(e===null||t<e)&&await this.state.storage.setAlarm(t)}async reindexOrphanedRecords(){if(this.reindexed)return;this.reindexed=!0;const t=new Set;await this.forEachPage("t:",s=>{t.add(s)});const e=[];await this.forEachPage(c,s=>{!t.has(s.id)&&v(s.scheduledFor)&&e.push(s)});for(const s of e)await this.state.storage.put(n.indexKey(s.scheduledFor,s.id),s.id),await this.armAlarmIfEarlier(s.scheduledFor);if(await this.state.storage.getAlarm()===null){const s=await this.earliestPendingTime();s!==void 0&&Number.isFinite(s)&&await this.state.storage.setAlarm(s)}}async earliestPendingTime(){const e=(await this.state.storage.list({limit:1,prefix:"t:"})).entries().next();if(e.done)return;const[s]=e.value;return Number.parseInt(s.slice(2,s.indexOf(":",2)),10)}async rescheduleAlarm(){const t=await this.earliestPendingTime();if(t===void 0){await this.state.storage.deleteAlarm();return}Number.isFinite(t)&&await this.state.storage.setAlarm(t)}}export{C as DISPATCH_LEASE_MS,T as MAX_CONCURRENT_DISPATCHES,$ as MAX_RETRY_ATTEMPTS,M as MAX_SCHEDULED_FOR_MS,b as RETRY_BASE_DELAY_MS,n as SchedulerDO};