@lunora/scheduler 1.0.0-alpha.47 → 1.0.0-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -829,12 +829,15 @@ declare class SchedulerDO {
829
829
  * A throw reaching here always means the job was NOT dispatched:
830
830
  * {@link drainRecord} swallows its own post-dispatch cleanup errors and
831
831
  * returns instead of throwing once a kick succeeds, so every escaping throw
832
- * comes from the pre-dispatch or failed-dispatch paths. We therefore always
833
- * re-assert the time-index claim so a later alarm re-attempts it
834
- * (at-least-once): the claim delete may have removed it and
835
- * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
836
- * re-inserting the same key is idempotent, so a surviving claim is simply
837
- * rewritten to its prior value.
832
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore re-assert
833
+ * the time-index claim so a later alarm re-attempts it (at-least-once): the
834
+ * claim delete may have removed it and recordRetry()/requeuePooled() may not
835
+ * have re-armed it before throwing, and re-inserting the same key is
836
+ * idempotent, so a surviving claim is simply rewritten to its prior value.
837
+ *
838
+ * With one exception, checked first: a record that already has a durable
839
+ * `dead:` row is TERMINAL, and re-claiming it would re-dispatch a job the
840
+ * dead-letter says is finished. See the comment on that branch.
838
841
  */
839
842
  private drainRecordGuarded;
840
843
  /**
@@ -888,22 +891,33 @@ declare class SchedulerDO {
888
891
  * runtime doesn't support hibernated sockets.
889
892
  */
890
893
  private broadcastChange;
894
+ /**
895
+ * One bounded page of the rows under `prefix`, in key order, plus the
896
+ * `cursor` a caller resumes from (the last key of the page) when `truncated`.
897
+ * Lists `limit + 1` and slices back down so both facts are known without a
898
+ * second round-trip.
899
+ *
900
+ * Shared by `/list` (pending headers) and `/dead` (dead-letter records) so
901
+ * NEITHER can materialize an unbounded set into one JSON response: nothing
902
+ * prunes `dead:`, so a workpool with a broken origin parks thousands of rows
903
+ * and the studio's only view of them — and only way to requeue them — would
904
+ * fail exactly when it is needed.
905
+ */
906
+ private listPage;
891
907
  /**
892
908
  * The current pending job records (shared by `/list` and the live channel),
893
909
  * bounded to `limit` (default {@link DEFAULT_LIST_LIMIT}) so a large backlog
894
- * can't be JSON-serialized and fanned out to every socket in one shot. Lists
895
- * `limit + 1` and slices back down so `truncated` reflects whether there was
896
- * a next row, without a second round-trip.
910
+ * can't be JSON-serialized and fanned out to every socket in one shot.
897
911
  */
898
912
  private listRecords;
899
913
  /**
900
- * Page through every `id:` header exactly once with bounded per-page memory
901
- * (a `limit`+`startAfter` cursor loop), invoking `visit` for each record.
902
- * Unlike {@link listRecords}, which intentionally truncates for the studio's
914
+ * Page through every row under `prefix` exactly once with bounded per-page
915
+ * memory (a `limit`+`startAfter` cursor loop), invoking `visit` for each.
916
+ * Unlike {@link listPage}, which intentionally truncates for the studio's
903
917
  * live view, `/status` and `/pool` need EXACT counts — this walks the full
904
918
  * set, but never materializes more than one page at a time.
905
919
  */
906
- private countHeaders;
920
+ private forEachPage;
907
921
  /**
908
922
  * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
909
923
  * returning a base64url signature, or `undefined` when no secret is
@@ -918,6 +932,13 @@ declare class SchedulerDO {
918
932
  * under a `dead:` key for manual inspection.
919
933
  */
920
934
  private recordRetry;
935
+ /**
936
+ * Terminal park into the dead-letter (`dead:`) prefix, with `reason` naming
937
+ * why in the emitted warning. Shared by the two ways a retry ends for good:
938
+ * an exhausted attempt budget, and a backoff that ran past the largest
939
+ * schedulable time (see {@link isIndexableTime}).
940
+ */
941
+ private parkDead;
921
942
  /** Read the durable `pool:<name>` row, defaulting to a fresh `inFlight: 0` pool. */
922
943
  private loadPool;
923
944
  private savePool;
@@ -955,13 +976,19 @@ declare class SchedulerDO {
955
976
  private handleStatus;
956
977
  private handleSchedule;
957
978
  private handleCancel;
979
+ /**
980
+ * `GET /list[?cursor=]` — one bounded page of pending jobs. `truncated` says
981
+ * whether more rows follow and `cursor` is what a caller passes back to get
982
+ * them (`createScheduler.list()` walks every page; the studio shows one).
983
+ */
958
984
  private handleList;
959
985
  /**
960
986
  * `GET /dead` — list the dead-letter records: jobs that exhausted their
961
987
  * retry budget ({@link recordRetry}) and were parked under `dead:<id>`
962
988
  * instead of being silently dropped. These never appear in `/list` (their
963
989
  * `id:` header is deleted on park), so this is the ONLY way the studio can
964
- * surface — and recover — a permanently-failed job.
990
+ * surface — and recover — a permanently-failed job. Bounded and cursored
991
+ * like `/list`: nothing prunes `dead:`, so this set grows without limit.
965
992
  */
966
993
  private handleDeadList;
967
994
  /**
package/dist/index.d.ts CHANGED
@@ -829,12 +829,15 @@ declare class SchedulerDO {
829
829
  * A throw reaching here always means the job was NOT dispatched:
830
830
  * {@link drainRecord} swallows its own post-dispatch cleanup errors and
831
831
  * returns instead of throwing once a kick succeeds, so every escaping throw
832
- * comes from the pre-dispatch or failed-dispatch paths. We therefore always
833
- * re-assert the time-index claim so a later alarm re-attempts it
834
- * (at-least-once): the claim delete may have removed it and
835
- * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
836
- * re-inserting the same key is idempotent, so a surviving claim is simply
837
- * rewritten to its prior value.
832
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore re-assert
833
+ * the time-index claim so a later alarm re-attempts it (at-least-once): the
834
+ * claim delete may have removed it and recordRetry()/requeuePooled() may not
835
+ * have re-armed it before throwing, and re-inserting the same key is
836
+ * idempotent, so a surviving claim is simply rewritten to its prior value.
837
+ *
838
+ * With one exception, checked first: a record that already has a durable
839
+ * `dead:` row is TERMINAL, and re-claiming it would re-dispatch a job the
840
+ * dead-letter says is finished. See the comment on that branch.
838
841
  */
839
842
  private drainRecordGuarded;
840
843
  /**
@@ -888,22 +891,33 @@ declare class SchedulerDO {
888
891
  * runtime doesn't support hibernated sockets.
889
892
  */
890
893
  private broadcastChange;
894
+ /**
895
+ * One bounded page of the rows under `prefix`, in key order, plus the
896
+ * `cursor` a caller resumes from (the last key of the page) when `truncated`.
897
+ * Lists `limit + 1` and slices back down so both facts are known without a
898
+ * second round-trip.
899
+ *
900
+ * Shared by `/list` (pending headers) and `/dead` (dead-letter records) so
901
+ * NEITHER can materialize an unbounded set into one JSON response: nothing
902
+ * prunes `dead:`, so a workpool with a broken origin parks thousands of rows
903
+ * and the studio's only view of them — and only way to requeue them — would
904
+ * fail exactly when it is needed.
905
+ */
906
+ private listPage;
891
907
  /**
892
908
  * The current pending job records (shared by `/list` and the live channel),
893
909
  * bounded to `limit` (default {@link DEFAULT_LIST_LIMIT}) so a large backlog
894
- * can't be JSON-serialized and fanned out to every socket in one shot. Lists
895
- * `limit + 1` and slices back down so `truncated` reflects whether there was
896
- * a next row, without a second round-trip.
910
+ * can't be JSON-serialized and fanned out to every socket in one shot.
897
911
  */
898
912
  private listRecords;
899
913
  /**
900
- * Page through every `id:` header exactly once with bounded per-page memory
901
- * (a `limit`+`startAfter` cursor loop), invoking `visit` for each record.
902
- * Unlike {@link listRecords}, which intentionally truncates for the studio's
914
+ * Page through every row under `prefix` exactly once with bounded per-page
915
+ * memory (a `limit`+`startAfter` cursor loop), invoking `visit` for each.
916
+ * Unlike {@link listPage}, which intentionally truncates for the studio's
903
917
  * live view, `/status` and `/pool` need EXACT counts — this walks the full
904
918
  * set, but never materializes more than one page at a time.
905
919
  */
906
- private countHeaders;
920
+ private forEachPage;
907
921
  /**
908
922
  * HMAC-SHA-256 sign the dispatch body with `env.LUNORA_SCHEDULER_SECRET`,
909
923
  * returning a base64url signature, or `undefined` when no secret is
@@ -918,6 +932,13 @@ declare class SchedulerDO {
918
932
  * under a `dead:` key for manual inspection.
919
933
  */
920
934
  private recordRetry;
935
+ /**
936
+ * Terminal park into the dead-letter (`dead:`) prefix, with `reason` naming
937
+ * why in the emitted warning. Shared by the two ways a retry ends for good:
938
+ * an exhausted attempt budget, and a backoff that ran past the largest
939
+ * schedulable time (see {@link isIndexableTime}).
940
+ */
941
+ private parkDead;
921
942
  /** Read the durable `pool:<name>` row, defaulting to a fresh `inFlight: 0` pool. */
922
943
  private loadPool;
923
944
  private savePool;
@@ -955,13 +976,19 @@ declare class SchedulerDO {
955
976
  private handleStatus;
956
977
  private handleSchedule;
957
978
  private handleCancel;
979
+ /**
980
+ * `GET /list[?cursor=]` — one bounded page of pending jobs. `truncated` says
981
+ * whether more rows follow and `cursor` is what a caller passes back to get
982
+ * them (`createScheduler.list()` walks every page; the studio shows one).
983
+ */
958
984
  private handleList;
959
985
  /**
960
986
  * `GET /dead` — list the dead-letter records: jobs that exhausted their
961
987
  * retry budget ({@link recordRetry}) and were parked under `dead:<id>`
962
988
  * instead of being silently dropped. These never appear in `/list` (their
963
989
  * `id:` header is deleted on park), so this is the ONLY way the studio can
964
- * surface — and recover — a permanently-failed job.
990
+ * surface — and recover — a permanently-failed job. Bounded and cursored
991
+ * like `/list`: nothing prunes `dead:`, so this set grows without limit.
965
992
  */
966
993
  private handleDeadList;
967
994
  /**
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{default as o}from"./packem_shared/createScheduler-CW8pPy0j.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-adiyW-4L.mjs";import{MAX_RETRY_ATTEMPTS as S,RETRY_BASE_DELAY_MS as E,SchedulerDO as C}from"./packem_shared/MAX_RETRY_ATTEMPTS-D24I8zY0.mjs";import{createSchedulerHost as h}from"./packem_shared/createSchedulerHost-B-5nnRt8.mjs";import{isWorkflowReference as T}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as A,isValidCronExpression as g,warnIfSecondsLeading as k}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";export{f as CRON_SCHEDULE_KINDS,S as MAX_RETRY_ATTEMPTS,E as RETRY_BASE_DELAY_MS,C as SchedulerDO,A 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,g as isValidCronExpression,T as isWorkflowReference,k as warnIfSecondsLeading};
1
+ import{default as o}from"./packem_shared/createScheduler-DismdyOw.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-adiyW-4L.mjs";import{MAX_RETRY_ATTEMPTS as S,RETRY_BASE_DELAY_MS as E,SchedulerDO as C}from"./packem_shared/MAX_RETRY_ATTEMPTS-B2mOoWHR.mjs";import{createSchedulerHost as h}from"./packem_shared/createSchedulerHost-bI3AKuoP.mjs";import{isWorkflowReference as T}from"./packem_shared/isWorkflowReference-CT3tdefh.mjs";import{assertValidCronExpression as A,isValidCronExpression as g,warnIfSecondsLeading as k}from"./packem_shared/assertValidCronExpression-DnBtukpq.mjs";export{f as CRON_SCHEDULE_KINDS,S as MAX_RETRY_ATTEMPTS,E as RETRY_BASE_DELAY_MS,C as SchedulerDO,A 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,g as isValidCronExpression,T as isWorkflowReference,k as warnIfSecondsLeading};
@@ -0,0 +1 @@
1
+ const u="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",I=c=>{let e="",t=0;const s=c.length-2;for(;t<s;t+=3){const i=c[t]<<16|c[t+1]<<8|c[t+2];e+=u.charAt(i>>18&63)+u.charAt(i>>12&63)+u.charAt(i>>6&63)+u.charAt(i&63)}const a=c.length-t;if(a===1){const i=c[t]<<16;e+=u.charAt(i>>18&63)+u.charAt(i>>12&63)}else if(a===2){const i=c[t]<<16|c[t+1]<<8;e+=u.charAt(i>>18&63)+u.charAt(i>>12&63)+u.charAt(i>>6&63)}return e},y=(c,e=200,t)=>Response.json(c,{headers:{"content-type":"application/json",...t},status:e}),R="lunora-ping",A="lunora-pong";const m="retry:",f="dead:",p="pool:";const P=5,v=3e4;const w=c=>String(c).padStart(15,"0"),E=c=>Number.isInteger(c)&&c>0&&c<=999999999999999,F=()=>I(crypto.getRandomValues(new Uint8Array(12)));class n{static indexKey(e,t){return`t:${w(e)}:${t}`}static json(e,t=200){return y(e,t)}static error(e,t,s){return n.json({error:{code:t,message:s}},e)}static resolveRetry(e){const t=e.retry,s=typeof t?.maxAttempts=="number"&&Number.isInteger(t.maxAttempts)&&t.maxAttempts>0?t.maxAttempts:5,a=typeof t?.baseMs=="number"&&Number.isFinite(t.baseMs)&&t.baseMs>=0?t.baseMs:3e4,i=t?.backoff==="linear"?"linear":"exponential",o=typeof t?.maxMs=="number"&&Number.isFinite(t.maxMs)&&t.maxMs>=0?t.maxMs:void 0;return{backoff:i,baseMs:a,maxAttempts:s,maxMs:o}}static normalizeConcurrency(e,t){return typeof e=="number"&&Number.isInteger(e)&&e>0?e:t}static normalizeRetry(e){if(typeof e!="object"||e===null)return;const t=e,s={};return typeof t.maxAttempts=="number"&&Number.isInteger(t.maxAttempts)&&t.maxAttempts>0&&(s.maxAttempts=t.maxAttempts),typeof t.baseMs=="number"&&Number.isFinite(t.baseMs)&&t.baseMs>=0&&(s.baseMs=t.baseMs),(t.backoff==="exponential"||t.backoff==="linear")&&(s.backoff=t.backoff),typeof t.maxMs=="number"&&Number.isFinite(t.maxMs)&&t.maxMs>=0&&(s.maxMs=t.maxMs),Object.keys(s).length===0?void 0:s}static releaseSlot(e,t){if(e.inFlightIds===void 0)return{...e,inFlight:Math.max(0,e.inFlight-1)};const s=e.inFlightIds.filter(a=>a!==t);return{...e,inFlight:s.length,inFlightIds:s}}static releaseFirstSlot(e){if(e.inFlightIds===void 0)return{...e,inFlight:Math.max(0,e.inFlight-1)};const t=e.inFlightIds.slice(0,Math.max(0,e.inFlightIds.length-1));return{...e,inFlight:t.length,inFlightIds:t}}static resolveScheduleTarget(e){const t=typeof e?.functionPath=="string"&&e.functionPath.length>0?e.functionPath:void 0,s=typeof e?.workflow=="string"&&e.workflow.length>0?e.workflow:void 0;if(!(t===void 0&&s===void 0))return{functionPath:t,workflow:s}}state;env;constructor(e,t){this.state=e,this.env=t,this.armWebSocketKeepalive()}async fetch(e){const t=new URL(e.url);if(t.pathname==="/ws"&&e.headers.get("Upgrade")==="websocket")return this.handleWebSocketUpgrade();switch(`${e.method} ${t.pathname}`){case"GET /dead":return this.handleDeadList(t);case"GET /get":return this.handleGet(t);case"GET /list":return this.handleList(t);case"GET /pool":return this.handlePoolStatus(t);case"GET /status":return this.handleStatus();case"POST /cancel":return this.handleCancel(e);case"POST /complete":return this.handleComplete(e);case"POST /dead/cancel":return this.handleDeadCancel(e);case"POST /dead/retry":return this.handleDeadRetry(e);case"POST /schedule":return this.handleSchedule(e)}return y({error:{code:"NOT_FOUND"}},404)}async alarm(){const e=Date.now(),t=[],s=await this.state.storage.list({end:`t:${w(e)}:~`,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<=e){const r=await this.state.storage.get(`id:${i}`);r?t.push(r):await this.state.storage.delete(a)}}try{for(const a of t)await this.drainRecordGuarded(a)}finally{await this.rescheduleAlarm()}t.length>0&&await this.broadcastChange()}async dispatch(e){const t=typeof this.env.LUNORA_ORIGIN_URL=="string"&&this.env.LUNORA_ORIGIN_URL.length>0?this.env.LUNORA_ORIGIN_URL:void 0;if(!t)return!1;const s=JSON.stringify({args:e.args,functionPath:e.functionPath,id:e.id,instanceName:e.instanceName,pool:e.pool,scheduledFor:e.scheduledFor,shardKey:e.shardKey,workflow:e.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(`${t}/_lunora/scheduler/dispatch`,{body:s,headers:a,method:"POST"})).ok}catch{return!1}}armWebSocketKeepalive(){const e=this.state.setWebSocketAutoResponse;typeof e!="function"||typeof WebSocketRequestResponsePair>"u"||e.call(this.state,new WebSocketRequestResponsePair(R,A))}async drainRecordGuarded(e){try{await this.state.storage.delete(n.indexKey(e.scheduledFor,e.id)),await this.drainRecord(e)}catch{try{if(await this.state.storage.get(`${f}${e.id}`)!==void 0){await this.state.storage.delete([`${m}${e.id}`,`id:${e.id}`]);return}await this.state.storage.put(n.indexKey(e.scheduledFor,e.id),e.id)}catch{}}}async drainRecord(e){if(!await this.reservePoolSlot(e))return!1;const s=await this.dispatch(e);if(!s&&e.pool!==void 0){const a=await this.loadPool(e.pool),i=n.releaseSlot(a,e.id);await this.savePool(e.pool,i)}if(s){try{await this.state.storage.delete([`id:${e.id}`,`${m}${e.id}`])}catch{}return!0}return await this.recordRetry(e),!1}async reservePoolSlot(e){if(e.pool===void 0)return!0;const t=await this.loadPool(e.pool);if(t.inFlight>=t.maxConcurrency)return await this.requeuePooled(e),!1;const s=t.inFlightIds??[];return s.includes(e.id)||s.push(e.id),t.inFlightIds=s,t.inFlight=s.length,await this.savePool(e.pool,t),!0}async handleWebSocketUpgrade(){if(this.state.acceptWebSocket===void 0)return n.error(501,"WS_UNSUPPORTED","WebSocket subscriptions are not supported in this runtime");const e=new WebSocketPair,t=e[0],s=e[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:t})}async broadcastChange(){const e=this.state.getWebSockets?.();if(e===void 0||e.length===0)return;const{records:t,truncated:s}=await this.listRecords(),a=JSON.stringify({records:t,truncated:s,type:"jobs"});for(const i of e)try{i.send(a)}catch{}}async listPage(e,t,s){const a=await this.state.storage.list({limit:t+1,prefix:e,...s===void 0?{}:{startAfter:s}}),i=[...a.keys()],o=[...a.values()],r=o.length>t;return r?{cursor:i[t-1],records:o.slice(0,t),truncated:r}:{records:o,truncated:r}}async listRecords(e=100,t){return this.listPage("id:",e,t)}async forEachPage(e,t,s=100){let a;for(;;){const i=await this.state.storage.list(a===void 0?{limit:s,prefix:e}:{limit:s,prefix:e,startAfter:a});if(i.size===0)break;for(const[r,h]of i.entries())t(h,r);if(a=[...i.keys()].at(-1),i.size<s)break}}async signDispatch(e){const t=typeof this.env.LUNORA_SCHEDULER_SECRET=="string"?this.env.LUNORA_SCHEDULER_SECRET:void 0;if(!t||t.length===0)return;const s=new TextEncoder,a=await crypto.subtle.importKey("raw",s.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",a,s.encode(e));return I(new Uint8Array(i))}async recordRetry(e){const t=(e.attempts??0)+1,{backoff:s,baseMs:a,maxAttempts:i,maxMs:o}=n.resolveRetry(e),r=s==="linear"?a*t:a*2**(t-1),h=o===void 0?r:Math.min(r,o),l=Math.round(Date.now()+h);if(t>i){await this.parkDead(e,t,`after ${String(t)} attempts`);return}if(!E(l)){await this.parkDead(e,t,`at attempt ${String(t)}: the retry backoff exceeded the largest schedulable time`);return}const d={...e,attempts:t,scheduledFor:l};await this.state.storage.put(`${m}${e.id}`,d),await this.state.storage.put(`id:${e.id}`,d),await this.state.storage.put(n.indexKey(l,e.id),e.id)}async parkDead(e,t,s){await this.state.storage.put(`${f}${e.id}`,{...e,attempts:t}),await this.state.storage.delete([`${m}${e.id}`,`id:${e.id}`]),console.warn(`@lunora/scheduler: job "${e.id}" (${e.functionPath??e.workflow??"unknown"}) parked in dead-letter ${s}`)}async loadPool(e,t){const s=await this.state.storage.get(`${p}${e}`);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(t,1)}}async savePool(e,t){await this.state.storage.put(`${p}${e}`,t)}async requeuePooled(e){const t=Date.now()+1e3,s={...e,scheduledFor:t};await this.state.storage.put(`id:${e.id}`,s),await this.state.storage.put(n.indexKey(t,e.id),e.id)}async handleComplete(e){const t=await e.json().catch(()=>{}),s=typeof t?.pool=="string"&&t.pool.length>0?t.pool:void 0,a=typeof t?.id=="string"&&t.id.length>0?t.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(e){const t=e.searchParams.get("name");if(t===null||t.length===0)return n.error(400,"INVALID_INPUT","name is required");const s=await this.loadPool(t);let a=0;return await this.forEachPage("id:",i=>{i.pool===t&&(a+=1)}),n.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const e=new Map;await this.forEachPage("id:",o=>{const r=o;r.pool!==void 0&&e.set(r.pool,(e.get(r.pool)??0)+1)});const t=[];let s=0,a=0;await this.forEachPage(p,(o,r)=>{const h=o,l=r.slice(p.length),d=Math.max(0,h.inFlight),g=e.get(l)??0;t.push({inFlight:d,maxConcurrency:h.maxConcurrency,name:l,queued:g}),s+=g,a+=d});const i={backlog:s,inFlight:a,pools:t};return n.json(i)}async handleSchedule(e){const t=await e.json().catch(()=>{}),s=n.resolveScheduleTarget(t);if(!t||s===void 0)return n.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:a,workflow:i}=s;if(typeof t.scheduledFor!="number"||!E(t.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 t.pool=="string"&&t.pool.length>0?t.pool:void 0,r=typeof t.instanceName=="string"&&t.instanceName.length>0?t.instanceName:void 0,h=n.normalizeRetry(t.retry),l=F(),d={args:t.args??{},enqueuedAt:Date.now(),id:l,...a===void 0?{}:{functionPath:a},...r===void 0?{}:{instanceName:r},...o===void 0?{}:{pool:o},...h===void 0?{}:{retry:h},scheduledFor:t.scheduledFor,shardKey:t.shardKey,...i===void 0?{}:{workflow:i}};if(o!==void 0){const g=await this.loadPool(o,t.maxConcurrency);await this.savePool(o,{inFlight:g.inFlight,...g.inFlightIds===void 0?{}:{inFlightIds:g.inFlightIds},maxConcurrency:n.normalizeConcurrency(t.maxConcurrency,g.maxConcurrency)})}return await this.state.storage.put(`id:${l}`,d),await this.state.storage.put(n.indexKey(d.scheduledFor,l),l),await this.armAlarmIfEarlier(d.scheduledFor),await this.broadcastChange(),n.json({id:l,scheduledFor:d.scheduledFor})}async handleCancel(e){const t=await e.json().catch(()=>{});if(!t?.id)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${t.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),n.json({cancelled:!0})):n.json({cancelled:!1})}async handleList(e){const{cursor:t,records:s,truncated:a}=await this.listRecords(100,e.searchParams.get("cursor")??void 0);return n.json({cursor:t,records:s,truncated:a})}async handleDeadList(e){const{cursor:t,records:s,truncated:a}=await this.listPage(f,100,e.searchParams.get("cursor")??void 0);return n.json({cursor:t,records:s,truncated:a})}async handleDeadRetry(e){const t=await e.json().catch(()=>{});if(typeof t?.id!="string"||t.id.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${f}${t.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(`${f}${s.id}`),await this.armAlarmIfEarlier(a),await this.broadcastChange(),n.json({id:s.id,retried:!0,scheduledFor:a})}async handleDeadCancel(e){const t=await e.json().catch(()=>{});if(typeof t?.id!="string"||t.id.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.delete(`${f}${t.id}`);return n.json({removed:!!s})}async handleGet(e){const t=e.searchParams.get("id");if(t===null||t.length===0)return n.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${t}`);return n.json(s===void 0?{}:{record:s})}async removeRecord(e){await this.state.storage.delete([`id:${e.id}`,n.indexKey(e.scheduledFor,e.id),`${m}${e.id}`])}async armAlarmIfEarlier(e){const t=await this.state.storage.getAlarm();(t===null||e<t)&&await this.state.storage.setAlarm(e)}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[s]=t.value,a=Number.parseInt(s.slice(2,s.indexOf(":",2)),10);Number.isFinite(a)&&await this.state.storage.setAlarm(a)}}export{P as MAX_RETRY_ATTEMPTS,v as RETRY_BASE_DELAY_MS,n as SchedulerDO};
@@ -0,0 +1 @@
1
+ import{LunoraError as u}from"@lunora/errors";import{a as m,c as a,g as l}from"./do-client-BKEFA9pM.mjs";import{isWorkflowReference as g}from"./isWorkflowReference-CT3tdefh.mjs";const I=n=>{m(n);const o=async(e,r,s,c={})=>{const t=e instanceof Date?e.getTime():e,i={args:s,instanceName:n.instanceName??"default",maxConcurrency:c.pool===void 0?void 0:c.maxConcurrency,originUrl:n.originUrl,pool:c.pool,retry:c.retry,scheduledFor:t,shardKey:c.shardKey};if(g(r)){if(typeof r.binding!="string"||r.binding.length===0)throw new u("INTERNAL","@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference");return(await a(n,"/schedule",{...i,workflow:r.binding})).id}const h=typeof r=="string"?r:r.__lunoraRef;return(await a(n,"/schedule",{...i,functionPath:h})).id},y=async(e,r,s,c={})=>{if(!Number.isFinite(e)||e<0)throw new u("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return o(Date.now()+e,r,s,c)},f=async e=>a(n,"/cancel",{id:e}),d=async e=>{const r=[];let s;for(;;){const c=s===void 0?"":`?cursor=${encodeURIComponent(s)}`,t=await l(n,`${e}${c}`);if(r.push(...Array.isArray(t.records)?t.records:[]),t.truncated!==!0||typeof t.cursor!="string"||t.cursor.length===0)return r;s=t.cursor}};return{cancel:f,dead:async()=>d("/dead"),deadRetry:async e=>{const{retried:r}=await a(n,"/dead/retry",{id:e});return r===!0},get:async e=>(await l(n,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>d("/list"),runAfter:y,runAt:o}};export{I as default};
@@ -1 +1 @@
1
- import u from"./createScheduler-CW8pPy0j.mjs";const i=t=>t?.at!==void 0?typeof t.at=="number"?t.at:t.at.getTime():Date.now()+(t?.delayMs??0),m=t=>{const a=u({instanceName:t.instanceName,jurisdiction:t.jurisdiction,namespace:t.namespace,originUrl:t.originUrl}),n=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:r}=await a.cancel(e);return r},deadLetter:{list:async()=>(await a.dead()).map(r=>n(r)),requeue:async e=>a.deadRetry(e)},list:async()=>(await a.list()).map(r=>n(r)),schedule:async(e,r,c)=>{const d=a.runAt,s=i(c);return{id:await d(s,e,r,{retry:c?.retry,shardKey:c?.shardKey}),scheduledFor:s}}}};export{m as createSchedulerHost};
1
+ import u from"./createScheduler-DismdyOw.mjs";const i=t=>t?.at!==void 0?typeof t.at=="number"?t.at:t.at.getTime():Date.now()+(t?.delayMs??0),m=t=>{const a=u({instanceName:t.instanceName,jurisdiction:t.jurisdiction,namespace:t.namespace,originUrl:t.originUrl}),n=e=>({attempts:e.attempts??0,functionPath:e.functionPath??e.workflow??"",id:e.id,scheduledFor:e.scheduledFor});return{cancel:async e=>{const{cancelled:r}=await a.cancel(e);return r},deadLetter:{list:async()=>(await a.dead()).map(r=>n(r)),requeue:async e=>a.deadRetry(e)},list:async()=>(await a.list()).map(r=>n(r)),schedule:async(e,r,c)=>{const d=a.runAt,s=i(c);return{id:await d(s,e,r,{retry:c?.retry,shardKey:c?.shardKey}),scheduledFor:s}}}};export{m as createSchedulerHost};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "1.0.0-alpha.47",
3
+ "version": "1.0.0-alpha.49",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,8 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.27",
50
- "@lunora/platform": "1.0.0-alpha.23",
49
+ "@lunora/errors": "1.0.0-alpha.29",
50
+ "@lunora/platform": "1.0.0-alpha.24",
51
51
  "cron-parser": "5.8.1"
52
52
  },
53
53
  "engines": {
@@ -1 +0,0 @@
1
- const h="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",R=c=>{let e="",t=0;const s=c.length-2;for(;t<s;t+=3){const n=c[t]<<16|c[t+1]<<8|c[t+2];e+=h.charAt(n>>18&63)+h.charAt(n>>12&63)+h.charAt(n>>6&63)+h.charAt(n&63)}const a=c.length-t;if(a===1){const n=c[t]<<16;e+=h.charAt(n>>18&63)+h.charAt(n>>12&63)}else if(a===2){const n=c[t]<<16|c[t+1]<<8;e+=h.charAt(n>>18&63)+h.charAt(n>>12&63)+h.charAt(n>>6&63)}return e},y=(c,e=200,t)=>Response.json(c,{headers:{"content-type":"application/json",...t},status:e}),A="lunora-ping",E="lunora-pong";const m="retry:",f="dead:",p="pool:";const F=5,P=3e4;const w=c=>String(c).padStart(15,"0"),I=()=>R(crypto.getRandomValues(new Uint8Array(12)));class i{static indexKey(e,t){return`t:${w(e)}:${t}`}static json(e,t=200){return y(e,t)}static error(e,t,s){return i.json({error:{code:t,message:s}},e)}static resolveRetry(e){const t=e.retry,s=typeof t?.maxAttempts=="number"&&Number.isInteger(t.maxAttempts)&&t.maxAttempts>0?t.maxAttempts:5,a=typeof t?.baseMs=="number"&&Number.isFinite(t.baseMs)&&t.baseMs>=0?t.baseMs:3e4,n=t?.backoff==="linear"?"linear":"exponential",o=typeof t?.maxMs=="number"&&Number.isFinite(t.maxMs)&&t.maxMs>=0?t.maxMs:void 0;return{backoff:n,baseMs:a,maxAttempts:s,maxMs:o}}static normalizeConcurrency(e,t){return typeof e=="number"&&Number.isInteger(e)&&e>0?e:t}static normalizeRetry(e){if(typeof e!="object"||e===null)return;const t=e,s={};return typeof t.maxAttempts=="number"&&Number.isInteger(t.maxAttempts)&&t.maxAttempts>0&&(s.maxAttempts=t.maxAttempts),typeof t.baseMs=="number"&&Number.isFinite(t.baseMs)&&t.baseMs>=0&&(s.baseMs=t.baseMs),(t.backoff==="exponential"||t.backoff==="linear")&&(s.backoff=t.backoff),typeof t.maxMs=="number"&&Number.isFinite(t.maxMs)&&t.maxMs>=0&&(s.maxMs=t.maxMs),Object.keys(s).length===0?void 0:s}static releaseSlot(e,t){if(e.inFlightIds===void 0)return{...e,inFlight:Math.max(0,e.inFlight-1)};const s=e.inFlightIds.filter(a=>a!==t);return{...e,inFlight:s.length,inFlightIds:s}}static releaseFirstSlot(e){if(e.inFlightIds===void 0)return{...e,inFlight:Math.max(0,e.inFlight-1)};const t=e.inFlightIds.slice(0,Math.max(0,e.inFlightIds.length-1));return{...e,inFlight:t.length,inFlightIds:t}}static resolveScheduleTarget(e){const t=typeof e?.functionPath=="string"&&e.functionPath.length>0?e.functionPath:void 0,s=typeof e?.workflow=="string"&&e.workflow.length>0?e.workflow:void 0;if(!(t===void 0&&s===void 0))return{functionPath:t,workflow:s}}state;env;constructor(e,t){this.state=e,this.env=t,this.armWebSocketKeepalive()}async fetch(e){const t=new URL(e.url);if(t.pathname==="/ws"&&e.headers.get("Upgrade")==="websocket")return this.handleWebSocketUpgrade();switch(`${e.method} ${t.pathname}`){case"GET /dead":return this.handleDeadList();case"GET /get":return this.handleGet(t);case"GET /list":return this.handleList();case"GET /pool":return this.handlePoolStatus(t);case"GET /status":return this.handleStatus();case"POST /cancel":return this.handleCancel(e);case"POST /complete":return this.handleComplete(e);case"POST /dead/cancel":return this.handleDeadCancel(e);case"POST /dead/retry":return this.handleDeadRetry(e);case"POST /schedule":return this.handleSchedule(e)}return y({error:{code:"NOT_FOUND"}},404)}async alarm(){const e=Date.now(),t=[],s=await this.state.storage.list({end:`t:${w(e)}:~`,limit:100,prefix:"t:"});for(const[a,n]of s.entries()){const o=Number.parseInt(a.slice(2,a.indexOf(":",2)),10);if(Number.isFinite(o)&&o<=e){const r=await this.state.storage.get(`id:${n}`);r?t.push(r):await this.state.storage.delete(a)}}try{for(const a of t)await this.drainRecordGuarded(a)}finally{await this.rescheduleAlarm()}t.length>0&&await this.broadcastChange()}async dispatch(e){const t=typeof this.env.LUNORA_ORIGIN_URL=="string"&&this.env.LUNORA_ORIGIN_URL.length>0?this.env.LUNORA_ORIGIN_URL:void 0;if(!t)return!1;const s=JSON.stringify({args:e.args,functionPath:e.functionPath,id:e.id,instanceName:e.instanceName,pool:e.pool,scheduledFor:e.scheduledFor,shardKey:e.shardKey,workflow:e.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(`${t}/_lunora/scheduler/dispatch`,{body:s,headers:a,method:"POST"})).ok}catch{return!1}}armWebSocketKeepalive(){const e=this.state.setWebSocketAutoResponse;typeof e!="function"||typeof WebSocketRequestResponsePair>"u"||e.call(this.state,new WebSocketRequestResponsePair(A,E))}async drainRecordGuarded(e){try{await this.state.storage.delete(i.indexKey(e.scheduledFor,e.id)),await this.drainRecord(e)}catch{try{await this.state.storage.put(i.indexKey(e.scheduledFor,e.id),e.id)}catch{}}}async drainRecord(e){if(!await this.reservePoolSlot(e))return!1;const s=await this.dispatch(e);if(!s&&e.pool!==void 0){const a=await this.loadPool(e.pool),n=i.releaseSlot(a,e.id);await this.savePool(e.pool,n)}if(s){try{await this.state.storage.delete([`id:${e.id}`,`${m}${e.id}`])}catch{}return!0}return await this.recordRetry(e),!1}async reservePoolSlot(e){if(e.pool===void 0)return!0;const t=await this.loadPool(e.pool);if(t.inFlight>=t.maxConcurrency)return await this.requeuePooled(e),!1;const s=t.inFlightIds??[];return s.includes(e.id)||s.push(e.id),t.inFlightIds=s,t.inFlight=s.length,await this.savePool(e.pool,t),!0}async handleWebSocketUpgrade(){if(this.state.acceptWebSocket===void 0)return i.error(501,"WS_UNSUPPORTED","WebSocket subscriptions are not supported in this runtime");const e=new WebSocketPair,t=e[0],s=e[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:t})}async broadcastChange(){const e=this.state.getWebSockets?.();if(e===void 0||e.length===0)return;const{records:t,truncated:s}=await this.listRecords(),a=JSON.stringify({records:t,truncated:s,type:"jobs"});for(const n of e)try{n.send(a)}catch{}}async listRecords(e=100){const s=[...(await this.state.storage.list({limit:e+1,prefix:"id:"})).values()],a=s.length>e;return{records:a?s.slice(0,e):s,truncated:a}}async countHeaders(e,t=100){let s;for(;;){const a=await this.state.storage.list(s===void 0?{limit:t,prefix:"id:"}:{limit:t,prefix:"id:",startAfter:s});if(a.size===0)break;for(const o of a.values())e(o);if(s=[...a.keys()].at(-1),a.size<t)break}}async signDispatch(e){const t=typeof this.env.LUNORA_SCHEDULER_SECRET=="string"?this.env.LUNORA_SCHEDULER_SECRET:void 0;if(!t||t.length===0)return;const s=new TextEncoder,a=await crypto.subtle.importKey("raw",s.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),n=await crypto.subtle.sign("HMAC",a,s.encode(e));return R(new Uint8Array(n))}async recordRetry(e){const t=(e.attempts??0)+1,{backoff:s,baseMs:a,maxAttempts:n,maxMs:o}=i.resolveRetry(e);if(t>n){await this.state.storage.put(`${f}${e.id}`,{...e,attempts:t}),await this.state.storage.delete([`${m}${e.id}`,`id:${e.id}`]),console.warn(`@lunora/scheduler: job "${e.id}" (${e.functionPath??e.workflow??"unknown"}) parked in dead-letter after ${String(t)} attempts`);return}const r=s==="linear"?a*t:a*2**(t-1),u=o===void 0?r:Math.min(r,o),l=Date.now()+u,d={...e,attempts:t,scheduledFor:l};await this.state.storage.put(`${m}${e.id}`,d),await this.state.storage.put(`id:${e.id}`,d),await this.state.storage.put(i.indexKey(l,e.id),e.id)}async loadPool(e,t){const s=await this.state.storage.get(`${p}${e}`);return s!==void 0?Array.isArray(s.inFlightIds)?{inFlight:s.inFlightIds.length,inFlightIds:[...s.inFlightIds],maxConcurrency:s.maxConcurrency}:{inFlight:Math.max(0,s.inFlight),maxConcurrency:s.maxConcurrency}:{inFlight:0,inFlightIds:[],maxConcurrency:i.normalizeConcurrency(t,1)}}async savePool(e,t){await this.state.storage.put(`${p}${e}`,t)}async requeuePooled(e){const t=Date.now()+1e3,s={...e,scheduledFor:t};await this.state.storage.put(`id:${e.id}`,s),await this.state.storage.put(i.indexKey(t,e.id),e.id)}async handleComplete(e){const t=await e.json().catch(()=>{}),s=typeof t?.pool=="string"&&t.pool.length>0?t.pool:void 0,a=typeof t?.id=="string"&&t.id.length>0?t.id:void 0;if(s===void 0)return i.error(400,"INVALID_INPUT","pool is required");const n=await this.loadPool(s),o=a===void 0?i.releaseFirstSlot(n):i.releaseSlot(n,a);return await this.savePool(s,o),await this.armAlarmIfEarlier(Date.now()),i.json({inFlight:o.inFlight})}async handlePoolStatus(e){const t=e.searchParams.get("name");if(t===null||t.length===0)return i.error(400,"INVALID_INPUT","name is required");const s=await this.loadPool(t);let a=0;return await this.countHeaders(n=>{n.pool===t&&(a+=1)}),i.json({inFlight:s.inFlight,maxConcurrency:s.maxConcurrency,queued:a})}async handleStatus(){const e=await this.state.storage.list({prefix:p}),t=new Map;await this.countHeaders(r=>{r.pool!==void 0&&t.set(r.pool,(t.get(r.pool)??0)+1)});const s=[];let a=0,n=0;for(const[r,u]of e.entries()){const l=r.slice(p.length),d=Math.max(0,u.inFlight),g=t.get(l)??0;s.push({inFlight:d,maxConcurrency:u.maxConcurrency,name:l,queued:g}),a+=g,n+=d}const o={backlog:a,inFlight:n,pools:s};return i.json(o)}async handleSchedule(e){const t=await e.json().catch(()=>{}),s=i.resolveScheduleTarget(t);if(!t||s===void 0)return i.error(400,"INVALID_INPUT","functionPath or workflow is required");const{functionPath:a,workflow:n}=s;if(typeof t.scheduledFor!="number"||!Number.isInteger(t.scheduledFor)||t.scheduledFor<=0||t.scheduledFor>999999999999999)return i.error(400,"INVALID_INPUT","scheduledFor must be a positive integer epoch-millisecond number no greater than 999999999999999");if(typeof this.env.LUNORA_ORIGIN_URL!="string"||this.env.LUNORA_ORIGIN_URL.length===0)return i.error(500,"ORIGIN_NOT_CONFIGURED","LUNORA_ORIGIN_URL env binding must be set on the SchedulerDO");const o=typeof t.pool=="string"&&t.pool.length>0?t.pool:void 0,r=typeof t.instanceName=="string"&&t.instanceName.length>0?t.instanceName:void 0,u=i.normalizeRetry(t.retry),l=I(),d={args:t.args??{},enqueuedAt:Date.now(),id:l,...a===void 0?{}:{functionPath:a},...r===void 0?{}:{instanceName:r},...o===void 0?{}:{pool:o},...u===void 0?{}:{retry:u},scheduledFor:t.scheduledFor,shardKey:t.shardKey,...n===void 0?{}:{workflow:n}};if(o!==void 0){const g=await this.loadPool(o,t.maxConcurrency);await this.savePool(o,{inFlight:g.inFlight,...g.inFlightIds===void 0?{}:{inFlightIds:g.inFlightIds},maxConcurrency:i.normalizeConcurrency(t.maxConcurrency,g.maxConcurrency)})}return await this.state.storage.put(`id:${l}`,d),await this.state.storage.put(i.indexKey(d.scheduledFor,l),l),await this.armAlarmIfEarlier(d.scheduledFor),await this.broadcastChange(),i.json({id:l,scheduledFor:d.scheduledFor})}async handleCancel(e){const t=await e.json().catch(()=>{});if(!t?.id)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${t.id}`);return s?(await this.removeRecord(s),await this.rescheduleAlarm(),await this.broadcastChange(),i.json({cancelled:!0})):i.json({cancelled:!1})}async handleList(){const{records:e,truncated:t}=await this.listRecords();return i.json({records:e,truncated:t})}async handleDeadList(){const e=await this.state.storage.list({prefix:f});return i.json({records:[...e.values()]})}async handleDeadRetry(e){const t=await e.json().catch(()=>{});if(typeof t?.id!="string"||t.id.length===0)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`${f}${t.id}`);if(s===void 0)return i.json({retried:!1});const a=Date.now(),n={...s,attempts:0,scheduledFor:a};return await this.state.storage.put(`id:${s.id}`,n),await this.state.storage.put(i.indexKey(a,s.id),s.id),await this.state.storage.delete(`${f}${s.id}`),await this.armAlarmIfEarlier(a),await this.broadcastChange(),i.json({id:s.id,retried:!0,scheduledFor:a})}async handleDeadCancel(e){const t=await e.json().catch(()=>{});if(typeof t?.id!="string"||t.id.length===0)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.delete(`${f}${t.id}`);return i.json({removed:!!s})}async handleGet(e){const t=e.searchParams.get("id");if(t===null||t.length===0)return i.error(400,"INVALID_INPUT","id is required");const s=await this.state.storage.get(`id:${t}`);return i.json(s===void 0?{}:{record:s})}async removeRecord(e){await this.state.storage.delete([`id:${e.id}`,i.indexKey(e.scheduledFor,e.id),`${m}${e.id}`])}async armAlarmIfEarlier(e){const t=await this.state.storage.getAlarm();(t===null||e<t)&&await this.state.storage.setAlarm(e)}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[s]=t.value,a=Number.parseInt(s.slice(2,s.indexOf(":",2)),10);Number.isFinite(a)&&await this.state.storage.setAlarm(a)}}export{F as MAX_RETRY_ATTEMPTS,P as RETRY_BASE_DELAY_MS,i as SchedulerDO};
@@ -1 +0,0 @@
1
- import{LunoraError as i}from"@lunora/errors";import{a as y,c as a,g as s}from"./do-client-BKEFA9pM.mjs";import{isWorkflowReference as f}from"./isWorkflowReference-CT3tdefh.mjs";const x=n=>{y(n);const d=async(e,r,t,c={})=>{const u=e instanceof Date?e.getTime():e,o={args:t,instanceName:n.instanceName??"default",maxConcurrency:c.pool===void 0?void 0:c.maxConcurrency,originUrl:n.originUrl,pool:c.pool,retry:c.retry,scheduledFor:u,shardKey:c.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(await a(n,"/schedule",{...o,workflow:r.binding})).id}const l=typeof r=="string"?r:r.__lunoraRef;return(await a(n,"/schedule",{...o,functionPath:l})).id};return{cancel:async e=>a(n,"/cancel",{id:e}),dead:async()=>{const e=await s(n,"/dead");return Array.isArray(e.records)?e.records:[]},deadRetry:async e=>{const{retried:r}=await a(n,"/dead/retry",{id:e});return r===!0},get:async e=>(await s(n,`/get?id=${encodeURIComponent(e)}`)).record??null,list:async()=>{const e=await s(n,"/list");return Array.isArray(e.records)?e.records:[]},runAfter:async(e,r,t,c={})=>{if(!Number.isFinite(e)||e<0)throw new i("INTERNAL","@lunora/scheduler: `delayMs` must be a non-negative finite number");return d(Date.now()+e,r,t,c)},runAt:d}};export{x as default};