@stacksjs/scheduler 0.75.451 → 0.75.457

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
- import{ok as _}from"@stacksjs/error-handling";import{log as O}from"@stacksjs/logging";import{path as N}from"@stacksjs/path";import{schedule as A}from"@stacksjs/scheduler";import{globSync as U}from"@stacksjs/storage";async function xt(){let t=U([N.appPath("Jobs/*.ts")],{absolute:!0});await B();for(let e of t)try{let r=(await import(e)).default,s=K(e);if(r.enabled===!1||!r.rate)continue;let i=[s,r.name].find((o)=>typeof o==="string"&&A.isScheduled(o));if(i){O.debug(`[scheduler] ${i} is declared in app/Scheduler.ts; ignoring its \`rate\` so it is not scheduled twice`);continue}H(s,r.rate)}catch(n){O.error(`[scheduler] could not schedule ${e}: ${n instanceof Error?n.message:String(n)}`)}return _("Schedules ran successfully")}async function B(){let t=N.appPath("Scheduler.ts");try{let e=await import(t);if(typeof e.default==="function")e.default();else console.warn(`Scheduler file ${t} does not export a default function`)}catch(e){console.warn(`Could not load scheduler file ${t}:`,e)}}function H(t,e){A.job(t).cron(e)}function K(t){return N.basename(t).replace(/\.ts$/,"")}import{spawn as st}from"child_process";import{createHash as it}from"crypto";import{closeSync as ot,mkdirSync as at,openSync as ut,readFileSync as ct,renameSync as lt,writeFileSync as dt}from"fs";import{join as P}from"path";import{runNamedAction as ht}from"@stacksjs/action-runner";import{log as c,runCommand as ft}from"@stacksjs/cli";import{config as pt}from"@stacksjs/config";import{parse as S}from"@stacksjs/cron";import{job as mt,runJob as gt}from"@stacksjs/queue";import{createHash as E,randomUUID as Q}from"crypto";import{existsSync as X,mkdirSync as G,readFileSync as M,statSync as W,unlinkSync as Y,writeFileSync as R}from"fs";import{join as j}from"path";import I from"process";var J="stacks_scheduler";function Z(t){let e=Date.now()-t.acquiredAt;if(e<0)return 0;return e}function V(t){let e=t.replace(/[^\w.-]+/g,"_").replace(/^\.+/,"_").slice(0,80),n=E("sha256").update(t).digest("hex").slice(0,12);return`${e}-${n}.lock`}async function q(t,e,n,r,s){let i=s??j(I.cwd(),"storage","framework","locks"),o=j(i,V(t));if(!X(i))G(i,{recursive:!0});let a=!1,l=null;if(r&&(n==="postgres"||n==="mysql")){if(!await tt(t,n,r))return null;l=async()=>{if(a)return;a=!0,await et(t,n,r).catch(()=>{})}}let f={token:Q(),acquiredAt:Date.now(),monotonicStart:Math.round(performance.now()),holder:I.pid};try{R(o,JSON.stringify(f),{flag:"wx"})}catch{let p=!1;try{let d=M(o,"utf8"),g=JSON.parse(d);if(Z(g)>=e)p=!0}catch{try{let d=W(o);if(Math.max(0,Date.now()-d.mtimeMs)>=e)p=!0}catch{p=!0}}if(!p){if(l)await l();return null}try{R(o,JSON.stringify(f))}catch{if(l)await l();return null}}let h=!1;return{release:async()=>{if(h)return;h=!0;try{if(JSON.parse(M(o,"utf8")).token===f.token)Y(o)}catch{}if(l)await l()}}}function z(t){let e=E("sha256").update(`${J}:${t}`).digest();return{key1:e.readInt32BE(0),key2:e.readInt32BE(4)}}async function tt(t,e,n){if(e==="postgres"){let{key1:i,key2:o}=z(t),a=await n.unsafe(`SELECT pg_try_advisory_lock(${i}, ${o}) AS acquired`);return nt(a,"acquired")}let r=L(t),s=await n.unsafe(`SELECT GET_LOCK('${r}', 0) AS acquired`);return rt(s,"acquired")===1}async function et(t,e,n){if(e==="postgres"){let{key1:s,key2:i}=z(t);await n.unsafe(`SELECT pg_advisory_unlock(${s}, ${i})`);return}let r=L(t);await n.unsafe(`SELECT RELEASE_LOCK('${r}')`)}function L(t){let e=E("sha256").update(t).digest("hex").slice(0,32);return`${J}_${e}`}function nt(t,e){let n=F(t);if(!n)return!1;let r=n[e];return r===!0||r===1||r==="1"||r==="t"}function rt(t,e){let n=F(t);if(!n)return null;let r=n[e];if(typeof r==="number")return r;if(typeof r==="string"&&/^-?\d+$/.test(r))return Number.parseInt(r,10);return null}function F(t){if(!t)return null;if(Array.isArray(t))return t[0];if(typeof t==="object"&&"rows"in t&&Array.isArray(t.rows))return t.rows[0];return null}function yt(...t){let e=JSON.stringify(t,(n,r)=>{if(typeof r==="bigint")return`${r}n`;if(typeof r==="function")return`[function ${r.name}]`;if(r&&typeof r==="object"&&!Array.isArray(r))return Object.fromEntries(Object.entries(r).sort(([s],[i])=>s.localeCompare(i)));return r});return`notification-${it("sha256").update(e).digest("hex").slice(0,12)}`}function C(t,e){let n=t instanceof Date?new Date(t.getTime()):new Date(t);if(Number.isNaN(n.getTime()))throw Error(`between(): ${e} is not a valid date: ${String(t)}`);return n}class u{static jobs=new Map;static scheduledKeys=new Map;cronPattern="";intervalMs=null;timezone=null;task;static lockDir=P(process.cwd(),"storage","framework","locks");static stateFile=P(process.cwd(),"storage","framework","runtime","scheduler-state.json");static activeLocks=new Map;static inProcessRuns=new Map;shouldPreventOverlap=!1;overlapExpiresAfterMinutes=1440;shouldRunOnOneServer=!1;shouldRunInBackground=!1;outputPath=null;outputAppend=!1;handlesOwnOutput=!1;jobName=null;dispatchQueue=null;options={};constructor(t){this.task=t,queueMicrotask(()=>{try{this.start()}catch(e){c.error(`Failed to start scheduled task: ${e}`)}})}get pattern(){return this.cronPattern}cronFields(){let t=this.cronPattern.trim().split(/\s+/);return t.length===5?t:["0","0","*","*","*"]}setTimeOfDay(t,e){let[,,n,r,s]=this.cronFields();return this.releaseInterval(),this.cronPattern=`${t} ${e} ${n} ${r} ${s}`,this}setDays(t,e,n){let[r,s]=this.cronFields();return this.releaseInterval(),this.cronPattern=`${r} ${s} ${t} ${e} ${n}`,this}releaseInterval(){this.intervalMs=null}everySecond(){return this.intervalMs=1000,this.cronPattern="@every_second",this}everyMinute(){return this.setTimeOfDay("*","*")}everyTwoMinutes(){return this.setTimeOfDay("*/2","*")}everyFiveMinutes(){return this.setTimeOfDay("*/5","*")}everyTenMinutes(){return this.setTimeOfDay("*/10","*")}everyThirtyMinutes(){return this.setTimeOfDay("*/30","*")}everyHour(){return this.setTimeOfDay("0","*")}everyDay(){return this.setDays("*","*","*")}hourly(){return this.setTimeOfDay("0","*")}daily(){return this.setDays("*","*","*")}weekly(){return this.setDays("*","*","0")}monthly(){return this.setDays("1","*","*")}yearly(){return this.setDays("1","1","*")}annually(){return this.setDays("1","1","*")}cron(t){let e=String(t).trim().split(/\s+/).filter(Boolean);if(e.length===6){let[s,...i]=e;if(i.every((o)=>o==="*")){let o=s==="*"?1:Number(/^\*\/(\d+)$/.exec(s)?.[1]);if(Number.isInteger(o)&&o>=1&&o<60&&60%o===0)return this.intervalMs=o*1000,this.cronPattern=o===1?"@every_second":`@every_${o}_seconds`,this}if(s!=="0")throw Error(`cron(): '${t}' runs on particular seconds, which the scheduler cannot do. Use '*' or '*/N' (N dividing 60) with every other field '*', or a seconds field of 0`);e.shift()}if(e.length!==5)throw Error(`cron(): '${t}' is not a cron expression; expected 5 fields, or 6 with leading seconds`);let n=e.join(" "),r;try{r=S(n,Date.now(),{tz:"UTC"})}catch(s){throw Error(`cron(): '${t}' is not a valid cron expression: ${s instanceof Error?s.message:String(s)}`)}if(!r)throw Error(`cron(): '${t}' never matches a date`);return this.intervalMs=null,this.cronPattern=n,this}dailyAt(t){return this.daily().at(t)}weekdays(){return this.setDays("*","*","1-5")}onDays(t){if(!Array.isArray(t)||t.length===0)throw Error(`onDays() requires a non-empty array; got ${JSON.stringify(t)}`);for(let e of t)if(!Number.isInteger(e)||e<0||e>6)throw Error(`onDays(): each day must be an integer 0-6 (Sun-Sat). Got ${e}`);return this.setDays("*","*",t.join(","))}at(t){let e=t.split(":");if(e.length!==2||e[0]===""||e[1]==="")throw Error(`Invalid time format "${t}". Expected "HH:MM" (e.g., "14:30")`);let[n,r]=e.map(Number);if(n===void 0||r===void 0||Number.isNaN(n)||Number.isNaN(r)||n<0||n>23||r<0||r>59)throw Error(`Invalid time "${t}". Hour must be 0-23, minute must be 0-59`);return this.setTimeOfDay(String(r),String(n))}setTimeZone(t){return this.timezone=t,this.options.timezone=t,this}withErrorHandler(t){return this.options.catch=t,this}withMaxRuns(t){return this.options.maxRuns=t,this}withProtection(t){return this.options.protect=t||!0,this}withName(t){return this.options.name=t,this}withContext(t){return this.options.context=t,this}withInterval(t){if(typeof t!=="number"||!Number.isFinite(t)||t<=0)throw Error(`withInterval() takes a positive number of seconds; got ${String(t)}`);return this.options.interval=t,this}between(t,e){let n=C(t,"startAt"),r=C(e,"stopAt");if(r.getTime()<=n.getTime())throw Error(`between(): stopAt (${r.toISOString()}) must be after startAt (${n.toISOString()})`);return this.options.startAt=n,this.options.stopAt=r,this}onQueue(t){if(this.jobName===null)throw Error("onQueue() applies to schedule.job(...) only: an action, command or callback has no job to dispatch");if(typeof t!=="string"||t.trim()==="")throw Error(`onQueue() takes a queue name; got ${JSON.stringify(t)}`);return this.dispatchQueue=t,this}withoutOverlapping(t){if(this.shouldPreventOverlap=!0,t!==void 0)this.overlapExpiresAfterMinutes=t;return this}onOneServer(){return this.shouldRunOnOneServer=!0,this}runInBackground(){return this.shouldRunInBackground=!0,this}sendOutputTo(t){return this.outputPath=t,this.outputAppend=!1,this}appendOutputTo(t){return this.outputPath=t,this.outputAppend=!0,this}async runMissed(t){if(!this.cronPattern||this.intervalMs!==null)return c.warn("[scheduler] runMissed() requires a cron-based schedule; interval-based tasks (everySecond) have no concept of missed slots"),0;let e=t.max??100,n=t.since instanceof Date?t.since.getTime():t.since,r=Date.now();if(n>=r)return 0;let s=this.zone(),i=s!=="UTC"?{tz:s}:{},o=(p)=>{let d=[],g=p;while(!0){let m=S(this.cronPattern,g,i);if(!m||m.getTime()>r)return d;d.push(m),g=m.getTime()}},a=60000*(e+1),l=Math.max(n,r-a),f=o(l);while(f.length<=e&&l>n)a*=2,l=Math.max(n,r-a),f=o(l);if(f.length>e){let p=l===n?`${f.length}`:`more than ${e}`;c.warn(`[scheduler] runMissed: ${p} missed slots since ${new Date(n).toISOString()}, running the ${e} most recent - older runs dropped`),f=f.slice(-e)}let h=0;for(let p of f)try{let d=this.task(this.options.context);if(d&&typeof d.then==="function")await d;h++}catch(d){if(c.error(`[scheduler] runMissed slot ${p.toISOString()} failed: ${d instanceof Error?d.message:String(d)}`),this.options.catch)this.options.catch(d)}return h}static listJobs(){let t=[];for(let[e,n]of u.jobs)t.push({name:e,pattern:n.pattern,timezone:n.timezone,nextRun:n.nextRun?n.nextRun():null,enabled:u.isEnabled(e)});return t}static async runNow(t,e={}){let n=u.jobs.get(t);if(!n)throw Error(`Scheduled task "${t}" was not found.`);if(!u.isEnabled(t))throw Error(`Scheduled task "${t}" is paused.`);let r=e.inProcess?u.inProcessRuns.get(t):void 0;await(r?r():n.run())}static setEnabled(t,e){let n=u.disabledJobs();if(e)n.delete(t);else n.add(t);let r=P(process.cwd(),"storage","framework","runtime");at(r,{recursive:!0});let s=`${u.stateFile}.${process.pid}.tmp`;dt(s,`${JSON.stringify({disabled:[...n].sort()},null,2)}
3
- `,{mode:384}),lt(s,u.stateFile)}static isEnabled(t){return!u.disabledJobs().has(t)}static isScheduled(t){return u.jobs.has(t)}static disabledJobs(){try{let t=JSON.parse(ct(u.stateFile,"utf8"));return new Set(Array.isArray(t.disabled)?t.disabled.filter((e)=>typeof e==="string"):[])}catch{return new Set}}static listLocks(){return Array.from(u.activeLocks.keys())}async acquireLock(t){if(u.activeLocks.has(t))return!1;let e=this.overlapExpiresAfterMinutes*60*1000,n=null,r=null;if(this.shouldRunOnOneServer)try{let{db:i}=await import("@stacksjs/database/runtime");r=i;let o=(await import("@stacksjs/env")).env.DB_CONNECTION||"sqlite";if(o==="postgres"||o==="mysql"||o==="sqlite")n=o}catch(i){c.warn(`[scheduler] onOneServer() couldn't reach DB; falling back to file-only lock (which only serializes within this process): ${i instanceof Error?i.message:String(i)}`)}let s=await q(t,e,n,r,u.lockDir);if(!s)return!1;return u.activeLocks.set(t,s),!0}async releaseLock(t){let e=u.activeLocks.get(t);if(!e)return;u.activeLocks.delete(t),await e.release()}wrapTask(t,e=!1){let n=this.options.name||"unnamed-task",r=t;if(this.shouldPreventOverlap||this.shouldRunOnOneServer){let s=this,i=r;r=()=>(async()=>{if(!await s.acquireLock(n)){c.info(`Skipping overlapping task: ${n}`);return}try{let a=i();if(a&&typeof a==="object"&&typeof a.finally==="function")await a}finally{try{await s.releaseLock(n)}catch(a){c.error(`[scheduler] releasing the lock for ${n} failed: ${a instanceof Error?a.message:String(a)}`)}}})()}if(this.shouldRunInBackground&&!e){let s=P(process.cwd(),"buddy"),i=this.handlesOwnOutput?null:this.outputPath,o=this.outputAppend;r=()=>{let a="inherit";if(i)try{a=ut(i,o?"a":"w")}catch(h){c.warn(`[scheduler] couldn't open ${i} for background task ${n}: ${h instanceof Error?h.message:String(h)}; its output goes to this process instead`)}let l=()=>{if(typeof a!=="number")return;try{ot(a)}catch{}a="inherit"},f=st(s,["schedule:run-one",n,"--in-process"],{cwd:process.cwd(),detached:!0,stdio:["ignore",a,a]});l(),f.on("error",(h)=>{c.error(`[scheduler] background task ${n} failed to start (${s}): ${h.message}`)}),f.on("exit",(h,p)=>{if(h!==0&&h!==null)c.error(`[scheduler] background task ${n} (pid: ${f.pid}) exited with code ${h}`);else if(p)c.warn(`[scheduler] background task ${n} (pid: ${f.pid}) terminated by signal ${p}`)}),f.unref(),c.info(`Task ${n} spawned in background (pid: ${f.pid})`)}}return r}getNextRunTime(){if(!this.cronPattern||this.intervalMs!==null)return null;let{startAt:t,stopAt:e}=this.options,n=Date.now(),r=t&&t.getTime()>n?t.getTime()-1:n,s=this.nextCronTime(r);if(s&&e&&s.getTime()>e.getTime())return null;return s}zone(){if(this.timezone)return this.timezone;return pt.app?.timezone||"UTC"}nextCronTime(t){let e=this.zone();if(e==="UTC")return S(this.cronPattern,t);return S(this.cronPattern,t,{tz:e})}start(){if(!this.cronPattern&&this.intervalMs===null)return{stop:()=>{},run:async()=>{},nextRun:()=>null};let t=this.options.name?`${this.options.name.toLowerCase().replace(/[^a-z0-9]+/g,"")}::${this.cronPattern||`every:${this.intervalMs}`}`:null;if(t){let k=u.scheduledKeys.get(t);if(k)return c.debug(`[scheduler] duplicate schedule for "${this.options.name}" (${this.cronPattern||`every ${this.intervalMs}ms`}) skipped`),k}if(this.shouldRunInBackground&&!this.options.name)throw Error("runInBackground() needs a named task: add .withName(...) so the background process can find it");let e=this.options.context,n=e===void 0?this.task:()=>this.task(e),r=this.wrapTask(n),s=this.shouldRunInBackground?this.wrapTask(n,!0):r,i=!1,o=!1,a=null,l=null,f=0,h=()=>{if(i=!0,l!==null){if(this.intervalMs!==null)clearInterval(l);else clearTimeout(l);l=null}},p=()=>this.getNextRunTime(),d=this.options.name||"unnamed-task",g=async(k)=>{if(!u.isEnabled(d)){c.debug(`[scheduler] task ${d} is paused`);return}let{startAt:w,stopAt:v,interval:b,protect:D}=this.options,x=Date.now();if(v&&x>v.getTime()){c.debug(`[scheduler] task ${d} is past its between() window; stopping`),h();return}if(w&&x<w.getTime()){c.debug(`[scheduler] task ${d} has not reached its between() window`);return}if(this.options.maxRuns&&f>=this.options.maxRuns){h();return}if(D&&o){if(c.debug(`[scheduler] task ${d} is still running; skipping this tick (withProtection)`),typeof D==="function")D(T);return}if(b&&a!==null&&x-a<b*1000){c.debug(`[scheduler] task ${d} ran less than ${b}s ago; skipping this tick (withInterval)`);return}f++,o=!0,a=x;try{let y=k();if(y&&typeof y.then==="function")await y}catch(y){if(this.options.catch)this.options.catch(y);else c.error(`[scheduler] task ${d} failed (no withErrorHandler() installed): ${y instanceof Error?y.message:String(y)}`);throw y}finally{o=!1}},m=()=>g(r),T={stop:h,run:m,nextRun:p,pattern:this.intervalMs!==null?`every ${this.intervalMs/1000}s`:this.cronPattern,timezone:this.zone(),name:this.options.name,enabled:u.isEnabled(d)};if(this.intervalMs!==null)l=setInterval(()=>{if(i)return;m().catch(()=>{})},this.intervalMs),l.unref?.();else if(this.cronPattern){let w=()=>{if(i)return;if(this.options.maxRuns&&f>=this.options.maxRuns)return;let v=this.getNextRunTime();if(!v){h();return}let b=Math.max(v.getTime()-Date.now(),0);if(b>2147483647)l=setTimeout(()=>{if(i)return;w()},2147483647);else l=setTimeout(()=>{if(i)return;m().catch(()=>{}),w()},b)};w()}if(this.options.name)u.jobs.set(this.options.name,T),u.inProcessRuns.set(this.options.name,()=>g(s));if(t)u.scheduledKeys.set(t,T);return c.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.zone()}`),T}static job(t){let e=null;return e=new u(async(n)=>{let r=e?.dispatchQueue??null;try{if(r!==null){c.info(`Dispatching job: ${t} to queue ${r}`);let s=mt(t).onQueue(r);await(n===void 0?s:s.withContext(n)).dispatch();return}c.info(`Running job: ${t}`),await gt(t,n===void 0?{}:{context:n})}catch(s){throw c.error(`Job ${t} failed:`,s),s}}),e.jobName=t,e.withName(t)}static action(t){return new u(async()=>{c.info(`Running action: ${t}`);try{await ht(t)}catch(e){throw c.error(`Action ${t} failed:`,e),e}}).withName(t)}static command(t){let e=null;return e=new u(async()=>{let r=e?.outputPath??null,s=e?.outputAppend??!1;try{if(c.info(`Executing command: ${t}`),r){let{spawn:o}=await import("child_process"),{openSync:a,closeSync:l}=await import("fs"),h=a(r,s?"a":"w");try{await new Promise((p,d)=>{let g=o(t,{shell:!0,stdio:["ignore",h,h]});g.on("error",d),g.on("exit",(m)=>{if(m===0)p();else d(Error(`Command '${t}' exited with code ${m}`))})})}finally{l(h)}return}let i=await ft(t);if(i.isErr)throw c.error(i.error),i.error}catch(i){throw c.error(`Command execution failed: ${i}`),i}}),e.handlesOwnOutput=!0,e.withName(`command-${t}`)}static notification(t,e,n,r){return new u(async()=>{try{let{notify:s}=await import("@stacksjs/notifications"),i=await s(t,e,n,r),o=i.filter((a)=>!a.success);if(o.length>0){let a=o.map((l)=>`${l.channel}: ${l.error?.message??"not delivered"}`).join("; ");throw Error(`Scheduled notification was not delivered on ${o.length} of ${i.length} channel(s) - ${a}`)}}catch(s){throw c.error("Scheduled notification failed:",s),s}}).withName(yt(t,e,n,r))}static async gracefulShutdown(){c.info("Gracefully shutting down scheduled jobs...");for(let[t,e]of u.jobs)c.info(`Stopping job: ${t}`),e.stop();u.jobs.clear(),u.inProcessRuns.clear(),u.scheduledKeys.clear(),c.info("All jobs have been stopped")}}class wt extends u{}function bt(t){if(t instanceof Date)return t>new Date?t:null;return S(t)}function _t(t){let e=bt(t);if(!e)return-1;return e.getTime()-Date.now()}var Ut=u;export{wt as Queue,u as Schedule,xt as runScheduler,Ut as schedule,bt as sendAt,_t as timeout};
2
+ import{ok as Y}from"@stacksjs/error-handling";import{log as C}from"@stacksjs/logging";import{path as M}from"@stacksjs/path";import{schedule as F}from"@stacksjs/scheduler";import{globSync as V}from"@stacksjs/storage";async function jt(){let t=V([M.appPath("Jobs/*.ts")],{absolute:!0});await Z();for(let e of t)try{let r=(await import(e)).default,s=et(e);if(r.enabled===!1||!r.rate)continue;let i=[s,r.name].find((o)=>typeof o==="string"&&F.isScheduled(o));if(i){C.debug(`[scheduler] ${i} is declared in app/Scheduler.ts; ignoring its \`rate\` so it is not scheduled twice`);continue}tt(s,r.rate)}catch(n){C.error(`[scheduler] could not schedule ${e}: ${n instanceof Error?n.message:String(n)}`)}return Y("Schedules ran successfully")}async function Z(){let t=M.appPath("Scheduler.ts");try{let e=await import(t);if(typeof e.default==="function")e.default();else console.warn(`Scheduler file ${t} does not export a default function`)}catch(e){console.warn(`Could not load scheduler file ${t}:`,e)}}function tt(t,e){F.job(t).cron(e)}function et(t){return M.basename(t).replace(/\.ts$/,"")}import{spawn as pt}from"child_process";import{createHash as gt}from"crypto";import{closeSync as wt,mkdirSync as yt,openSync as bt,readFileSync as kt,renameSync as St,writeFileSync as vt}from"fs";import{join as O}from"path";import{runNamedAction as $t}from"@stacksjs/action-runner";import{log as a,runCommand as Tt}from"@stacksjs/cli";import{config as Et}from"@stacksjs/config";import{parse as E}from"@stacksjs/cron";import{job as Nt,runJob as Dt}from"@stacksjs/queue";import{randomUUID as ht}from"crypto";import{createHash as R,randomUUID as nt}from"crypto";import{existsSync as rt,mkdirSync as st,readFileSync as q,statSync as it,unlinkSync as ot,writeFileSync as J}from"fs";import{join as z}from"path";import U from"process";var _="stacks_scheduler";function at(t){let e=Date.now()-t.acquiredAt;if(e<0)return 0;return e}function I(t){let e=t.replace(/[^\w.-]+/g,"_").replace(/^\.+/,"_").slice(0,80),n=R("sha256").update(t).digest("hex").slice(0,12);return`${e}-${n}.lock`}async function B(t,e,n,r,s){let i=s??z(U.cwd(),"storage","framework","locks"),o=z(i,I(t));if(!rt(i))st(i,{recursive:!0});let u=!1,l=null;if(r&&(n==="postgres"||n==="mysql")){if(!await ut(t,n,r))return null;l=async()=>{if(u)return;u=!0,await ct(t,n,r).catch(()=>{})}}let p={token:nt(),acquiredAt:Date.now(),monotonicStart:Math.round(performance.now()),holder:U.pid};try{J(o,JSON.stringify(p),{flag:"wx"})}catch{let g=!1;try{let f=q(o,"utf8"),w=JSON.parse(f);if(at(w)>=e)g=!0}catch{try{let f=it(o);if(Math.max(0,Date.now()-f.mtimeMs)>=e)g=!0}catch{g=!0}}if(!g){if(l)await l();return null}try{J(o,JSON.stringify(p))}catch{if(l)await l();return null}}let h=!1;return{release:async()=>{if(h)return;h=!0;try{if(JSON.parse(q(o,"utf8")).token===p.token)ot(o)}catch{}if(l)await l()}}}function H(t){let e=R("sha256").update(`${_}:${t}`).digest();return{key1:e.readInt32BE(0),key2:e.readInt32BE(4)}}async function ut(t,e,n){if(e==="postgres"){let{key1:i,key2:o}=H(t),u=await n.unsafe(`SELECT pg_try_advisory_lock(${i}, ${o}) AS acquired`);return lt(u,"acquired")}let r=K(t),s=await n.unsafe(`SELECT GET_LOCK('${r}', 0) AS acquired`);return dt(s,"acquired")===1}async function ct(t,e,n){if(e==="postgres"){let{key1:s,key2:i}=H(t);await n.unsafe(`SELECT pg_advisory_unlock(${s}, ${i})`);return}let r=K(t);await n.unsafe(`SELECT RELEASE_LOCK('${r}')`)}function K(t){let e=R("sha256").update(t).digest("hex").slice(0,32);return`${_}_${e}`}function lt(t,e){let n=x(t);if(!n)return!1;let r=n[e];return r===!0||r===1||r==="1"||r==="t"}function dt(t,e){let n=x(t);if(!n)return null;let r=n[e];if(typeof r==="number")return r;if(typeof r==="string"&&/^-?\d+$/.test(r))return Number.parseInt(r,10);return null}function x(t){if(!t)return null;if(Array.isArray(t))return t[0];if(typeof t==="object"&&"rows"in t&&Array.isArray(t.rows))return t.rows[0];return null}var T="scheduler_claims",ft=86400000;function Q(){return`CREATE TABLE IF NOT EXISTS ${T} (task VARCHAR(128) NOT NULL,slot BIGINT NOT NULL,claimant CHAR(36) NOT NULL,PRIMARY KEY (task, slot))`}var L=new WeakMap;function mt(t){let e=L.get(t);if(!e)e=(async()=>{try{await t.unsafe(Q())}catch{await t.unsafe(Q())}})(),L.set(t,e),e.catch(()=>L.delete(t));return e}async function W(t,e,n,r){if(!r||n!=="postgres"&&n!=="mysql")return!0;await mt(r);let s=I(t).replace(/\.lock$/,""),i=Math.trunc(e),o=ht(),u=`('${s}', ${i}, '${o}')`;await r.unsafe(n==="postgres"?`INSERT INTO ${T} (task, slot, claimant) VALUES ${u} ON CONFLICT (task, slot) DO NOTHING`:`INSERT INTO ${T} (task, slot, claimant) VALUES ${u} ON DUPLICATE KEY UPDATE task = task`);let l=x(await r.unsafe(`SELECT claimant FROM ${T} WHERE task = '${s}' AND slot = ${i}`)),p=typeof l?.claimant==="string"&&l.claimant.trim()===o;if(p)await r.unsafe(`DELETE FROM ${T} WHERE task = '${s}' AND slot < ${i-ft}`);return p}function xt(...t){let e=JSON.stringify(t,(n,r)=>{if(typeof r==="bigint")return`${r}n`;if(typeof r==="function")return`[function ${r.name}]`;if(r&&typeof r==="object"&&!Array.isArray(r))return Object.fromEntries(Object.entries(r).sort(([s],[i])=>s.localeCompare(i)));return r});return`notification-${gt("sha256").update(e).digest("hex").slice(0,12)}`}var G=Symbol("skipped-by-lock");function X(t,e){let n=t instanceof Date?new Date(t.getTime()):new Date(t);if(Number.isNaN(n.getTime()))throw Error(`between(): ${e} is not a valid date: ${String(t)}`);return n}class c{static jobs=new Map;static scheduledKeys=new Map;cronPattern="";intervalMs=null;timezone=null;task;static lockDir=O(process.cwd(),"storage","framework","locks");static stateFile=O(process.cwd(),"storage","framework","runtime","scheduler-state.json");static activeLocks=new Map;static inProcessRuns=new Map;shouldPreventOverlap=!1;overlapExpiresAfterMinutes=1440;shouldRunOnOneServer=!1;shouldRunInBackground=!1;outputPath=null;outputAppend=!1;handlesOwnOutput=!1;jobName=null;dispatchQueue=null;options={};constructor(t){this.task=t,queueMicrotask(()=>{try{this.start()}catch(e){a.error(`Failed to start scheduled task: ${e}`)}})}get pattern(){return this.cronPattern}cronFields(){let t=this.cronPattern.trim().split(/\s+/);return t.length===5?t:["0","0","*","*","*"]}setTimeOfDay(t,e){let[,,n,r,s]=this.cronFields();return this.releaseInterval(),this.cronPattern=`${t} ${e} ${n} ${r} ${s}`,this}setDays(t,e,n){let[r,s]=this.cronFields();return this.releaseInterval(),this.cronPattern=`${r} ${s} ${t} ${e} ${n}`,this}releaseInterval(){this.intervalMs=null}everySecond(){return this.intervalMs=1000,this.cronPattern="@every_second",this}everyMinute(){return this.setTimeOfDay("*","*")}everyTwoMinutes(){return this.setTimeOfDay("*/2","*")}everyFiveMinutes(){return this.setTimeOfDay("*/5","*")}everyTenMinutes(){return this.setTimeOfDay("*/10","*")}everyThirtyMinutes(){return this.setTimeOfDay("*/30","*")}everyHour(){return this.setTimeOfDay("0","*")}everyDay(){return this.setDays("*","*","*")}hourly(){return this.setTimeOfDay("0","*")}daily(){return this.setDays("*","*","*")}weekly(){return this.setDays("*","*","0")}monthly(){return this.setDays("1","*","*")}yearly(){return this.setDays("1","1","*")}annually(){return this.setDays("1","1","*")}cron(t){let e=String(t).trim().split(/\s+/).filter(Boolean);if(e.length===6){let[s,...i]=e;if(i.every((o)=>o==="*")){let o=s==="*"?1:Number(/^\*\/(\d+)$/.exec(s)?.[1]);if(Number.isInteger(o)&&o>=1&&o<60&&60%o===0)return this.intervalMs=o*1000,this.cronPattern=o===1?"@every_second":`@every_${o}_seconds`,this}if(s!=="0")throw Error(`cron(): '${t}' runs on particular seconds, which the scheduler cannot do. Use '*' or '*/N' (N dividing 60) with every other field '*', or a seconds field of 0`);e.shift()}if(e.length!==5)throw Error(`cron(): '${t}' is not a cron expression; expected 5 fields, or 6 with leading seconds`);let n=e.join(" "),r;try{r=E(n,Date.now(),{tz:"UTC"})}catch(s){throw Error(`cron(): '${t}' is not a valid cron expression: ${s instanceof Error?s.message:String(s)}`)}if(!r)throw Error(`cron(): '${t}' never matches a date`);return this.intervalMs=null,this.cronPattern=n,this}dailyAt(t){return this.daily().at(t)}weekdays(){return this.setDays("*","*","1-5")}onDays(t){if(!Array.isArray(t)||t.length===0)throw Error(`onDays() requires a non-empty array; got ${JSON.stringify(t)}`);for(let e of t)if(!Number.isInteger(e)||e<0||e>6)throw Error(`onDays(): each day must be an integer 0-6 (Sun-Sat). Got ${e}`);return this.setDays("*","*",t.join(","))}at(t){let e=t.split(":");if(e.length!==2||e[0]===""||e[1]==="")throw Error(`Invalid time format "${t}". Expected "HH:MM" (e.g., "14:30")`);let[n,r]=e.map(Number);if(n===void 0||r===void 0||Number.isNaN(n)||Number.isNaN(r)||n<0||n>23||r<0||r>59)throw Error(`Invalid time "${t}". Hour must be 0-23, minute must be 0-59`);return this.setTimeOfDay(String(r),String(n))}setTimeZone(t){return this.timezone=t,this.options.timezone=t,this}withErrorHandler(t){return this.options.catch=t,this}withMaxRuns(t){return this.options.maxRuns=t,this}withProtection(t){return this.options.protect=t||!0,this}withName(t){return this.options.name=t,this}withContext(t){return this.options.context=t,this}withInterval(t){if(typeof t!=="number"||!Number.isFinite(t)||t<=0)throw Error(`withInterval() takes a positive number of seconds; got ${String(t)}`);return this.options.interval=t,this}between(t,e){let n=X(t,"startAt"),r=X(e,"stopAt");if(r.getTime()<=n.getTime())throw Error(`between(): stopAt (${r.toISOString()}) must be after startAt (${n.toISOString()})`);return this.options.startAt=n,this.options.stopAt=r,this}onQueue(t){if(this.jobName===null)throw Error("onQueue() applies to schedule.job(...) only: an action, command or callback has no job to dispatch");if(typeof t!=="string"||t.trim()==="")throw Error(`onQueue() takes a queue name; got ${JSON.stringify(t)}`);return this.dispatchQueue=t,this}withoutOverlapping(t){if(this.shouldPreventOverlap=!0,t!==void 0)this.overlapExpiresAfterMinutes=t;return this}onOneServer(){return this.shouldRunOnOneServer=!0,this}runInBackground(){return this.shouldRunInBackground=!0,this}sendOutputTo(t){return this.outputPath=t,this.outputAppend=!1,this}appendOutputTo(t){return this.outputPath=t,this.outputAppend=!0,this}async runMissed(t){if(!this.cronPattern||this.intervalMs!==null)return a.warn("[scheduler] runMissed() requires a cron-based schedule; interval-based tasks (everySecond) have no concept of missed slots"),0;let e=t.max??100,n=t.since instanceof Date?t.since.getTime():t.since,r=Date.now();if(n>=r)return 0;let s=this.options.name||"unnamed-task";if(!c.isEnabled(s))return a.info(`[scheduler] runMissed: ${s} is paused, so its missed slots are not run`),0;let{startAt:i,stopAt:o}=this.options,u=(d)=>(!i||d>=i)&&(!o||d<=o),l=this.zone(),p=l!=="UTC"?{tz:l}:{},h=(d)=>{let m=[],k=d;while(!0){let y=E(this.cronPattern,k,p);if(!y||y.getTime()>r)return m;if(u(y))m.push(y);k=y.getTime()}},g=60000*(e+1),f=Math.max(n,r-g),w=h(f);while(w.length<=e&&f>n)g*=2,f=Math.max(n,r-g),w=h(f);if(w.length>e){let d=f===n?`${w.length}`:`more than ${e}`;a.warn(`[scheduler] runMissed: ${d} missed slots since ${new Date(n).toISOString()}, running the ${e} most recent - older runs dropped`),w=w.slice(-e)}let b=this.options.context,N=this.wrapTask(b===void 0?this.task:()=>this.task(b),!0),v=0;for(let d of w)try{if(!await this.claimSlot(s,d.getTime())){a.info(`[scheduler] runMissed slot ${d.toISOString()} skipped: another server claimed it`);continue}let m=N();if(m&&typeof m.then==="function")m=await m;if(m===G){a.info(`[scheduler] runMissed slot ${d.toISOString()} skipped: ${s} is already running`);continue}v++}catch(m){if(a.error(`[scheduler] runMissed slot ${d.toISOString()} failed: ${m instanceof Error?m.message:String(m)}`),this.options.catch)this.options.catch(m)}return v}static listJobs(){let t=[];for(let[e,n]of c.jobs)t.push({name:e,pattern:n.pattern,timezone:n.timezone,nextRun:n.nextRun?n.nextRun():null,enabled:c.isEnabled(e)});return t}static async runNow(t,e={}){let n=c.jobs.get(t);if(!n)throw Error(`Scheduled task "${t}" was not found.`);if(!c.isEnabled(t))throw Error(`Scheduled task "${t}" is paused.`);let r=e.inProcess?c.inProcessRuns.get(t):void 0;await(r?r():n.run())}static setEnabled(t,e){let n=c.disabledJobs();if(e)n.delete(t);else n.add(t);let r=O(process.cwd(),"storage","framework","runtime");yt(r,{recursive:!0});let s=`${c.stateFile}.${process.pid}.tmp`;vt(s,`${JSON.stringify({disabled:[...n].sort()},null,2)}
3
+ `,{mode:384}),St(s,c.stateFile)}static isEnabled(t){return!c.disabledJobs().has(t)}static isScheduled(t){return c.jobs.has(t)}static disabledJobs(){try{let t=JSON.parse(kt(c.stateFile,"utf8"));return new Set(Array.isArray(t.disabled)?t.disabled.filter((e)=>typeof e==="string"):[])}catch{return new Set}}static listLocks(){return Array.from(c.activeLocks.keys())}async acquireLock(t){if(c.activeLocks.has(t))return!1;let e=this.overlapExpiresAfterMinutes*60*1000,{dialect:n,adminDb:r}=this.shouldRunOnOneServer?await this.oneServerDb(!0):{dialect:null,adminDb:null},s=await B(t,e,n,r,c.lockDir);if(!s)return!1;return c.activeLocks.set(t,s),!0}async oneServerDb(t){try{let{db:e}=await import("@stacksjs/database/runtime"),n=e,r=(await import("@stacksjs/env")).env.DB_CONNECTION||"sqlite";return{dialect:r==="postgres"||r==="mysql"||r==="sqlite"?r:null,adminDb:n}}catch(e){if(t)a.warn(`[scheduler] onOneServer() couldn't reach DB; falling back to file-only lock (which only serializes within this process): ${e instanceof Error?e.message:String(e)}`);return{dialect:null,adminDb:null}}}async claimSlot(t,e){if(!this.shouldRunOnOneServer)return!0;let{dialect:n,adminDb:r}=await this.oneServerDb(!1);return W(t,e,n,r)}async releaseLock(t){let e=c.activeLocks.get(t);if(!e)return;c.activeLocks.delete(t),await e.release()}wrapTask(t,e=!1){let n=this.options.name||"unnamed-task",r=t;if(this.shouldPreventOverlap||this.shouldRunOnOneServer){let s=this,i=r;r=()=>(async()=>{if(!await s.acquireLock(n))return a.info(`Skipping overlapping task: ${n}`),G;try{let u=i();if(u&&typeof u==="object"&&typeof u.finally==="function")await u}finally{try{await s.releaseLock(n)}catch(u){a.error(`[scheduler] releasing the lock for ${n} failed: ${u instanceof Error?u.message:String(u)}`)}}})()}if(this.shouldRunInBackground&&!e){let s=O(process.cwd(),"buddy"),i=this.handlesOwnOutput?null:this.outputPath,o=this.outputAppend;r=()=>{let u="inherit";if(i)try{u=bt(i,o?"a":"w")}catch(h){a.warn(`[scheduler] couldn't open ${i} for background task ${n}: ${h instanceof Error?h.message:String(h)}; its output goes to this process instead`)}let l=()=>{if(typeof u!=="number")return;try{wt(u)}catch{}u="inherit"},p=pt(s,["schedule:run-one",n,"--in-process"],{cwd:process.cwd(),detached:!0,stdio:["ignore",u,u]});l(),p.on("error",(h)=>{a.error(`[scheduler] background task ${n} failed to start (${s}): ${h.message}`)}),p.on("exit",(h,g)=>{if(h!==0&&h!==null)a.error(`[scheduler] background task ${n} (pid: ${p.pid}) exited with code ${h}`);else if(g)a.warn(`[scheduler] background task ${n} (pid: ${p.pid}) terminated by signal ${g}`)}),p.unref(),a.info(`Task ${n} spawned in background (pid: ${p.pid})`)}}return r}getNextRunTime(){if(!this.cronPattern||this.intervalMs!==null)return null;let{startAt:t,stopAt:e}=this.options,n=Date.now(),r=t&&t.getTime()>n?t.getTime()-1:n,s=this.nextCronTime(r);if(s&&e&&s.getTime()>e.getTime())return null;return s}zone(){if(this.timezone)return this.timezone;return Et.app?.timezone||"UTC"}nextCronTime(t){let e=this.zone();if(e==="UTC")return E(this.cronPattern,t);return E(this.cronPattern,t,{tz:e})}start(){if(!this.cronPattern&&this.intervalMs===null)return{stop:()=>{},run:async()=>{},nextRun:()=>null};let t=this.options.name?`${this.options.name.toLowerCase().replace(/[^a-z0-9]+/g,"")}::${this.cronPattern||`every:${this.intervalMs}`}`:null;if(t){let d=c.scheduledKeys.get(t);if(d)return a.debug(`[scheduler] duplicate schedule for "${this.options.name}" (${this.cronPattern||`every ${this.intervalMs}ms`}) skipped`),d}if(this.shouldRunInBackground&&!this.options.name)throw Error("runInBackground() needs a named task: add .withName(...) so the background process can find it");let e=this.options.context,n=e===void 0?this.task:()=>this.task(e),r=this.wrapTask(n),s=this.shouldRunInBackground?this.wrapTask(n,!0):r,i=!1,o=!1,u=null,l=null,p=0,h=()=>{if(i=!0,l!==null){if(this.intervalMs!==null)clearInterval(l);else clearTimeout(l);l=null}},g=()=>this.getNextRunTime(),f=this.options.name||"unnamed-task",w=async(d,m)=>{if(!c.isEnabled(f)){a.debug(`[scheduler] task ${f} is paused`);return}let{startAt:k,stopAt:y,interval:P,protect:A}=this.options,D=Date.now();if(y&&D>y.getTime()){a.debug(`[scheduler] task ${f} is past its between() window; stopping`),h();return}if(k&&D<k.getTime()){a.debug(`[scheduler] task ${f} has not reached its between() window`);return}if(this.options.maxRuns&&p>=this.options.maxRuns){h();return}if(A&&o){if(a.debug(`[scheduler] task ${f} is still running; skipping this tick (withProtection)`),typeof A==="function")A(v);return}if(P&&u!==null&&D-u<P*1000){a.debug(`[scheduler] task ${f} ran less than ${P}s ago; skipping this tick (withInterval)`);return}if(m!==void 0){let S;try{S=await this.claimSlot(f,m)}catch(j){throw b(j),j}if(!S){a.info(`[scheduler] task ${f} slot ${new Date(m).toISOString()} skipped: another server claimed it (onOneServer)`);return}}p++,o=!0,u=D;try{let S=d();if(S&&typeof S.then==="function")await S}catch(S){throw b(S),S}finally{o=!1}},b=(d)=>{if(this.options.catch)this.options.catch(d);else a.error(`[scheduler] task ${f} failed (no withErrorHandler() installed): ${d instanceof Error?d.message:String(d)}`)},N=(d)=>w(r,d),v={stop:h,run:()=>w(r),nextRun:g,pattern:this.intervalMs!==null?`every ${this.intervalMs/1000}s`:this.cronPattern,timezone:this.zone(),name:this.options.name,enabled:c.isEnabled(f)};if(this.intervalMs!==null){let d=this.intervalMs;l=setInterval(()=>{if(i)return;N(Math.floor(Date.now()/d)*d).catch(()=>{})},d),l.unref?.()}else if(this.cronPattern){let m=()=>{if(i)return;if(this.options.maxRuns&&p>=this.options.maxRuns)return;let k=this.getNextRunTime();if(!k){h();return}let y=Math.max(k.getTime()-Date.now(),0);if(y>2147483647)l=setTimeout(()=>{if(i)return;m()},2147483647);else l=setTimeout(()=>{if(i)return;N(k.getTime()).catch(()=>{}),m()},y)};m()}if(this.options.name)c.jobs.set(this.options.name,v),c.inProcessRuns.set(this.options.name,()=>w(s));if(t)c.scheduledKeys.set(t,v);return a.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.zone()}`),v}static job(t){let e=null;return e=new c(async(n)=>{let r=e?.dispatchQueue??null;try{if(r!==null){a.info(`Dispatching job: ${t} to queue ${r}`);let s=Nt(t).onQueue(r);await(n===void 0?s:s.withContext(n)).dispatch();return}a.info(`Running job: ${t}`),await Dt(t,n===void 0?{}:{context:n})}catch(s){throw a.error(`Job ${t} failed:`,s),s}}),e.jobName=t,e.withName(t)}static action(t){return new c(async()=>{a.info(`Running action: ${t}`);try{await $t(t)}catch(e){throw a.error(`Action ${t} failed:`,e),e}}).withName(t)}static command(t){let e=null;return e=new c(async()=>{let r=e?.outputPath??null,s=e?.outputAppend??!1;try{if(a.info(`Executing command: ${t}`),r){let{spawn:o}=await import("child_process"),{openSync:u,closeSync:l}=await import("fs"),h=u(r,s?"a":"w");try{await new Promise((g,f)=>{let w=o(t,{shell:!0,stdio:["ignore",h,h]});w.on("error",f),w.on("exit",(b)=>{if(b===0)g();else f(Error(`Command '${t}' exited with code ${b}`))})})}finally{l(h)}return}let i=await Tt(t);if(i.isErr)throw a.error(i.error),i.error}catch(i){throw a.error(`Command execution failed: ${i}`),i}}),e.handlesOwnOutput=!0,e.withName(`command-${t}`)}static notification(t,e,n,r){return new c(async()=>{try{let{notify:s}=await import("@stacksjs/notifications"),i=await s(t,e,n,r),o=i.filter((u)=>!u.success);if(o.length>0){let u=o.map((l)=>`${l.channel}: ${l.error?.message??"not delivered"}`).join("; ");throw Error(`Scheduled notification was not delivered on ${o.length} of ${i.length} channel(s) - ${u}`)}}catch(s){throw a.error("Scheduled notification failed:",s),s}}).withName(xt(t,e,n,r))}static async gracefulShutdown(){a.info("Gracefully shutting down scheduled jobs...");for(let[t,e]of c.jobs)a.info(`Stopping job: ${t}`),e.stop();c.jobs.clear(),c.inProcessRuns.clear(),c.scheduledKeys.clear(),a.info("All jobs have been stopped")}}class Ot extends c{}function Pt(t){if(t instanceof Date)return t>new Date?t:null;return E(t)}function re(t){let e=Pt(t);if(!e)return-1;return e.getTime()-Date.now()}var se=c;export{Ot as Queue,c as Schedule,jt as runScheduler,se as schedule,Pt as sendAt,re as timeout};
@@ -0,0 +1,14 @@
1
+ import type { Dialect } from './scheduler-lock';
2
+ /** The table's DDL, the same on PostgreSQL and MySQL. */
3
+ export declare function schedulerClaimsTableSql(): string;
4
+ /**
5
+ * Claim `slot` (epoch ms) of `taskName` for this server. Resolves true when
6
+ * this server should run the slot, false when another server claimed it
7
+ * first. Throws when the database cannot answer: a claim that cannot be
8
+ * checked is a failure to run, as a lock that cannot be checked is.
9
+ */
10
+ export declare function claimSchedulerSlot(taskName: string, slot: number, dialect: Dialect | null, adminDb: AdminDb | null): Promise<boolean>;
11
+ export declare const SCHEDULER_CLAIMS_TABLE: 'scheduler_claims';
12
+ /** How long a claim row is kept before the winner of a later slot prunes it. */
13
+ export declare const SCHEDULER_CLAIM_RETENTION_MS: number;
14
+ declare type AdminDb = { unsafe: (sql: string) => Promise<unknown> }
@@ -31,6 +31,7 @@ export declare function lockFileName(taskName: string): string;
31
31
  * @param lockDir - file-lock directory; default is `storage/framework/locks`
32
32
  */
33
33
  export declare function acquireSchedulerLock(taskName: string, expiryMs: number, dialect: Dialect | null, adminDb: { unsafe: (sql: string) => Promise<unknown> } | null, lockDir?: string): Promise<SchedulerLockHandle | null>;
34
+ export declare function pluckFirstRow(result: unknown): unknown;
34
35
  export declare interface SchedulerLockHandle {
35
36
  release: () => Promise<void>
36
37
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/scheduler",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.451",
5
+ "version": "0.75.457",
6
6
  "description": "The Stacks scheduler.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,19 +58,19 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "dependencies": {
61
- "@stacksjs/action-runner": "0.75.451",
62
- "@stacksjs/cli": "0.75.451",
63
- "@stacksjs/config": "0.75.451",
64
- "@stacksjs/cron": "0.75.451",
65
- "@stacksjs/database": "0.75.451",
66
- "@stacksjs/env": "0.75.451",
67
- "@stacksjs/error-handling": "0.75.451",
68
- "@stacksjs/logging": "0.75.451",
69
- "@stacksjs/notifications": "0.75.451",
70
- "@stacksjs/path": "0.75.451",
71
- "@stacksjs/queue": "0.75.451",
72
- "@stacksjs/storage": "0.75.451",
73
- "@stacksjs/types": "0.75.451"
61
+ "@stacksjs/action-runner": "0.75.457",
62
+ "@stacksjs/cli": "0.75.457",
63
+ "@stacksjs/config": "0.75.457",
64
+ "@stacksjs/cron": "0.75.457",
65
+ "@stacksjs/database": "0.75.457",
66
+ "@stacksjs/env": "0.75.457",
67
+ "@stacksjs/error-handling": "0.75.457",
68
+ "@stacksjs/logging": "0.75.457",
69
+ "@stacksjs/notifications": "0.75.457",
70
+ "@stacksjs/path": "0.75.457",
71
+ "@stacksjs/queue": "0.75.457",
72
+ "@stacksjs/storage": "0.75.457",
73
+ "@stacksjs/types": "0.75.457"
74
74
  },
75
75
  "devDependencies": {
76
76
  "better-dx": "^0.2.24"