@stacksjs/scheduler 0.70.380 → 0.71.1

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.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // @bun
2
- var L=import.meta.require;import{ok as x}from"@stacksjs/error-handling";import{path as P}from"@stacksjs/path";import{schedule as C}from"@stacksjs/scheduler";import{globSync as N}from"@stacksjs/storage";import{Every as J}from"@stacksjs/types";async function BU(){let U=N([P.appPath("Jobs/*.ts")],{absolute:!0});for(let V of U)try{let Z=(await import(V)).default,$=S(Z,V);if(Z.rate)y($,Z.rate)}catch(W){console.error(W)}return await j(),x("Schedules ran successfully")}async function j(){let U=P.appPath("Scheduler.ts");try{let V=await import(U);if(typeof V.default==="function")V.default();else console.warn(`Scheduler file ${U} does not export a default function`)}catch(V){console.warn(`Could not load scheduler file ${U}:`,V)}}function y(U,V){switch(V){case J.Minute:C.job(U).everyMinute();break;case J.TwoMinutes:C.job(U).everyTwoMinutes();break;case J.FiveMinutes:C.job(U).everyFiveMinutes();break;case J.TenMinutes:C.job(U).everyTenMinutes();break;case J.ThirtyMinutes:C.job(U).everyThirtyMinutes();break;case J.HalfHour:C.job(U).everyThirtyMinutes();break;case J.Hour:C.job(U).everyHour();break;case J.Day:C.job(U).everyDay();break;case J.Week:C.job(U).weekly();break;case J.Month:C.job(U).monthly();break;case J.Year:C.job(U).yearly();break;default:throw Error(`Unsupported rate: ${V}`)}}function S(U,V){if(U.name)return U.name;return P.basename(V).replace(/\.ts$/,"")}import{spawn as o}from"child_process";import{mkdirSync as l,readFileSync as t,renameSync as r,writeFileSync as s}from"fs";import{join as M}from"path";import{runAction as a}from"@stacksjs/actions";import{log as X,runCommand as e}from"@stacksjs/cli";import{parse as K}from"@stacksjs/cron";import{runJob as UU}from"@stacksjs/queue";import{createHash as f}from"crypto";import{existsSync as k,mkdirSync as b,readFileSync as p,statSync as h,unlinkSync as m,writeFileSync as R}from"fs";import{join as w}from"path";import D from"process";var A="stacks_scheduler";function i(U){let V=Date.now()-U.acquiredAt;if(V<0)return 0;return V}async function E(U,V,W,Z,$){let Q=$??w(D.cwd(),"storage","framework","locks"),G=w(Q,`${U}.lock`);if(!k(Q))b(Q,{recursive:!0});let Y=!1,B=null;if(Z&&(W==="postgres"||W==="mysql")){if(!await u(U,W,Z))return null;B=async()=>{if(Y)return;Y=!0,await c(U,W,Z).catch(()=>{})}}let z={acquiredAt:Date.now(),monotonicStart:Math.round(performance.now()),holder:D.pid};try{R(G,JSON.stringify(z),{flag:"wx"})}catch{let O=!1;try{let q=p(G,"utf8"),_=JSON.parse(q);if(i(_)>=V)O=!0}catch{try{let q=h(G);if(Math.max(0,Date.now()-q.mtimeMs)>=V)O=!0}catch{O=!0}}if(!O){if(B)await B();return null}try{R(G,JSON.stringify(z))}catch{if(B)await B();return null}}return{release:async()=>{try{m(G)}catch{}if(B)await B()}}}function v(U){let V=f("sha256").update(`${A}:${U}`).digest();return{key1:V.readInt32BE(0),key2:V.readInt32BE(4)}}async function u(U,V,W){if(V==="postgres"){let{key1:Q,key2:G}=v(U),Y=await W.unsafe(`SELECT pg_try_advisory_lock(${Q}, ${G}) AS acquired`);return n(Y,"acquired")}let Z=F(U),$=await W.unsafe(`SELECT GET_LOCK('${Z}', 0) AS acquired`);return d($,"acquired")===1}async function c(U,V,W){if(V==="postgres"){let{key1:$,key2:Q}=v(U);await W.unsafe(`SELECT pg_advisory_unlock(${$}, ${Q})`);return}let Z=F(U);await W.unsafe(`SELECT RELEASE_LOCK('${Z}')`)}function F(U){let V=`${A}_${U}`.replace(/[^A-Za-z0-9_]/g,"_");if(V.length<=64)return V;let W=f("sha256").update(U).digest("hex").slice(0,32);return`${A}_${W}`}function n(U,V){let W=g(U);if(!W)return!1;let Z=W[V];return Z===!0||Z===1||Z==="1"||Z==="t"}function d(U,V){let W=g(U);if(!W)return null;let Z=W[V];if(typeof Z==="number")return Z;if(typeof Z==="string"&&/^-?\d+$/.test(Z))return Number.parseInt(Z,10);return null}function g(U){if(!U)return null;if(Array.isArray(U))return U[0];if(typeof U==="object"&&"rows"in U&&Array.isArray(U.rows))return U.rows[0];return null}class H{static jobs=new Map;static scheduledKeys=new Map;cronPattern="";intervalMs=null;timezone="America/Los_Angeles";task;static lockDir=M(process.cwd(),"storage","framework","locks");static stateFile=M(process.cwd(),"storage","framework","runtime","scheduler-state.json");static activeLocks=new Map;shouldPreventOverlap=!1;overlapExpiresAfterMinutes=1440;shouldRunOnOneServer=!1;shouldRunInBackground=!1;outputPath=null;outputAppend=!1;options={};constructor(U){this.task=U,queueMicrotask(()=>{try{this.start()}catch(V){X.error(`Failed to start scheduled task: ${V}`)}})}get pattern(){return this.cronPattern}everySecond(){return this.intervalMs=1000,this.cronPattern="@every_second",this}everyMinute(){return this.cronPattern="* * * * *",this}everyTwoMinutes(){return this.cronPattern="*/2 * * * *",this}everyFiveMinutes(){return this.cronPattern="*/5 * * * *",this}everyTenMinutes(){return this.cronPattern="*/10 * * * *",this}everyThirtyMinutes(){return this.cronPattern="*/30 * * * *",this}everyHour(){return this.cronPattern="0 * * * *",this}everyDay(){return this.cronPattern="0 0 * * *",this}hourly(){return this.cronPattern="0 * * * *",this}daily(){return this.cronPattern="0 0 * * *",this}weekly(){return this.cronPattern="0 0 * * 0",this}monthly(){return this.cronPattern="0 0 1 * *",this}yearly(){return this.cronPattern="0 0 1 1 *",this}annually(){return this.cronPattern="0 0 1 1 *",this}onDays(U){if(!Array.isArray(U)||U.length===0)throw Error(`onDays() requires a non-empty array; got ${JSON.stringify(U)}`);for(let V of U)if(!Number.isInteger(V)||V<0||V>6)throw Error(`onDays(): each day must be an integer 0-6 (Sun-Sat). Got ${V}`);return this.cronPattern=`0 0 * * ${U.join(",")}`,this}at(U){let V=U.split(":");if(V.length!==2||V[0]===""||V[1]==="")throw Error(`Invalid time format "${U}". Expected "HH:MM" (e.g., "14:30")`);let[W,Z]=V.map(Number);if(W===void 0||Z===void 0||Number.isNaN(W)||Number.isNaN(Z)||W<0||W>23||Z<0||Z>59)throw Error(`Invalid time "${U}". Hour must be 0-23, minute must be 0-59`);return this.cronPattern=`${Z} ${W} * * *`,this}setTimeZone(U){return this.timezone=U,this.options.timezone=U,this}withErrorHandler(U){return this.options.catch=U,this}withMaxRuns(U){return this.options.maxRuns=U,this}withProtection(U){return this.options.protect=U||!0,this}withName(U){return this.options.name=U,this}withContext(U){return this.options.context=U,this}withInterval(U){return this.options.interval=U,this}between(U,V){return this.options.startAt=U,this.options.stopAt=V,this}withoutOverlapping(U){if(this.shouldPreventOverlap=!0,U!==void 0)this.overlapExpiresAfterMinutes=U;return this}onOneServer(){return this.shouldRunOnOneServer=!0,this}runInBackground(){return this.shouldRunInBackground=!0,this}sendOutputTo(U){return this.outputPath=U,this.outputAppend=!1,this}appendOutputTo(U){return this.outputPath=U,this.outputAppend=!0,this}async runMissed(U){if(!this.cronPattern)return X.warn("[scheduler] runMissed() requires a cron-based schedule; interval-based tasks (everySecond) have no concept of missed slots"),0;let V=U.max??100,W=U.since instanceof Date?U.since.getTime():U.since,Z=Date.now();if(W>=Z)return 0;let $=[],Q=W;while($.length<V+1){let Y=K(this.cronPattern,Q);if(!Y||Y.getTime()>Z)break;$.push(Y),Q=Y.getTime()}if($.length>V)X.warn(`[scheduler] runMissed: ${$.length} missed slots since ${new Date(W).toISOString()}, capping at ${V} \u2014 older runs dropped`),$.splice(0,$.length-V);let G=0;for(let Y of $)try{let B=this.task();if(B&&typeof B.then==="function")await B;G++}catch(B){if(X.error(`[scheduler] runMissed slot ${Y.toISOString()} failed: ${B instanceof Error?B.message:String(B)}`),this.options.catch)this.options.catch(B)}return G}static listJobs(){let U=[];for(let[V,W]of H.jobs)U.push({name:V,pattern:W.pattern,timezone:W.timezone,nextRun:W.nextRun?W.nextRun():null,enabled:H.isEnabled(V)});return U}static async runNow(U){let V=H.jobs.get(U);if(!V)throw Error(`Scheduled task "${U}" was not found.`);if(!H.isEnabled(U))throw Error(`Scheduled task "${U}" is paused.`);await V.run()}static setEnabled(U,V){let W=H.disabledJobs();if(V)W.delete(U);else W.add(U);let Z=M(process.cwd(),"storage","framework","runtime");l(Z,{recursive:!0});let $=`${H.stateFile}.${process.pid}.tmp`;s($,`${JSON.stringify({disabled:[...W].sort()},null,2)}
3
- `,{mode:384}),r($,H.stateFile)}static isEnabled(U){return!H.disabledJobs().has(U)}static disabledJobs(){try{let U=JSON.parse(t(H.stateFile,"utf8"));return new Set(Array.isArray(U.disabled)?U.disabled.filter((V)=>typeof V==="string"):[])}catch{return new Set}}static listLocks(){return Array.from(H.activeLocks.keys())}async acquireLock(U){if(H.activeLocks.has(U))return!1;let V=this.overlapExpiresAfterMinutes*60*1000,W=null,Z=null;if(this.shouldRunOnOneServer)try{let{db:Q}=await import("@stacksjs/database");Z=Q;let G=(await import("@stacksjs/env")).env.DB_CONNECTION||"sqlite";if(G==="postgres"||G==="mysql"||G==="sqlite")W=G}catch(Q){X.warn(`[scheduler] onOneServer() couldn't reach DB; falling back to file-only lock (which only serializes within this process): ${Q instanceof Error?Q.message:String(Q)}`)}let $=await E(U,V,W,Z,H.lockDir);if(!$)return!1;return H.activeLocks.set(U,$),!0}async releaseLock(U){let V=H.activeLocks.get(U);if(!V)return;H.activeLocks.delete(U),await V.release()}wrapTask(U){let V=this.options.name||"unnamed-task",W=U;if(this.shouldPreventOverlap||this.shouldRunOnOneServer){let Z=this,$=W;W=()=>{(async()=>{if(!await Z.acquireLock(V)){X.info(`Skipping overlapping task: ${V}`);return}try{let G=$();if(G&&typeof G==="object"&&typeof G.finally==="function")await G}catch(G){X.error(`[scheduler] task ${V} threw: ${G instanceof Error?G.message:String(G)}`)}finally{await Z.releaseLock(V)}})()}}if(this.shouldRunInBackground){let Z=W,$=this.outputPath,Q=this.outputAppend;W=()=>{let G="inherit",Y="inherit",B=null;if($)try{let{openSync:O}=L("fs"),q=O($,Q?"a":"w");G=q,Y=q,B=q}catch(O){X.warn(`[scheduler] couldn't open ${$} for background task ${V}: ${O instanceof Error?O.message:String(O)}; falling back to inherit`)}let z=o(process.execPath,["-e",`(${Z.toString()})()`],{detached:!0,stdio:["ignore",G,Y]});z.on("error",(O)=>{if(X.error(`Background task ${V} failed to spawn: ${O.message}`),B!==null)try{let{closeSync:q}=L("fs");q(B)}catch{}}),z.on("exit",(O,q)=>{if(O!==0&&O!==null)X.error(`[scheduler] background task ${V} (pid: ${z.pid}) exited with code ${O}`);else if(q)X.warn(`[scheduler] background task ${V} (pid: ${z.pid}) terminated by signal ${q}`);if(B!==null)try{let{closeSync:_}=L("fs");_(B)}catch{}}),z.unref(),X.info(`Task ${V} spawned in background (pid: ${z.pid})`)}}return W}getNextRunTime(){if(!this.cronPattern||this.intervalMs!==null)return null;if(!this.timezone||this.timezone==="UTC")return K(this.cronPattern);return K(this.cronPattern,void 0,{tz:this.timezone})}start(){if(!this.cronPattern&&this.intervalMs===null)return{stop:()=>{},run:async()=>{},nextRun:()=>null};let U=this.options.name?`${this.options.name.toLowerCase().replace(/[^a-z0-9]+/g,"")}::${this.cronPattern||`every:${this.intervalMs}`}`:null;if(U){let O=H.scheduledKeys.get(U);if(O)return X.debug(`[scheduler] duplicate schedule for "${this.options.name}" (${this.cronPattern||`every ${this.intervalMs}ms`}) skipped`),O}let V=this.wrapTask(this.task),W=!1,Z=null,$=0,Q=()=>{if(W=!0,Z!==null){if(this.intervalMs!==null)clearInterval(Z);else clearTimeout(Z);Z=null}},G=()=>{return this.getNextRunTime()},Y=this.options.name||"unnamed-task",B=async()=>{if(!H.isEnabled(Y)){X.debug(`[scheduler] task ${Y} is paused`);return}if(this.options.maxRuns&&$>=this.options.maxRuns){Q();return}$++;try{let O=V();if(O&&typeof O.then==="function")await O}catch(O){if(this.options.catch)this.options.catch(O);else X.error(`[scheduler] task ${Y} failed (no .catch handler installed): ${O instanceof Error?O.message:String(O)}`);throw O}},z={stop:Q,run:B,nextRun:G,pattern:this.intervalMs!==null?`every ${this.intervalMs/1000}s`:this.cronPattern,timezone:this.timezone,name:this.options.name,enabled:H.isEnabled(Y)};if(this.intervalMs!==null)Z=setInterval(()=>{if(W)return;B().catch(()=>{})},this.intervalMs),Z.unref?.();else if(this.cronPattern){let q=()=>{if(W)return;if(this.options.maxRuns&&$>=this.options.maxRuns)return;let _=this.getNextRunTime();if(!_)return;let I=Math.max(_.getTime()-Date.now(),0);if(I>2147483647)Z=setTimeout(()=>{if(W)return;q()},2147483647);else Z=setTimeout(()=>{if(W)return;B().catch(()=>{}),q()},I)};q()}if(this.options.name)H.jobs.set(this.options.name,z);if(U)H.scheduledKeys.set(U,z);return X.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`),z}static job(U){return new H(async()=>{X.info(`Running job: ${U}`);try{await UU(U)}catch(V){throw X.error(`Job ${U} failed:`,V),V}}).withName(U)}static action(U){return new H(async()=>{X.info(`Running action: ${U}`);try{await a(U)}catch(V){throw X.error(`Action ${U} failed:`,V),V}}).withName(U)}static command(U){let V=null;return V=new H(async()=>{let Z=V?.outputPath??null,$=V?.outputAppend??!1;try{if(X.info(`Executing command: ${U}`),Z){let{spawn:G}=await import("child_process"),{openSync:Y,closeSync:B}=await import("fs"),O=Y(Z,$?"a":"w");try{await new Promise((q,_)=>{let I=G(U,{shell:!0,stdio:["ignore",O,O]});I.on("error",_),I.on("exit",(T)=>{if(T===0)q();else _(Error(`Command '${U}' exited with code ${T}`))})})}finally{B(O)}return}let Q=await e(U);if(Q.isErr)throw X.error(Q.error),Q.error}catch(Q){throw X.error(`Command execution failed: ${Q}`),Q}}),V.withName(`command-${U}`)}static notification(U,V,W,Z){return new H(async()=>{try{let Q=(await import("@stacksjs/notifications").catch(()=>null))?.notify;if(!Q){X.error("schedule.notification: @stacksjs/notifications.notify is not available \u2014 install / wire the notifications package.");return}await Q(U,V,W,Z)}catch($){throw X.error("Scheduled notification failed:",$),$}}).withName("notification")}static async gracefulShutdown(){X.info("Gracefully shutting down scheduled jobs...");for(let[U,V]of H.jobs)X.info(`Stopping job: ${U}`),V.stop();H.jobs.clear(),H.scheduledKeys.clear(),X.info("All jobs have been stopped")}}class VU extends H{}function WU(U){if(U instanceof Date)return U>new Date?U:null;return K(U)}function fU(U){let V=WU(U);if(!V)return-1;return V.getTime()-Date.now()}var EU=H;export{fU as timeout,WU as sendAt,EU as schedule,BU as runScheduler,H as Schedule,VU as Queue};
2
+ var L=import.meta.require;import{ok as g}from"@stacksjs/error-handling";import{log as N}from"@stacksjs/logging";import{path as P}from"@stacksjs/path";import{schedule as C}from"@stacksjs/scheduler";import{globSync as j}from"@stacksjs/storage";import{Every as J}from"@stacksjs/types";async function YU(){let U=j([P.appPath("Jobs/*.ts")],{absolute:!0});await y();for(let V of U)try{let Z=(await import(V)).default,$=k(Z,V);if(!Z.rate)continue;if(C.isScheduled($)){N.debug(`[scheduler] ${$} is declared in app/Scheduler.ts; ignoring its \`rate\` so it is not scheduled twice`);continue}S($,Z.rate)}catch(W){console.error(W)}return g("Schedules ran successfully")}async function y(){let U=P.appPath("Scheduler.ts");try{let V=await import(U);if(typeof V.default==="function")V.default();else console.warn(`Scheduler file ${U} does not export a default function`)}catch(V){console.warn(`Could not load scheduler file ${U}:`,V)}}function S(U,V){switch(V){case J.Minute:C.job(U).everyMinute();break;case J.TwoMinutes:C.job(U).everyTwoMinutes();break;case J.FiveMinutes:C.job(U).everyFiveMinutes();break;case J.TenMinutes:C.job(U).everyTenMinutes();break;case J.ThirtyMinutes:C.job(U).everyThirtyMinutes();break;case J.HalfHour:C.job(U).everyThirtyMinutes();break;case J.Hour:C.job(U).everyHour();break;case J.Day:C.job(U).everyDay();break;case J.Week:C.job(U).weekly();break;case J.Month:C.job(U).monthly();break;case J.Year:C.job(U).yearly();break;default:throw Error(`Unsupported rate: ${V}`)}}function k(U,V){if(U.name)return U.name;return P.basename(V).replace(/\.ts$/,"")}import{spawn as l}from"child_process";import{mkdirSync as r,readFileSync as t,renameSync as s,writeFileSync as a}from"fs";import{join as M}from"path";import{runAction as e}from"@stacksjs/actions";import{log as X,runCommand as UU}from"@stacksjs/cli";import{parse as K}from"@stacksjs/cron";import{runJob as VU}from"@stacksjs/queue";import{createHash as f}from"crypto";import{existsSync as b,mkdirSync as p,readFileSync as h,statSync as m,unlinkSync as i,writeFileSync as R}from"fs";import{join as w}from"path";import D from"process";var A="stacks_scheduler";function u(U){let V=Date.now()-U.acquiredAt;if(V<0)return 0;return V}async function E(U,V,W,Z,$){let Q=$??w(D.cwd(),"storage","framework","locks"),G=w(Q,`${U}.lock`);if(!b(Q))p(Q,{recursive:!0});let Y=!1,B=null;if(Z&&(W==="postgres"||W==="mysql")){if(!await c(U,W,Z))return null;B=async()=>{if(Y)return;Y=!0,await n(U,W,Z).catch(()=>{})}}let z={acquiredAt:Date.now(),monotonicStart:Math.round(performance.now()),holder:D.pid};try{R(G,JSON.stringify(z),{flag:"wx"})}catch{let O=!1;try{let q=h(G,"utf8"),_=JSON.parse(q);if(u(_)>=V)O=!0}catch{try{let q=m(G);if(Math.max(0,Date.now()-q.mtimeMs)>=V)O=!0}catch{O=!0}}if(!O){if(B)await B();return null}try{R(G,JSON.stringify(z))}catch{if(B)await B();return null}}return{release:async()=>{try{i(G)}catch{}if(B)await B()}}}function v(U){let V=f("sha256").update(`${A}:${U}`).digest();return{key1:V.readInt32BE(0),key2:V.readInt32BE(4)}}async function c(U,V,W){if(V==="postgres"){let{key1:Q,key2:G}=v(U),Y=await W.unsafe(`SELECT pg_try_advisory_lock(${Q}, ${G}) AS acquired`);return d(Y,"acquired")}let Z=F(U),$=await W.unsafe(`SELECT GET_LOCK('${Z}', 0) AS acquired`);return o($,"acquired")===1}async function n(U,V,W){if(V==="postgres"){let{key1:$,key2:Q}=v(U);await W.unsafe(`SELECT pg_advisory_unlock(${$}, ${Q})`);return}let Z=F(U);await W.unsafe(`SELECT RELEASE_LOCK('${Z}')`)}function F(U){let V=`${A}_${U}`.replace(/[^A-Za-z0-9_]/g,"_");if(V.length<=64)return V;let W=f("sha256").update(U).digest("hex").slice(0,32);return`${A}_${W}`}function d(U,V){let W=x(U);if(!W)return!1;let Z=W[V];return Z===!0||Z===1||Z==="1"||Z==="t"}function o(U,V){let W=x(U);if(!W)return null;let Z=W[V];if(typeof Z==="number")return Z;if(typeof Z==="string"&&/^-?\d+$/.test(Z))return Number.parseInt(Z,10);return null}function x(U){if(!U)return null;if(Array.isArray(U))return U[0];if(typeof U==="object"&&"rows"in U&&Array.isArray(U.rows))return U.rows[0];return null}class H{static jobs=new Map;static scheduledKeys=new Map;cronPattern="";intervalMs=null;timezone="America/Los_Angeles";task;static lockDir=M(process.cwd(),"storage","framework","locks");static stateFile=M(process.cwd(),"storage","framework","runtime","scheduler-state.json");static activeLocks=new Map;shouldPreventOverlap=!1;overlapExpiresAfterMinutes=1440;shouldRunOnOneServer=!1;shouldRunInBackground=!1;outputPath=null;outputAppend=!1;options={};constructor(U){this.task=U,queueMicrotask(()=>{try{this.start()}catch(V){X.error(`Failed to start scheduled task: ${V}`)}})}get pattern(){return this.cronPattern}everySecond(){return this.intervalMs=1000,this.cronPattern="@every_second",this}everyMinute(){return this.cronPattern="* * * * *",this}everyTwoMinutes(){return this.cronPattern="*/2 * * * *",this}everyFiveMinutes(){return this.cronPattern="*/5 * * * *",this}everyTenMinutes(){return this.cronPattern="*/10 * * * *",this}everyThirtyMinutes(){return this.cronPattern="*/30 * * * *",this}everyHour(){return this.cronPattern="0 * * * *",this}everyDay(){return this.cronPattern="0 0 * * *",this}hourly(){return this.cronPattern="0 * * * *",this}daily(){return this.cronPattern="0 0 * * *",this}weekly(){return this.cronPattern="0 0 * * 0",this}monthly(){return this.cronPattern="0 0 1 * *",this}yearly(){return this.cronPattern="0 0 1 1 *",this}annually(){return this.cronPattern="0 0 1 1 *",this}onDays(U){if(!Array.isArray(U)||U.length===0)throw Error(`onDays() requires a non-empty array; got ${JSON.stringify(U)}`);for(let V of U)if(!Number.isInteger(V)||V<0||V>6)throw Error(`onDays(): each day must be an integer 0-6 (Sun-Sat). Got ${V}`);return this.cronPattern=`0 0 * * ${U.join(",")}`,this}at(U){let V=U.split(":");if(V.length!==2||V[0]===""||V[1]==="")throw Error(`Invalid time format "${U}". Expected "HH:MM" (e.g., "14:30")`);let[W,Z]=V.map(Number);if(W===void 0||Z===void 0||Number.isNaN(W)||Number.isNaN(Z)||W<0||W>23||Z<0||Z>59)throw Error(`Invalid time "${U}". Hour must be 0-23, minute must be 0-59`);return this.cronPattern=`${Z} ${W} * * *`,this}setTimeZone(U){return this.timezone=U,this.options.timezone=U,this}withErrorHandler(U){return this.options.catch=U,this}withMaxRuns(U){return this.options.maxRuns=U,this}withProtection(U){return this.options.protect=U||!0,this}withName(U){return this.options.name=U,this}withContext(U){return this.options.context=U,this}withInterval(U){return this.options.interval=U,this}between(U,V){return this.options.startAt=U,this.options.stopAt=V,this}withoutOverlapping(U){if(this.shouldPreventOverlap=!0,U!==void 0)this.overlapExpiresAfterMinutes=U;return this}onOneServer(){return this.shouldRunOnOneServer=!0,this}runInBackground(){return this.shouldRunInBackground=!0,this}sendOutputTo(U){return this.outputPath=U,this.outputAppend=!1,this}appendOutputTo(U){return this.outputPath=U,this.outputAppend=!0,this}async runMissed(U){if(!this.cronPattern)return X.warn("[scheduler] runMissed() requires a cron-based schedule; interval-based tasks (everySecond) have no concept of missed slots"),0;let V=U.max??100,W=U.since instanceof Date?U.since.getTime():U.since,Z=Date.now();if(W>=Z)return 0;let $=[],Q=W;while($.length<V+1){let Y=K(this.cronPattern,Q);if(!Y||Y.getTime()>Z)break;$.push(Y),Q=Y.getTime()}if($.length>V)X.warn(`[scheduler] runMissed: ${$.length} missed slots since ${new Date(W).toISOString()}, capping at ${V} \u2014 older runs dropped`),$.splice(0,$.length-V);let G=0;for(let Y of $)try{let B=this.task();if(B&&typeof B.then==="function")await B;G++}catch(B){if(X.error(`[scheduler] runMissed slot ${Y.toISOString()} failed: ${B instanceof Error?B.message:String(B)}`),this.options.catch)this.options.catch(B)}return G}static listJobs(){let U=[];for(let[V,W]of H.jobs)U.push({name:V,pattern:W.pattern,timezone:W.timezone,nextRun:W.nextRun?W.nextRun():null,enabled:H.isEnabled(V)});return U}static async runNow(U){let V=H.jobs.get(U);if(!V)throw Error(`Scheduled task "${U}" was not found.`);if(!H.isEnabled(U))throw Error(`Scheduled task "${U}" is paused.`);await V.run()}static setEnabled(U,V){let W=H.disabledJobs();if(V)W.delete(U);else W.add(U);let Z=M(process.cwd(),"storage","framework","runtime");r(Z,{recursive:!0});let $=`${H.stateFile}.${process.pid}.tmp`;a($,`${JSON.stringify({disabled:[...W].sort()},null,2)}
3
+ `,{mode:384}),s($,H.stateFile)}static isEnabled(U){return!H.disabledJobs().has(U)}static isScheduled(U){return H.jobs.has(U)}static disabledJobs(){try{let U=JSON.parse(t(H.stateFile,"utf8"));return new Set(Array.isArray(U.disabled)?U.disabled.filter((V)=>typeof V==="string"):[])}catch{return new Set}}static listLocks(){return Array.from(H.activeLocks.keys())}async acquireLock(U){if(H.activeLocks.has(U))return!1;let V=this.overlapExpiresAfterMinutes*60*1000,W=null,Z=null;if(this.shouldRunOnOneServer)try{let{db:Q}=await import("@stacksjs/database");Z=Q;let G=(await import("@stacksjs/env")).env.DB_CONNECTION||"sqlite";if(G==="postgres"||G==="mysql"||G==="sqlite")W=G}catch(Q){X.warn(`[scheduler] onOneServer() couldn't reach DB; falling back to file-only lock (which only serializes within this process): ${Q instanceof Error?Q.message:String(Q)}`)}let $=await E(U,V,W,Z,H.lockDir);if(!$)return!1;return H.activeLocks.set(U,$),!0}async releaseLock(U){let V=H.activeLocks.get(U);if(!V)return;H.activeLocks.delete(U),await V.release()}wrapTask(U){let V=this.options.name||"unnamed-task",W=U;if(this.shouldPreventOverlap||this.shouldRunOnOneServer){let Z=this,$=W;W=()=>{(async()=>{if(!await Z.acquireLock(V)){X.info(`Skipping overlapping task: ${V}`);return}try{let G=$();if(G&&typeof G==="object"&&typeof G.finally==="function")await G}catch(G){X.error(`[scheduler] task ${V} threw: ${G instanceof Error?G.message:String(G)}`)}finally{await Z.releaseLock(V)}})()}}if(this.shouldRunInBackground){let Z=W,$=this.outputPath,Q=this.outputAppend;W=()=>{let G="inherit",Y="inherit",B=null;if($)try{let{openSync:O}=L("fs"),q=O($,Q?"a":"w");G=q,Y=q,B=q}catch(O){X.warn(`[scheduler] couldn't open ${$} for background task ${V}: ${O instanceof Error?O.message:String(O)}; falling back to inherit`)}let z=l(process.execPath,["-e",`(${Z.toString()})()`],{detached:!0,stdio:["ignore",G,Y]});z.on("error",(O)=>{if(X.error(`Background task ${V} failed to spawn: ${O.message}`),B!==null)try{let{closeSync:q}=L("fs");q(B)}catch{}}),z.on("exit",(O,q)=>{if(O!==0&&O!==null)X.error(`[scheduler] background task ${V} (pid: ${z.pid}) exited with code ${O}`);else if(q)X.warn(`[scheduler] background task ${V} (pid: ${z.pid}) terminated by signal ${q}`);if(B!==null)try{let{closeSync:_}=L("fs");_(B)}catch{}}),z.unref(),X.info(`Task ${V} spawned in background (pid: ${z.pid})`)}}return W}getNextRunTime(){if(!this.cronPattern||this.intervalMs!==null)return null;if(!this.timezone||this.timezone==="UTC")return K(this.cronPattern);return K(this.cronPattern,void 0,{tz:this.timezone})}start(){if(!this.cronPattern&&this.intervalMs===null)return{stop:()=>{},run:async()=>{},nextRun:()=>null};let U=this.options.name?`${this.options.name.toLowerCase().replace(/[^a-z0-9]+/g,"")}::${this.cronPattern||`every:${this.intervalMs}`}`:null;if(U){let O=H.scheduledKeys.get(U);if(O)return X.debug(`[scheduler] duplicate schedule for "${this.options.name}" (${this.cronPattern||`every ${this.intervalMs}ms`}) skipped`),O}let V=this.wrapTask(this.task),W=!1,Z=null,$=0,Q=()=>{if(W=!0,Z!==null){if(this.intervalMs!==null)clearInterval(Z);else clearTimeout(Z);Z=null}},G=()=>{return this.getNextRunTime()},Y=this.options.name||"unnamed-task",B=async()=>{if(!H.isEnabled(Y)){X.debug(`[scheduler] task ${Y} is paused`);return}if(this.options.maxRuns&&$>=this.options.maxRuns){Q();return}$++;try{let O=V();if(O&&typeof O.then==="function")await O}catch(O){if(this.options.catch)this.options.catch(O);else X.error(`[scheduler] task ${Y} failed (no .catch handler installed): ${O instanceof Error?O.message:String(O)}`);throw O}},z={stop:Q,run:B,nextRun:G,pattern:this.intervalMs!==null?`every ${this.intervalMs/1000}s`:this.cronPattern,timezone:this.timezone,name:this.options.name,enabled:H.isEnabled(Y)};if(this.intervalMs!==null)Z=setInterval(()=>{if(W)return;B().catch(()=>{})},this.intervalMs),Z.unref?.();else if(this.cronPattern){let q=()=>{if(W)return;if(this.options.maxRuns&&$>=this.options.maxRuns)return;let _=this.getNextRunTime();if(!_)return;let I=Math.max(_.getTime()-Date.now(),0);if(I>2147483647)Z=setTimeout(()=>{if(W)return;q()},2147483647);else Z=setTimeout(()=>{if(W)return;B().catch(()=>{}),q()},I)};q()}if(this.options.name)H.jobs.set(this.options.name,z);if(U)H.scheduledKeys.set(U,z);return X.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`),z}static job(U){return new H(async()=>{X.info(`Running job: ${U}`);try{await VU(U)}catch(V){throw X.error(`Job ${U} failed:`,V),V}}).withName(U)}static action(U){return new H(async()=>{X.info(`Running action: ${U}`);try{await e(U)}catch(V){throw X.error(`Action ${U} failed:`,V),V}}).withName(U)}static command(U){let V=null;return V=new H(async()=>{let Z=V?.outputPath??null,$=V?.outputAppend??!1;try{if(X.info(`Executing command: ${U}`),Z){let{spawn:G}=await import("child_process"),{openSync:Y,closeSync:B}=await import("fs"),O=Y(Z,$?"a":"w");try{await new Promise((q,_)=>{let I=G(U,{shell:!0,stdio:["ignore",O,O]});I.on("error",_),I.on("exit",(T)=>{if(T===0)q();else _(Error(`Command '${U}' exited with code ${T}`))})})}finally{B(O)}return}let Q=await UU(U);if(Q.isErr)throw X.error(Q.error),Q.error}catch(Q){throw X.error(`Command execution failed: ${Q}`),Q}}),V.withName(`command-${U}`)}static notification(U,V,W,Z){return new H(async()=>{try{let Q=(await import("@stacksjs/notifications").catch(()=>null))?.notify;if(!Q){X.error("schedule.notification: @stacksjs/notifications.notify is not available \u2014 install / wire the notifications package.");return}await Q(U,V,W,Z)}catch($){throw X.error("Scheduled notification failed:",$),$}}).withName("notification")}static async gracefulShutdown(){X.info("Gracefully shutting down scheduled jobs...");for(let[U,V]of H.jobs)X.info(`Stopping job: ${U}`),V.stop();H.jobs.clear(),H.scheduledKeys.clear(),X.info("All jobs have been stopped")}}class WU extends H{}function ZU(U){if(U instanceof Date)return U>new Date?U:null;return K(U)}function vU(U){let V=ZU(U);if(!V)return-1;return V.getTime()-Date.now()}var FU=H;export{vU as timeout,ZU as sendAt,FU as schedule,YU as runScheduler,H as Schedule,WU as Queue};
@@ -69,6 +69,7 @@ export declare class Schedule implements UntimedSchedule {
69
69
  static runNow(name: string): Promise<void>;
70
70
  static setEnabled(name: string, enabled: boolean): void;
71
71
  static isEnabled(name: string): boolean;
72
+ static isScheduled(name: string): boolean;
72
73
  static listLocks(): string[];
73
74
  static job(name: string): UntimedSchedule;
74
75
  static action(name: string): UntimedSchedule;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/scheduler",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.380",
5
+ "version": "0.71.1",
6
6
  "description": "The Stacks scheduler.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,8 +58,8 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "devDependencies": {
61
- "@stacksjs/actions": "0.70.380",
62
- "@stacksjs/cron": "0.70.380",
63
- "better-dx": "^0.2.17"
61
+ "@stacksjs/actions": "0.71.1",
62
+ "@stacksjs/cron": "0.71.1",
63
+ "better-dx": "^0.2.23"
64
64
  }
65
65
  }