@lunora/scheduler 1.0.0-alpha.48 → 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 +41 -14
- package/dist/index.d.ts +41 -14
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/MAX_RETRY_ATTEMPTS-B2mOoWHR.mjs +1 -0
- package/dist/packem_shared/createScheduler-DismdyOw.mjs +1 -0
- package/dist/packem_shared/{createSchedulerHost-B-5nnRt8.mjs → createSchedulerHost-bI3AKuoP.mjs} +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/MAX_RETRY_ATTEMPTS-D24I8zY0.mjs +0 -1
- package/dist/packem_shared/createScheduler-CW8pPy0j.mjs +0 -1
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
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
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.
|
|
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 `
|
|
901
|
-
* (a `limit`+`startAfter` cursor loop), invoking `visit` for each
|
|
902
|
-
* Unlike {@link
|
|
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
|
|
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
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
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.
|
|
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 `
|
|
901
|
-
* (a `limit`+`startAfter` cursor loop), invoking `visit` for each
|
|
902
|
-
* Unlike {@link
|
|
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
|
|
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-
|
|
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};
|
package/dist/packem_shared/{createSchedulerHost-B-5nnRt8.mjs → createSchedulerHost-bI3AKuoP.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import u from"./createScheduler-
|
|
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.
|
|
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.
|
|
50
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
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};
|