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