@stacksjs/scheduler 0.70.84 → 0.70.86

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.
Files changed (2) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var _=import.meta.require;import{ok as x}from"@stacksjs/error-handling";import{path as P}from"@stacksjs/path";import{schedule as z}from"@stacksjs/scheduler";import{globSync as g}from"@stacksjs/storage";import{snakeCase as S}from"@stacksjs/strings";import{Every as C}from"@stacksjs/types";async function GU(){let U=g([P.appPath("Jobs/*.ts")],{absolute:!0});for(let V of U)try{let Z=(await import(V)).default,$=S(y(Z,V));if(Z.rate)j($,Z.rate)}catch(W){console.error(W)}return await N(),x("Schedules ran successfully")}async function N(){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 j(U,V){switch(V){case C.Minute:z.job(U).everyMinute();break;case C.TwoMinutes:z.job(U).everyTwoMinutes();break;case C.FiveMinutes:z.job(U).everyFiveMinutes();break;case C.TenMinutes:z.job(U).everyTenMinutes();break;case C.ThirtyMinutes:z.job(U).everyThirtyMinutes();break;case C.HalfHour:z.job(U).everyThirtyMinutes();break;case C.Hour:z.job(U).everyHour();break;case C.Day:z.job(U).everyDay();break;case C.Week:z.job(U).weekly();break;case C.Month:z.job(U).monthly();break;case C.Year:z.job(U).yearly();break;default:throw Error(`Unsupported rate: ${V}`)}}function y(U,V){if(U.name)return U.name;return P.basename(V).replace(/\.ts$/,"")}import{spawn as l}from"child_process";import{join as o}from"path";import{runAction as t}from"@stacksjs/actions";import{log as H,runCommand as r}from"@stacksjs/cli";import{parse as K}from"@stacksjs/cron";import{runJob as a}from"@stacksjs/queue";import{createHash as D}from"crypto";import{existsSync as b,mkdirSync as k,readFileSync as p,statSync as h,unlinkSync as m,writeFileSync as T}from"fs";import{join as R}from"path";import w 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=$??R(w.cwd(),"storage","framework","locks"),G=R(Q,`${U}.lock`);if(!b(Q))k(Q,{recursive:!0});let X=!1,B=null;if(Z&&(W==="postgres"||W==="mysql")){if(!await i(U,W,Z))return null;B=async()=>{if(X)return;X=!0,await c(U,W,Z).catch(()=>{})}}let q={acquiredAt:Date.now(),monotonicStart:Math.round(performance.now()),holder:w.pid};try{T(G,JSON.stringify(q),{flag:"wx"})}catch{let O=!1;try{let J=p(G,"utf8"),L=JSON.parse(J);if(u(L)>=V)O=!0}catch{try{let J=h(G);if(Math.max(0,Date.now()-J.mtimeMs)>=V)O=!0}catch{O=!0}}if(!O){if(B)await B();return null}try{T(G,JSON.stringify(q))}catch{if(B)await B();return null}}return{release:async()=>{try{m(G)}catch{}if(B)await B()}}}function v(U){let V=D("sha256").update(`${A}:${U}`).digest();return{key1:V.readInt32BE(0),key2:V.readInt32BE(4)}}async function i(U,V,W){if(V==="postgres"){let{key1:Q,key2:G}=v(U),X=await W.unsafe(`SELECT pg_try_advisory_lock(${Q}, ${G}) AS acquired`);return n(X,"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=D("sha256").update(U).digest("hex").slice(0,32);return`${A}_${W}`}function n(U,V){let W=F(U);if(!W)return!1;let Z=W[V];return Z===!0||Z===1||Z==="1"||Z==="t"}function d(U,V){let W=F(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 F(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 Y{static jobs=new Map;static scheduledKeys=new Map;cronPattern="";intervalMs=null;timezone="America/Los_Angeles";task;static lockDir=o(process.cwd(),"storage","framework","locks");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){H.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 H.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 X=K(this.cronPattern,Q);if(!X||X.getTime()>Z)break;$.push(X),Q=X.getTime()}if($.length>V)H.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 X of $)try{let B=this.task();if(B&&typeof B.then==="function")await B;G++}catch(B){if(H.error(`[scheduler] runMissed slot ${X.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 Y.jobs)U.push({name:V,pattern:W.pattern,timezone:W.timezone,nextRun:W.nextRun?W.nextRun():null});return U}static listLocks(){return Array.from(Y.activeLocks.keys())}async acquireLock(U){if(Y.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){H.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,Y.lockDir);if(!$)return!1;return Y.activeLocks.set(U,$),!0}async releaseLock(U){let V=Y.activeLocks.get(U);if(!V)return;Y.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)){H.info(`Skipping overlapping task: ${V}`);return}try{let G=$();if(G&&typeof G==="object"&&typeof G.finally==="function")await G}catch(G){H.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",X="inherit",B=null;if($)try{let{openSync:O}=_("fs"),J=O($,Q?"a":"w");G=J,X=J,B=J}catch(O){H.warn(`[scheduler] couldn't open ${$} for background task ${V}: ${O instanceof Error?O.message:String(O)}; falling back to inherit`)}let q=l(process.execPath,["-e",`(${Z.toString()})()`],{detached:!0,stdio:["ignore",G,X]});q.on("error",(O)=>{if(H.error(`Background task ${V} failed to spawn: ${O.message}`),B!==null)try{let{closeSync:J}=_("fs");J(B)}catch{}}),q.on("exit",(O,J)=>{if(O!==0&&O!==null)H.error(`[scheduler] background task ${V} (pid: ${q.pid}) exited with code ${O}`);else if(J)H.warn(`[scheduler] background task ${V} (pid: ${q.pid}) terminated by signal ${J}`);if(B!==null)try{let{closeSync:L}=_("fs");L(B)}catch{}}),q.unref(),H.info(`Task ${V} spawned in background (pid: ${q.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:()=>{},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=Y.scheduledKeys.get(U);if(O)return H.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}},X={stop:Q,nextRun:()=>{return this.getNextRunTime()},pattern:this.intervalMs!==null?`every ${this.intervalMs/1000}s`:this.cronPattern,timezone:this.timezone,name:this.options.name},B=this.options.name||"unnamed-task",q=()=>{if(this.options.maxRuns&&$>=this.options.maxRuns){Q();return}$++;try{let O=V();if(O&&typeof O.then==="function")O.catch((J)=>{if(this.options.catch)this.options.catch(J);else H.error(`[scheduler] task ${B} async error (no .catch handler installed): ${J instanceof Error?J.message:String(J)}`)})}catch(O){if(this.options.catch)this.options.catch(O);else H.error(`[scheduler] task ${B} sync error (no .catch handler installed): ${O instanceof Error?O.message:String(O)}`)}};if(this.intervalMs!==null)Z=setInterval(()=>{if(W)return;q()},this.intervalMs),Z.unref?.();else if(this.cronPattern){let J=()=>{if(W)return;if(this.options.maxRuns&&$>=this.options.maxRuns)return;let L=this.getNextRunTime();if(!L)return;let I=Math.max(L.getTime()-Date.now(),0);if(I>2147483647)Z=setTimeout(()=>{if(W)return;J()},2147483647);else Z=setTimeout(()=>{if(W)return;q(),J()},I)};J()}if(this.options.name)Y.jobs.set(this.options.name,X);if(U)Y.scheduledKeys.set(U,X);return H.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`),X}static job(U){return new Y(async()=>{H.info(`Running job: ${U}`);try{await a(U)}catch(V){throw H.error(`Job ${U} failed:`,V),V}}).withName(U)}static action(U){return new Y(async()=>{H.info(`Running action: ${U}`);try{await t(U)}catch(V){throw H.error(`Action ${U} failed:`,V),V}}).withName(U)}static command(U){let V=null;return V=new Y(async()=>{let Z=V?.outputPath??null,$=V?.outputAppend??!1;try{if(H.info(`Executing command: ${U}`),Z){let{spawn:G}=await import("child_process"),{openSync:X,closeSync:B}=await import("fs"),O=X(Z,$?"a":"w");try{await new Promise((J,L)=>{let I=G(U,{shell:!0,stdio:["ignore",O,O]});I.on("error",L),I.on("exit",(M)=>{if(M===0)J();else L(Error(`Command '${U}' exited with code ${M}`))})})}finally{B(O)}return}let Q=await r(U);if(Q.isErr)throw H.error(Q.error),Q.error}catch(Q){throw H.error(`Command execution failed: ${Q}`),Q}}),V.withName(`command-${U}`)}static notification(U,V,W,Z){return new Y(async()=>{try{let Q=(await import("@stacksjs/notifications").catch(()=>null))?.notify;if(!Q){H.error("schedule.notification: @stacksjs/notifications.notify is not available \u2014 install / wire the notifications package.");return}await Q(U,V,W,Z)}catch($){throw H.error("Scheduled notification failed:",$),$}}).withName("notification")}static async gracefulShutdown(){H.info("Gracefully shutting down scheduled jobs...");for(let[U,V]of Y.jobs)H.info(`Stopping job: ${U}`),V.stop();Y.jobs.clear(),H.info("All jobs have been stopped")}}class s extends Y{}function e(U){if(U instanceof Date)return U>new Date?U:null;return K(U)}function AU(U){let V=e(U);if(!V)return-1;return V.getTime()-Date.now()}var MU=Y;export{AU as timeout,e as sendAt,MU as schedule,GU as runScheduler,Y as Schedule,s as Queue};
2
+ var _=import.meta.require;import{ok as x}from"@stacksjs/error-handling";import{path as P}from"@stacksjs/path";import{schedule as z}from"@stacksjs/scheduler";import{globSync as g}from"@stacksjs/storage";import{snakeCase as S}from"@stacksjs/strings";import{Every as C}from"@stacksjs/types";async function GU(){let U=g([P.appPath("Jobs/*.ts")],{absolute:!0});for(let V of U)try{let Z=(await import(V)).default,$=S(y(Z,V));if(Z.rate)j($,Z.rate)}catch(W){console.error(W)}return await N(),x("Schedules ran successfully")}async function N(){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 j(U,V){switch(V){case C.Minute:z.job(U).everyMinute();break;case C.TwoMinutes:z.job(U).everyTwoMinutes();break;case C.FiveMinutes:z.job(U).everyFiveMinutes();break;case C.TenMinutes:z.job(U).everyTenMinutes();break;case C.ThirtyMinutes:z.job(U).everyThirtyMinutes();break;case C.HalfHour:z.job(U).everyThirtyMinutes();break;case C.Hour:z.job(U).everyHour();break;case C.Day:z.job(U).everyDay();break;case C.Week:z.job(U).weekly();break;case C.Month:z.job(U).monthly();break;case C.Year:z.job(U).yearly();break;default:throw Error(`Unsupported rate: ${V}`)}}function y(U,V){if(U.name)return U.name;return P.basename(V).replace(/\.ts$/,"")}import{spawn as l}from"child_process";import{join as o}from"path";import{runAction as t}from"@stacksjs/actions";import{log as H,runCommand as r}from"@stacksjs/cli";import{parse as K}from"@stacksjs/cron";import{runJob as a}from"@stacksjs/queue";import{createHash as D}from"crypto";import{existsSync as b,mkdirSync as k,readFileSync as p,statSync as h,unlinkSync as m,writeFileSync as T}from"fs";import{join as R}from"path";import w from"process";var A="stacks_scheduler";function u(U){let V=Date.now()-U.acquiredAt;if(V<0)return 0;return V}async function f(U,V,W,Z,$){let Q=$??R(w.cwd(),"storage","framework","locks"),G=R(Q,`${U}.lock`);if(!b(Q))k(Q,{recursive:!0});let X=!1,B=null;if(Z&&(W==="postgres"||W==="mysql")){if(!await i(U,W,Z))return null;B=async()=>{if(X)return;X=!0,await c(U,W,Z).catch(()=>{})}}let q={acquiredAt:Date.now(),monotonicStart:Math.round(performance.now()),holder:w.pid};try{T(G,JSON.stringify(q),{flag:"wx"})}catch{let O=!1;try{let J=p(G,"utf8"),L=JSON.parse(J);if(u(L)>=V)O=!0}catch{try{let J=h(G);if(Math.max(0,Date.now()-J.mtimeMs)>=V)O=!0}catch{O=!0}}if(!O){if(B)await B();return null}try{T(G,JSON.stringify(q))}catch{if(B)await B();return null}}return{release:async()=>{try{m(G)}catch{}if(B)await B()}}}function E(U){let V=D("sha256").update(`${A}:${U}`).digest();return{key1:V.readInt32BE(0),key2:V.readInt32BE(4)}}async function i(U,V,W){if(V==="postgres"){let{key1:Q,key2:G}=E(U),X=await W.unsafe(`SELECT pg_try_advisory_lock(${Q}, ${G}) AS acquired`);return n(X,"acquired")}let Z=v(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}=E(U);await W.unsafe(`SELECT pg_advisory_unlock(${$}, ${Q})`);return}let Z=v(U);await W.unsafe(`SELECT RELEASE_LOCK('${Z}')`)}function v(U){let V=`${A}_${U}`.replace(/[^A-Za-z0-9_]/g,"_");if(V.length<=64)return V;let W=D("sha256").update(U).digest("hex").slice(0,32);return`${A}_${W}`}function n(U,V){let W=F(U);if(!W)return!1;let Z=W[V];return Z===!0||Z===1||Z==="1"||Z==="t"}function d(U,V){let W=F(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 F(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 Y{static jobs=new Map;static scheduledKeys=new Map;cronPattern="";intervalMs=null;timezone="America/Los_Angeles";task;static lockDir=o(process.cwd(),"storage","framework","locks");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){H.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 H.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 X=K(this.cronPattern,Q);if(!X||X.getTime()>Z)break;$.push(X),Q=X.getTime()}if($.length>V)H.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 X of $)try{let B=this.task();if(B&&typeof B.then==="function")await B;G++}catch(B){if(H.error(`[scheduler] runMissed slot ${X.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 Y.jobs)U.push({name:V,pattern:W.pattern,timezone:W.timezone,nextRun:W.nextRun?W.nextRun():null});return U}static listLocks(){return Array.from(Y.activeLocks.keys())}async acquireLock(U){if(Y.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){H.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 f(U,V,W,Z,Y.lockDir);if(!$)return!1;return Y.activeLocks.set(U,$),!0}async releaseLock(U){let V=Y.activeLocks.get(U);if(!V)return;Y.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)){H.info(`Skipping overlapping task: ${V}`);return}try{let G=$();if(G&&typeof G==="object"&&typeof G.finally==="function")await G}catch(G){H.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",X="inherit",B=null;if($)try{let{openSync:O}=_("fs"),J=O($,Q?"a":"w");G=J,X=J,B=J}catch(O){H.warn(`[scheduler] couldn't open ${$} for background task ${V}: ${O instanceof Error?O.message:String(O)}; falling back to inherit`)}let q=l(process.execPath,["-e",`(${Z.toString()})()`],{detached:!0,stdio:["ignore",G,X]});q.on("error",(O)=>{if(H.error(`Background task ${V} failed to spawn: ${O.message}`),B!==null)try{let{closeSync:J}=_("fs");J(B)}catch{}}),q.on("exit",(O,J)=>{if(O!==0&&O!==null)H.error(`[scheduler] background task ${V} (pid: ${q.pid}) exited with code ${O}`);else if(J)H.warn(`[scheduler] background task ${V} (pid: ${q.pid}) terminated by signal ${J}`);if(B!==null)try{let{closeSync:L}=_("fs");L(B)}catch{}}),q.unref(),H.info(`Task ${V} spawned in background (pid: ${q.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:()=>{},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=Y.scheduledKeys.get(U);if(O)return H.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}},X={stop:Q,nextRun:()=>{return this.getNextRunTime()},pattern:this.intervalMs!==null?`every ${this.intervalMs/1000}s`:this.cronPattern,timezone:this.timezone,name:this.options.name},B=this.options.name||"unnamed-task",q=()=>{if(this.options.maxRuns&&$>=this.options.maxRuns){Q();return}$++;try{let O=V();if(O&&typeof O.then==="function")O.catch((J)=>{if(this.options.catch)this.options.catch(J);else H.error(`[scheduler] task ${B} async error (no .catch handler installed): ${J instanceof Error?J.message:String(J)}`)})}catch(O){if(this.options.catch)this.options.catch(O);else H.error(`[scheduler] task ${B} sync error (no .catch handler installed): ${O instanceof Error?O.message:String(O)}`)}};if(this.intervalMs!==null)Z=setInterval(()=>{if(W)return;q()},this.intervalMs),Z.unref?.();else if(this.cronPattern){let J=()=>{if(W)return;if(this.options.maxRuns&&$>=this.options.maxRuns)return;let L=this.getNextRunTime();if(!L)return;let I=Math.max(L.getTime()-Date.now(),0);if(I>2147483647)Z=setTimeout(()=>{if(W)return;J()},2147483647);else Z=setTimeout(()=>{if(W)return;q(),J()},I)};J()}if(this.options.name)Y.jobs.set(this.options.name,X);if(U)Y.scheduledKeys.set(U,X);return H.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`),X}static job(U){return new Y(async()=>{H.info(`Running job: ${U}`);try{await a(U)}catch(V){throw H.error(`Job ${U} failed:`,V),V}}).withName(U)}static action(U){return new Y(async()=>{H.info(`Running action: ${U}`);try{await t(U)}catch(V){throw H.error(`Action ${U} failed:`,V),V}}).withName(U)}static command(U){let V=null;return V=new Y(async()=>{let Z=V?.outputPath??null,$=V?.outputAppend??!1;try{if(H.info(`Executing command: ${U}`),Z){let{spawn:G}=await import("child_process"),{openSync:X,closeSync:B}=await import("fs"),O=X(Z,$?"a":"w");try{await new Promise((J,L)=>{let I=G(U,{shell:!0,stdio:["ignore",O,O]});I.on("error",L),I.on("exit",(M)=>{if(M===0)J();else L(Error(`Command '${U}' exited with code ${M}`))})})}finally{B(O)}return}let Q=await r(U);if(Q.isErr)throw H.error(Q.error),Q.error}catch(Q){throw H.error(`Command execution failed: ${Q}`),Q}}),V.withName(`command-${U}`)}static notification(U,V,W,Z){return new Y(async()=>{try{let Q=(await import("@stacksjs/notifications").catch(()=>null))?.notify;if(!Q){H.error("schedule.notification: @stacksjs/notifications.notify is not available \u2014 install / wire the notifications package.");return}await Q(U,V,W,Z)}catch($){throw H.error("Scheduled notification failed:",$),$}}).withName("notification")}static async gracefulShutdown(){H.info("Gracefully shutting down scheduled jobs...");for(let[U,V]of Y.jobs)H.info(`Stopping job: ${U}`),V.stop();Y.jobs.clear(),H.info("All jobs have been stopped")}}class s extends Y{}function e(U){if(U instanceof Date)return U>new Date?U:null;return K(U)}function AU(U){let V=e(U);if(!V)return-1;return V.getTime()-Date.now()}var MU=Y;export{AU as timeout,e as sendAt,MU as schedule,GU as runScheduler,Y as Schedule,s as Queue};
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@stacksjs/scheduler",
3
3
  "type": "module",
4
- "version": "0.70.84",
4
+ "sideEffects": false,
5
+ "version": "0.70.86",
5
6
  "description": "The Stacks scheduler.",
6
7
  "author": "Chris Breuer",
7
8
  "contributors": [
@@ -52,8 +53,8 @@
52
53
  "prepublishOnly": "bun run build"
53
54
  },
54
55
  "devDependencies": {
55
- "@stacksjs/actions": "0.70.84",
56
- "@stacksjs/cron": "0.70.84",
56
+ "@stacksjs/actions": "0.70.86",
57
+ "@stacksjs/cron": "0.70.86",
57
58
  "better-dx": "^0.2.16"
58
59
  }
59
60
  }