@stacksjs/queue 0.72.102 → 0.73.0
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/action.d.ts +16 -8
- package/dist/health.d.ts +0 -9
- package/dist/index.js +2 -2
- package/dist/job.d.ts +32 -16
- package/package.json +2 -1
package/dist/action.d.ts
CHANGED
|
@@ -25,11 +25,19 @@ import type { JobOptions } from '@stacksjs/types';
|
|
|
25
25
|
* await SendWelcomeEmail.dispatchNow({ email: user.email })
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The payload argument list for a job.
|
|
30
|
+
*
|
|
31
|
+
* A job that declares what it takes must be given it; one that declares
|
|
32
|
+
* nothing (`T` left as `unknown`) may still be dispatched bare, which is what
|
|
33
|
+
* a job like `Inspire` does.
|
|
34
|
+
*/
|
|
35
|
+
export type JobPayloadArgs<T> = unknown extends T ? [payload?: T] : [payload: T];
|
|
36
|
+
export declare class Job<T = unknown> {
|
|
29
37
|
name: JobOptions['name'];
|
|
30
38
|
description: JobOptions['description'];
|
|
31
39
|
action?: JobOptions['action'];
|
|
32
|
-
handle?: JobOptions['handle'];
|
|
40
|
+
handle?: JobOptions<T>['handle'];
|
|
33
41
|
queue?: string;
|
|
34
42
|
rate: JobOptions['rate'];
|
|
35
43
|
tries: JobOptions['tries'];
|
|
@@ -37,10 +45,10 @@ export declare class Job {
|
|
|
37
45
|
backoff: JobOptions['backoff'];
|
|
38
46
|
backoffConfig: JobOptions['backoffConfig'];
|
|
39
47
|
enabled: JobOptions['enabled'];
|
|
40
|
-
constructor(options: JobOptions & { queue?: string; timeout?: number });
|
|
41
|
-
dispatch
|
|
42
|
-
dispatchIf
|
|
43
|
-
dispatchUnless
|
|
44
|
-
dispatchAfter
|
|
45
|
-
dispatchNow
|
|
48
|
+
constructor(options: JobOptions<T> & { queue?: string; timeout?: number });
|
|
49
|
+
dispatch(...[payload]: JobPayloadArgs<T>): Promise<void>;
|
|
50
|
+
dispatchIf(condition: boolean, ...[payload]: JobPayloadArgs<T>): Promise<void>;
|
|
51
|
+
dispatchUnless(condition: boolean, ...[payload]: JobPayloadArgs<T>): Promise<void>;
|
|
52
|
+
dispatchAfter(delaySeconds: number, ...[payload]: JobPayloadArgs<T>): Promise<void>;
|
|
53
|
+
dispatchNow(...[payload]: JobPayloadArgs<T>): Promise<void>;
|
|
46
54
|
}
|
package/dist/health.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derive a queued job's state from the columns that hold it.
|
|
3
|
-
*
|
|
4
|
-
* There is no `status` column: a worker claims a job by stamping
|
|
5
|
-
* `reserved_at`, and `available_at` in the future means it is not eligible
|
|
6
|
-
* yet. Dashboards and health checks both need this rule, and reading it off a
|
|
7
|
-
* `status` column - which several of them tried - silently reports every job
|
|
8
|
-
* as pending forever.
|
|
9
|
-
*/
|
|
10
1
|
export declare function queuedJobState(job: { reserved_at?: unknown, available_at?: unknown }, nowTimestamp?: number): QueuedJobState;
|
|
11
2
|
/**
|
|
12
3
|
* Perform queue health check
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var{defineProperty:Ie,getOwnPropertyNames:Gn,getOwnPropertyDescriptor:Yn}=Object,Xn=Object.prototype.hasOwnProperty;function Zn(e){return this[e]}var er=(e)=>{var t=(xt??=new WeakMap).get(e),n;if(t)return t;if(t=Ie({},"__esModule",{value:!0}),e&&typeof e==="object"||typeof e==="function"){for(var r of Gn(e))if(!Xn.call(t,r))Ie(t,r,{get:Zn.bind(e,r),enumerable:!(n=Yn(e,r))||n.enumerable})}return xt.set(e,t),t},xt;var tr=(e)=>e;function nr(e,t){this[e]=tr.bind(null,t)}var M=(e,t)=>{for(var n in t)Ie(e,n,{get:t[n],enumerable:!0,configurable:!0,set:nr.bind(t,n)})};var D=(e,t)=>()=>(e&&(t=e(e=0)),t);var h=import.meta.require;function O(e,t,n,r){return{jobName:e,payload:t,options:n,envelopeVersion:1,dispatchedAt:new Date().toISOString(),...r?{traceId:r}:{}}}function N(e){try{return JSON.stringify(e)}catch(t){throw ir(e,t)}}function de(e){N(e)}function ir(e,t){if(or(t)){let i=null;try{i=Qe(e,"",new Set)}catch{}let o=i?`\`${i.path}\` is a BigInt (${i.value}n)`:"it holds a BigInt";return Error(`[queue] Job "${e.jobName}" was not dispatched: ${o}, and job envelopes travel as JSON, which cannot represent one. Convert it where you dispatch - \`String(value)\` keeps every digit, \`Number(value)\` is exact below 2^53 - and convert it back inside the job handler. See the JSON round-trip contract in @stacksjs/queue's src/envelope.ts.`,{cause:t})}if(ar(t))return Error(`[queue] Job "${e.jobName}" was not dispatched: its payload contains a circular reference, which JSON cannot represent. Send the ids of the things the payload points at rather than the objects themselves - that also keeps the job replayable once those objects have changed. See the JSON round-trip contract in @stacksjs/queue's src/envelope.ts.`,{cause:t});let r=t instanceof Error?t.message:String(t);return Error(`[queue] Job "${e.jobName}" was not dispatched: serializing its payload threw (${r}). JSON rejects only two things on its own and this was neither, so the throw came from the payload - a \`toJSON()\` or a property getter. See the JSON round-trip contract in @stacksjs/queue's src/envelope.ts.`,{cause:t})}function or(e){return e instanceof TypeError&&/bigint/i.test(e.message)}function ar(e){return e instanceof TypeError&&/circular|cyclic/i.test(e.message)}function Qe(e,t,n){if(typeof e==="bigint")return{path:t||"envelope",value:e};if(!e||typeof e!=="object")return null;if(n.has(e))return null;if(n.add(e),Array.isArray(e)){for(let r=0;r<e.length;r++){let i=Qe(e[r],`${t}[${r}]`,n);if(i)return i}return null}for(let[r,i]of Object.entries(e)){let o=Qe(i,t?`${t}.${r}`:r,n);if(o)return o}return null}function jt(e,t){if(Be.has(e))return;Be.add(e),console.warn(t)}function sr(){Be.clear()}function xe(e){let t;if(typeof e==="string")try{t=JSON.parse(e)}catch{return{ok:!1,reason:"malformed",detail:"not valid JSON"}}else if(e&&typeof e==="object")t=e;else return{ok:!1,reason:"malformed",detail:`expected string or object, got ${typeof e}`};if(t.envelopeVersion===1){if(typeof t.jobName!=="string")return{ok:!1,reason:"missing-job-name"};return{ok:!0,envelope:{jobName:t.jobName,payload:t.payload,options:t.options??void 0,envelopeVersion:1,dispatchedAt:typeof t.dispatchedAt==="string"?t.dispatchedAt:new Date(0).toISOString(),...typeof t.traceId==="string"&&t.traceId?{traceId:t.traceId}:{}},source:"v1"}}if(typeof t.envelopeVersion==="number"&&t.envelopeVersion>1)return{ok:!1,reason:"unknown-version",detail:`envelopeVersion=${t.envelopeVersion}, this worker speaks v1`};if(typeof t.jobName==="string")return jt("v0-implicit","[queue/envelope] Processing pre-#1884 job envelope without envelopeVersion. In-flight jobs from before the upgrade will continue to work; new dispatches use the v1 shape automatically."),{ok:!0,envelope:{jobName:t.jobName,payload:t.payload,options:t.options??void 0,envelopeVersion:1,dispatchedAt:typeof t.dispatchedAt==="string"?t.dispatchedAt:new Date(0).toISOString(),...typeof t.traceId==="string"&&t.traceId?{traceId:t.traceId}:{}},source:"v0-implicit"};if(typeof t.job==="string")return jt("laravel-legacy","[queue/envelope] Processing Laravel-legacy job envelope (`{ job, data }` shape). Will continue to process but the queue table contains migration-era rows - consider flushing once they drain."),{ok:!0,envelope:{jobName:t.job.replace(/^App\\+Jobs\\+/,"").replace(/^.*\\/,""),payload:t.data,options:void 0,envelopeVersion:1,dispatchedAt:new Date(0).toISOString()},source:"laravel-legacy"};return{ok:!1,reason:"missing-job-name"}}var rr=1,Be;var te=D(()=>{Be=new Set});var Ue={};M(Ue,{QueueTester:()=>je,createQueueTester:()=>Pt,expectJobToFail:()=>Ct,fake:()=>He,getFakeQueue:()=>St,isFaked:()=>Et,restore:()=>Le,runJob:()=>Tt});class Jt{dispatchedJobs=[];pushedJobs=[];processedJobs=[];failedJobs=[];dispatch(e,t,n={}){this.dispatchedJobs.push({name:e,data:t,options:n,dispatchedAt:new Date,queue:n.queue||"default"})}push(e,t,n={}){this.pushedJobs.push({name:e,data:t,options:n,dispatchedAt:new Date,queue:n.queue||"default"})}dispatched(e){if(e)return this.dispatchedJobs.filter((t)=>t.name===e);return[...this.dispatchedJobs]}pushed(e){if(e)return this.pushedJobs.filter((t)=>t.name===e);return[...this.pushedJobs]}assertDispatched(e,t){let n=this.dispatched(e);if(n.length===0)throw Error(`Expected job "${e}" to be dispatched, but it was not.`);if(t){if(n.filter(t).length===0)throw Error(`Expected job "${e}" to be dispatched matching the callback, but no matching jobs were found.`)}}assertNotDispatched(e){let t=this.dispatched(e);if(t.length>0)throw Error(`Expected job "${e}" to not be dispatched, but it was dispatched ${t.length} time(s).`)}assertDispatchedTimes(e,t){let n=this.dispatched(e);if(n.length!==t)throw Error(`Expected job "${e}" to be dispatched ${t} time(s), but it was dispatched ${n.length} time(s).`)}assertNothingDispatched(){if(this.dispatchedJobs.length>0){let e=[...new Set(this.dispatchedJobs.map((t)=>t.name))].join(", ");throw Error(`Expected no jobs to be dispatched, but found: ${e}`)}}assertPushed(e,t){let n=this.pushed(e);if(n.length===0)throw Error(`Expected job "${e}" to be pushed, but it was not.`);if(t){if(n.filter(t).length===0)throw Error(`Expected job "${e}" to be pushed matching the callback, but no matching jobs were found.`)}}assertPushedWithDelay(e,t){if(this.pushed(e).filter((i)=>i.options.delay===t).length===0)throw Error(`Expected job "${e}" to be pushed with delay ${t}ms, but no matching jobs were found.`)}assertPushedOn(e,t){if(this.pushed(t).filter((r)=>r.queue===e).length===0)throw Error(`Expected job "${t}" to be pushed on queue "${e}", but it was not.`)}async processJob(e,t){let n=this.dispatchedJobs.find((r)=>r.name===e);if(!n)throw Error(`No dispatched job found with name "${e}"`);try{await t(n.data),this.processedJobs.push(n)}catch(r){throw this.failedJobs.push({job:n,error:r}),r}}processed(e){if(e)return this.processedJobs.filter((t)=>t.name===e);return[...this.processedJobs]}failed(e){if(e)return this.failedJobs.filter((t)=>t.job.name===e);return[...this.failedJobs]}reset(){this.dispatchedJobs=[],this.pushedJobs=[],this.processedJobs=[],this.failedJobs=[]}}function He(){return he=new Jt,he}function St(){return he}function Et(){return he!==null}function Le(){he=null}class je{queue;constructor(){this.queue=He()}dispatch(e,t,n={}){return this.queue.dispatch(e,t,n),this}push(e,t,n={}){return this.queue.push(e,t,n),this}assertDispatched(e,t){return this.queue.assertDispatched(e,t),this}assertNotDispatched(e){return this.queue.assertNotDispatched(e),this}assertDispatchedTimes(e,t){return this.queue.assertDispatchedTimes(e,t),this}assertNothingDispatched(){return this.queue.assertNothingDispatched(),this}dispatched(e){return this.queue.dispatched(e)}reset(){return this.queue.reset(),this}cleanup(){Le()}}function Pt(){return new je}async function Tt(e,t){return await e.handle(t)}async function Ct(e,t,n){try{throw await e.handle(t),Error("Expected job to fail, but it succeeded")}catch(r){if(r.message==="Expected job to fail, but it succeeded")throw r;if(n){let i=r.message;if(typeof n==="string"){if(!i.includes(n))throw Error(`Expected error to contain "${n}", got "${i}"`)}else if(!n.test(i))throw Error(`Expected error to match ${n}, got "${i}"`)}return r}}var he=null;var pe={};M(pe,{QueueManager:()=>mr,RedisJob:()=>Rt,RedisQueue:()=>ne,StacksQueueManager:()=>qt,batch:()=>ur,chain:()=>lr,createRedisDispatcher:()=>gr,dispatch:()=>dr,dispatchAfter:()=>hr,dispatchSync:()=>pr,getQueueManager:()=>fr,setQueueManager:()=>br});import{Queue as cr,batch as ur,chain as lr,dispatch as dr,dispatchAfter as hr,dispatchSync as pr,getQueueManager as fr,QueueManager as mr,setQueueManager as br}from"@stacksjs/bun-queue";import{log as We}from"@stacksjs/logging";class ne{queue;config;isProcessing=!1;constructor(e,t){this.config=t,this.queue=new cr(e,{driver:"redis",prefix:t.prefix,redis:t.redis?{url:t.redis.url||this.buildRedisUrl(t.redis)}:void 0,defaultJobOptions:t.defaultJobOptions?{delay:t.defaultJobOptions.delay,attempts:t.defaultJobOptions.attempts,backoff:t.defaultJobOptions.backoff,removeOnComplete:t.defaultJobOptions.removeOnComplete,removeOnFail:t.defaultJobOptions.removeOnFail,priority:t.defaultJobOptions.priority,lifo:t.defaultJobOptions.lifo,timeout:t.defaultJobOptions.timeout,jobId:t.defaultJobOptions.jobId,dependsOn:t.defaultJobOptions.dependsOn,keepJobs:t.defaultJobOptions.keepJobs,deadLetter:t.defaultJobOptions.deadLetter}:void 0,limiter:t.limiter,metrics:t.metrics,stalledJobCheckInterval:t.stalledJobCheckInterval,maxStalledJobRetries:t.maxStalledJobRetries,distributedLock:t.distributedLock,defaultDeadLetterOptions:t.defaultDeadLetterOptions,horizontalScaling:t.horizontalScaling,logLevel:t.logLevel}),We.debug(`Redis queue "${e}" initialized`)}buildRedisUrl(e){let t=e.host||"localhost",n=e.port||6379,r=e.password?`:${encodeURIComponent(e.password)}@`:"",i=Number.isFinite(e.db)&&e.db>=0?e.db:0;return`redis://${r}${t}:${n}/${i}`}async add(e,t){let n={delay:t?.delay?t.delay*1000:void 0,attempts:t?.maxTries,priority:t?.priority,timeout:t?.timeout?t.timeout*1000:void 0,backoff:Array.isArray(t?.backoff)?t.backoff.map((r)=>(Number(r)||1)*1000):t?.backoff};return this.queue.add(e,n)}process(e,t){if(this.isProcessing){We.warn("Queue is already processing");return}this.isProcessing=!0,this.queue.process(e,t),We.info(`Started processing queue with concurrency ${e}`)}async getJob(e){return this.queue.getJob(e)}async getJobs(e){return this.queue.getJobs(e)}async getJobCounts(){return this.queue.getJobCounts()}async removeJob(e){return this.queue.removeJob(e)}async pause(){return this.queue.pause()}async resume(){return this.queue.resume()}async empty(){return this.queue.empty()}async close(){return this.isProcessing=!1,this.queue.close()}async getMetrics(){return this.queue.getMetrics()}async ping(){return this.queue.ping()}async scheduleCron(e){return this.queue.scheduleCron({cronExpression:e.cron,data:e.data,timezone:e.tz,jobId:e.name})}async unscheduleCron(e){return this.queue.unscheduleCron(e)}async getDeadLetterJobs(){return this.queue.getDeadLetterJobs()}async republishDeadLetterJob(e){return this.queue.republishDeadLetterJob(e)}async clearDeadLetterQueue(){return this.queue.clearDeadLetterQueue()}async bulkRemove(e){return this.queue.bulkRemove(e)}async getClusterInfo(){return this.queue.getClusterInfo()}isLeader(){return this.queue.isLeader()}getQueue(){return this.queue}on(e,t){this.queue.events.on(e,t)}}class qt{config;queues=new Map;defaultConnection="default";constructor(e){this.config=e;for(let[t,n]of Object.entries(e))if(n.driver==="redis")this.queues.set(t,new ne(t,n))}queue(e){let t=e||this.defaultConnection,n=this.queues.get(t);if(!n){let r=this.config[t];if(!r)throw Error(`Queue "${t}" not configured`);n=new ne(t,r),this.queues.set(t,n)}return n}setDefaultConnection(e){this.defaultConnection=e}async closeAll(){let e=Array.from(this.queues.values()).map((t)=>t.close());await Promise.all(e),this.queues.clear()}}function gr(e,t){let n=new ne(e,t);return async(r,i)=>n.add(r,i)}class Rt{data;options={};queue;constructor(e,t,n){this.data=n;this.queue=new ne(e,t)}async dispatch(){await this.queue.add(this.data,this.options)}async dispatchNow(){await this.queue.add(this.data,{...this.options,immediate:!0})}delay(e){return this.options.delay=e,this}afterResponse(){return this.options.afterResponse=!0,this}chain(e){return this.options.chainedJobs=e,this}onQueue(e){return this.options.queue=e,this}priority(e){return this.options.priority=e,this}tries(e){return this.options.maxTries=e,this}timeout(e){return this.options.timeout=e,this}backoff(e){return this.options.backoff=e,this}}var fe=()=>{};function _(e){let t=e,n=t?.errno;if(typeof n==="string"&&n.toUpperCase()==="42P01")return!0;if(typeof n==="number"&&n===1146)return!0;if((typeof t?.code==="string"?t.code.toUpperCase():"")==="42P01")return!0;let i=t?.message??"";return i.includes("no such table")||i.includes("doesn't exist")||i.includes("does not exist")}var It={};M(It,{listDeadLetterJobs:()=>At,moveToDeadLetter:()=>Se,purgeDeadLetterJobs:()=>Nt,retryDeadLetterJob:()=>Ft});import{db as re}from"@stacksjs/database";function Je(){if(Ot)return;Ot=!0,console.warn("[queue/dlq] dead_letter_jobs table missing - DLQ disabled. Run migrations to enable poison-message isolation.")}async function Se(e,t,n=1){let r=new Date().toISOString().slice(0,19).replace("T"," ");try{return await re.insertInto("dead_letter_jobs").values({uuid:e.uuid??crypto.randomUUID(),connection:e.connection??"database",queue:e.queue??"default",payload:e.payload??"{}",exception:e.exception??"unknown",reason:t,total_failures:n,first_failed_at:e.failed_at??r,last_failed_at:r,dead_lettered_at:r}).execute(),!0}catch(i){if(_(i))return Je(),!1;throw i}}async function At(e={}){try{let t=re.selectFrom("dead_letter_jobs").selectAll();if(e.queue)t=t.where("queue","=",e.queue);if(e.reason)t=t.where("reason","=",e.reason);if(e.sinceCutoffMs){let r=new Date(e.sinceCutoffMs).toISOString().slice(0,19).replace("T"," ");t=t.where("dead_lettered_at",">=",r)}if(e.limit&&e.limit>0)t=t.limit(e.limit);return await t.execute()??[]}catch(t){if(_(t))return Je(),[];throw t}}async function Ft(e){try{let t=await re.selectFrom("dead_letter_jobs").where("id","=",e).selectAll().executeTakeFirst();if(!t)return!1;let n=Math.floor(Date.now()/1000);return await re.insertInto("jobs").values({queue:t.queue,payload:t.payload,attempts:0,reserved_at:null,available_at:n,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),await re.deleteFrom("dead_letter_jobs").where("id","=",e).execute(),!0}catch(t){if(_(t))return Je(),!1;throw t}}async function Nt(e=30){try{let t=new Date(Date.now()-e*24*60*60*1000).toISOString().slice(0,19).replace("T"," "),n=await re.deleteFrom("dead_letter_jobs").where("dead_lettered_at","<",t).execute();return Number(n?.numDeletedRows??n?.[0]?.numDeletedRows??n?.affectedRows??0)}catch(t){if(_(t))return Je(),0;throw t}}var Ot=!1;var Ee=()=>{};function ze(){if(Qt)return;Qt=!0,console.warn("[queue/idempotency] job_idempotency table missing - idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.")}async function yr(e){try{let{db:t}=await import("@stacksjs/database"),n=await t.selectFrom("job_idempotency").where("idempotency_key","=",e).select(["idempotency_key"]).executeTakeFirst();return Boolean(n)}catch(t){if(_(t))return ze(),!1;throw t}}async function vr(e,t,n){try{let{db:r}=await import("@stacksjs/database");await r.insertInto("job_idempotency").values({idempotency_key:e,job_name:t,queue:n??"default",dispatched_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch(r){if(_(r)){ze();return}let i=r?.message??"";if(i.includes("UNIQUE constraint")||i.includes("Duplicate entry"))return;throw r}}async function Ve(e,t,n){try{let{db:r}=await import("@stacksjs/database");return await r.insertInto("job_idempotency").values({idempotency_key:e,job_name:t,queue:n??"default",dispatched_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),"claimed"}catch(r){if(_(r))return ze(),"unenforced";let i=r?.message??"";if(i.includes("UNIQUE constraint")||i.includes("Duplicate entry"))return"duplicate";throw r}}async function Ke(e){try{let{db:t}=await import("@stacksjs/database");await t.deleteFrom("job_idempotency").where("idempotency_key","=",e).execute()}catch{}}var Qt=!1;var Ge=()=>{};var Wt={};M(Wt,{hashPayload:()=>be,isQuarantined:()=>Pe,listQuarantined:()=>Ut,quarantineJob:()=>Ht,recordFailureForPoison:()=>Mt,unquarantineJob:()=>Lt});import{createHash as _r}from"crypto";import{db as T}from"@stacksjs/database";function me(){if(Bt)return;Bt=!0,console.warn("[queue/poison] job_quarantine table missing - poison detection disabled. Run migrations to enable.")}function be(e){let t;try{t=typeof e==="string"?e:JSON.stringify(e??null)}catch{t=String(e)}return _r("sha256").update(t).digest("hex").slice(0,32)}async function Mt(e,t,n={}){let r=n.maxFailures??5,i=n.windowMinutes??60,o=be(t),a=new Date,l=a.toISOString().slice(0,19).replace("T"," ");try{let s=await T.selectFrom("job_quarantine").where("job_name","=",e).where("payload_hash","=",o).selectAll().executeTakeFirst();if(!s)return await T.insertInto("job_quarantine").values({job_name:e,payload_hash:o,failure_count:1,window_start:l,quarantined_at:null}).execute(),!1;if(s.quarantined_at)return!0;let u=Date.parse(s.window_start.replace(" ","T")+"Z"),c=a.getTime()-u,d=i*60*1000;if(Number.isFinite(u)&&c>d)return await T.updateTable("job_quarantine").set({failure_count:1,window_start:l}).where("id","=",s.id).execute(),!1;let w=s.failure_count+1;if(w>=r)return await T.updateTable("job_quarantine").set({failure_count:w,quarantined_at:l}).where("id","=",s.id).execute(),!0;return await T.updateTable("job_quarantine").set({failure_count:w}).where("id","=",s.id).execute(),!1}catch(s){if(_(s))return me(),!1;throw s}}async function Pe(e,t){let n=be(t);try{let r=await T.selectFrom("job_quarantine").where("job_name","=",e).where("payload_hash","in",[n,"*"]).whereNotNull("quarantined_at").select(["id"]).executeTakeFirst();return Boolean(r)}catch(r){if(_(r))return me(),!1;throw r}}async function Ht(e,t){let n=t===void 0?"*":be(t),r=new Date().toISOString().slice(0,19).replace("T"," ");try{let i=await T.selectFrom("job_quarantine").where("job_name","=",e).where("payload_hash","=",n).select(["id"]).executeTakeFirst();if(i)await T.updateTable("job_quarantine").set({quarantined_at:r}).where("id","=",i.id).execute();else await T.insertInto("job_quarantine").values({job_name:e,payload_hash:n,failure_count:0,window_start:r,quarantined_at:r}).execute()}catch(i){if(_(i)){me();return}throw i}}async function Lt(e){try{await T.deleteFrom("job_quarantine").where("job_name","=",e).execute()}catch(t){if(_(t)){me();return}throw t}}async function Ut(){try{return await T.selectFrom("job_quarantine").whereNotNull("quarantined_at").selectAll().execute()??[]}catch(e){if(_(e))return me(),[];throw e}}var Bt=!1;var Te=()=>{};function z(e){let t=e?.numUpdatedRows;if(t===null||t===void 0)return 0;if(typeof t==="object")return Number(t.changes??0);return Number(t)}function kr(e,t){let n=O(e,t.payload||{},{queue:t.queue,tries:t.maxTries,timeout:t.timeout,backoff:Array.isArray(t.backoff)?t.backoff:void 0});return{queue:t.queue||"default",payload:N(n),attempts:0,available_at:xr(t.delay||0),created_at:new Date().toISOString().slice(0,19).replace("T"," ")}}async function Ye(e,t){let n=kr(e,t),{db:r}=await import("@stacksjs/database");await r.insertInto("jobs").values(n).execute()}function xr(e){let t=Date.now();return Math.floor(t/1000+e)}var Ce=D(()=>{te()});var Q={};M(Q,{OnQueueEvent:()=>Kt,QueueEvents:()=>qe,QueueMetrics:()=>Re,emitQueueEvent:()=>V,getGlobalMetrics:()=>De,getQueueEvents:()=>ie,getWorkerTracker:()=>ge,onQueueEvent:()=>zt,withEvents:()=>Vt});import{log as I}from"@stacksjs/logging";class qe{handlers=new Map;wildcardHandlers=new Set;listenerSubscriptions=new WeakMap;reclaim=new FinalizationRegistry((e)=>{e()});on(e,t){if(!this.handlers.has(e))this.handlers.set(e,new Set);return this.handlers.get(e).add(t),()=>{this.handlers.get(e)?.delete(t)}}subscribeListener(e,t,n){let r=new WeakRef(e),i=(s)=>{let u=r.deref();if(u===void 0){o();return}return n.call(u,s)},o=this.on(t,i),a=()=>{o();let s=r.deref();if(s===void 0)return;let u=this.listenerSubscriptions.get(s);if(!u)return;if(u.delete(a),u.size===0)this.listenerSubscriptions.delete(s),this.reclaim.unregister(s)},l=this.listenerSubscriptions.get(e);if(!l)l=new Set,this.listenerSubscriptions.set(e,l);return l.add(a),this.reclaim.register(e,o,e),a}unsubscribeListener(e){let t=this.listenerSubscriptions.get(e);if(!t)return 0;let n=Array.from(t);for(let r of n)r();return this.listenerSubscriptions.delete(e),this.reclaim.unregister(e),n.length}listenerCount(e){if(e==="*")return this.wildcardHandlers.size;return this.handlers.get(e)?.size??0}onAny(e){return this.wildcardHandlers.add(e),()=>{this.wildcardHandlers.delete(e)}}once(e,t){let n=async(r)=>{this.handlers.get(e)?.delete(n),await t(r)};return this.on(e,n)}async emit(e,t){let n={...t,timestamp:Date.now()};this.logEvent(e,n);let r=this.handlers.get(e);if(r)for(let i of r)try{await i(n)}catch(o){I.error(`Error in queue event handler for ${e}:`,o)}for(let i of this.wildcardHandlers)try{await i(e,n)}catch(o){I.error("Error in wildcard queue event handler:",o)}}logEvent(e,t){let n=t.jobId?`[${t.jobId}]`:"",r=t.queueName?`on ${t.queueName}`:"";switch(e){case"job:added":I.debug(`Job added ${n} ${r}`);break;case"job:processing":I.debug(`Job processing ${n} ${r}`);break;case"job:completed":I.info(`Job completed ${n} ${r} in ${t.duration}ms`);break;case"job:failed":I.error(`Job failed ${n} ${r}:`,t.error);break;case"job:retrying":I.warn(`Job retrying ${n} ${r} (attempt ${t.attemptsMade})`);break;case"job:stalled":I.warn(`Job stalled ${n} ${r}`);break;case"queue:error":I.error(`Queue error ${r}:`,t.error);break}}off(e){this.handlers.delete(e)}removeAllListeners(){this.handlers.clear(),this.wildcardHandlers.clear(),this.listenerSubscriptions=new WeakMap}}function ie(){if(!Xe)Xe=new qe;return Xe}function zt(e,t){let n=ie();if(e==="*")return n.onAny(t);return n.on(e,t)}function V(e,t){return ie().emit(e,t)}function Vt(e,t){return async(...n)=>{let r=n[0]?.id||"unknown",i=Date.now();await V("job:processing",{jobId:r,queueName:e,data:n[0]?.data});try{let o=await t(...n);return await V("job:completed",{jobId:r,queueName:e,result:o,duration:Date.now()-i}),o}catch(o){throw await V("job:failed",{jobId:r,queueName:e,error:o,duration:Date.now()-i}),o}}}function Kt(e){return function(t,n){if(typeof n!=="object"||n===null)throw TypeError(`@OnQueueEvent('${e}') requires standard (TC39) decorators. Legacy decorators ('experimentalDecorators: true') give a method decorator no construction-time hook, so the handler cannot be bound to an instance.`);if(n.kind!=="method")throw TypeError(`@OnQueueEvent('${e}') can only decorate a class method, but it was applied to a ${n.kind}${n.name===void 0?"":` ('${String(n.name)}')`}. Move the handler into a method, or call onQueueEvent('${e}', handler) directly.`);if(n.static)throw TypeError(`@OnQueueEvent('${e}') cannot decorate the static method '${String(n.name)}': a static method has no instance, so it would subscribe at class-definition time and stay subscribed for the life of the process, even if the class is never used. Use an instance method, or subscribe explicitly with onQueueEvent('${e}', MyClass.${String(n.name)}).`);return n.addInitializer(function(){ie().subscribeListener(this,e,t)}),t}}class Re{jobCounts={added:0,completed:0,failed:0,processing:0};completions=[];errors=[];unsubscribe=[];constructor(){this.setupListeners()}setupListeners(){let e=ie();this.unsubscribe.push(e.on("job:added",()=>{this.jobCounts.added++}),e.on("job:processing",()=>{this.jobCounts.processing++}),e.on("job:completed",(t)=>{this.jobCounts.completed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1);let n=t.duration||0;if(this.completions.push({timestamp:Date.now(),duration:n}),this.completions.length>1000)this.completions.shift()}),e.on("job:failed",(t)=>{if(this.jobCounts.failed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1),t.error){if(this.errors.push({error:t.error,timestamp:Date.now()}),this.errors.length>100)this.errors.shift()}}))}getThroughputPerMinute(){let e=Date.now()-60000;return this.completions.filter((n)=>n.timestamp>=e).length}getAverageProcessingTime(){let e=Date.now()-60000,t=this.completions.filter((n)=>n.timestamp>=e);if(t.length===0)return 0;return t.reduce((n,r)=>n+r.duration,0)/t.length}getMetrics(){return{counts:{...this.jobCounts},averageDuration:this.getAverageProcessingTime(),recentErrors:[...this.errors],throughputPerMinute:this.getThroughputPerMinute()}}reset(){this.jobCounts={added:0,completed:0,failed:0,processing:0},this.completions=[],this.errors=[]}stop(){this.unsubscribe.forEach((e)=>e()),this.unsubscribe=[]}}function De(){if(!Ze)Ze=new Re;return Ze}class Gt{workers=new Map;register(e,t){this.workers.set(e,{id:e,status:"idle",queue:t,processedCount:0,failedCount:0,lastActivityAt:new Date().toISOString(),startedAt:new Date().toISOString()})}markActive(e){let t=this.workers.get(e);if(t)t.status="active",t.lastActivityAt=new Date().toISOString()}markIdle(e){let t=this.workers.get(e);if(t)t.status="idle",t.lastActivityAt=new Date().toISOString()}recordCompletion(e){let t=this.workers.get(e);if(t)t.processedCount++,t.lastActivityAt=new Date().toISOString()}recordFailure(e){let t=this.workers.get(e);if(t)t.failedCount++,t.lastActivityAt=new Date().toISOString()}unregister(e){let t=this.workers.get(e);if(t)t.status="stopped"}getAll(){return Array.from(this.workers.values())}clear(){this.workers.clear()}}function ge(){return jr}var Xe=null,Ze=null,jr;var C=D(()=>{jr=new Gt});var U={};M(U,{Batch:()=>et,DispatchedBatch:()=>L,PendingBatch:()=>Oe,batchCounterIncrements:()=>Yt,batchRecordFromHash:()=>it,batchRecordToHash:()=>rn,getBatchCallbacks:()=>we,isBatchCancelled:()=>cn,recordBatchJobCompletion:()=>at,recordBatchJobFailure:()=>st});var{RedisClient:Jr}=globalThis.Bun;import{log as k}from"@stacksjs/logging";import{env as Sr}from"@stacksjs/env";function R(){return Sr.QUEUE_DRIVER||"sync"}class Oe{jobs;options={thenCallbacks:[],catchCallbacks:[],finallyCallbacks:[],progressCallbacks:[]};constructor(e){this.jobs=e.map((t)=>("job"in t)?t:{job:t})}name(e){return this.options.name=e,this}onQueue(e){return this.options.queue=e,this}allowFailures(){return this.options.allowFailures=!0,this}then(e){return this.options.thenCallbacks.push(e),this}catch(e){return this.options.catchCallbacks.push(e),this}finally(e){return this.options.finallyCallbacks.push(e),this}progress(e){return this.options.progressCallbacks.push(e),this}thenHandler(e){return this.options.thenHandler=e,this}catchHandler(e){return this.options.catchHandler=e,this}finallyHandler(e){return this.options.finallyHandler=e,this}async dispatch(){let e=crypto.randomUUID(),t=this.jobs.length;if(t===0)throw Error("Cannot dispatch an empty batch");let n=R();await Pr({id:e,name:this.options.name||"",total_jobs:t,pending_jobs:t,failed_jobs:0,failed_job_ids:"[]",options:JSON.stringify({queue:this.options.queue,allowFailures:this.options.allowFailures||!1}),cancelled_at:null,created_at:new Date().toISOString().slice(0,19).replace("T"," "),finished_at:null,then_handler:this.options.thenHandler?JSON.stringify(this.options.thenHandler):null,catch_handler:this.options.catchHandler?JSON.stringify(this.options.catchHandler):null,finally_handler:this.options.finallyHandler?JSON.stringify(this.options.finallyHandler):null}),Er(e,this.options);try{let{emitQueueEvent:r}=await Promise.resolve().then(() => (C(),Q));await r("batch:added",{jobId:e,data:{name:this.options.name,totalJobs:t}})}catch{}for(let r=0;r<this.jobs.length;r++){let i=this.jobs[r];if(!i)continue;let{job:o,payload:a}=i,l={...a,_batchId:e,_batchIndex:r};if(this.options.queue&&!o.queue)o.queue=this.options.queue;if(n==="sync")try{await o.dispatchNow(l),await at(e)}catch(s){await st(e,`${e}:${r}`,s)}else await o.dispatch(l)}return k.info(`[Batch] Dispatched batch "${this.options.name||e}" with ${t} jobs`),new L(e)}getJobs(){return[...this.jobs]}getOptions(){return this.options}}class L{id;constructor(e){this.id=e}async fresh(){return H(this.id)}async getName(){return(await this.fresh())?.name||""}async totalJobs(){return(await this.fresh())?.total_jobs||0}async pendingJobs(){return(await this.fresh())?.pending_jobs||0}async failedJobs(){return(await this.fresh())?.failed_jobs||0}async completedJobs(){let e=await this.fresh();if(!e)return 0;return e.total_jobs-e.pending_jobs}async progress(){let e=await this.fresh();if(!e||e.total_jobs===0)return 0;let t=e.total_jobs-e.pending_jobs;return Math.round(t/e.total_jobs*100)}async finished(){return(await this.fresh())?.finished_at!==null}async cancelled(){return(await this.fresh())?.cancelled_at!==null}async hasFailures(){return((await this.fresh())?.failed_jobs||0)>0}async failedJobIds(){let e=await this.fresh();if(!e)return[];try{return JSON.parse(e.failed_job_ids||"[]")}catch{return[]}}async cancel(){if(R()==="redis")await Ir(this.id);else await Or(this.id);k.info(`[Batch] Cancelled batch ${this.id}`);let t=we(this.id);if(t)for(let n of t.finallyCallbacks)try{await n(this)}catch(r){k.error(`[Batch] Error in finally callback for batch ${this.id}:`,r)}}async add(e){let t=await this.fresh();if(!t)throw Error(`Batch ${this.id} not found`);if(t.cancelled_at)throw Error(`Batch ${this.id} has been cancelled`);if(t.finished_at)throw Error(`Batch ${this.id} has already finished`);let n=e.map((o)=>("job"in o)?o:{job:o});if(!await qr(this.id,n.length))throw Error(`Batch ${this.id} was cancelled, finished or deleted before the jobs could be added`);let i=JSON.parse(t.options||"{}");for(let o=0;o<n.length;o++){let a=n[o];if(!a)continue;let{job:l,payload:s}=a,u={...s,_batchId:this.id,_batchIndex:t.total_jobs+o};if(i.queue&&!l.queue)l.queue=i.queue;await l.dispatch(u)}k.info(`[Batch] Added ${n.length} jobs to batch ${this.id}`)}async delete(){await Rr(this.id),$e(this.id)}}class et{static create(e){return new Oe(e)}static async find(e){if(!await H(e))return null;return new L(e)}static async all(){return(await Tr()).map((t)=>new L(t.id))}static async prune(e=24){return Dr(e)}}function Er(e,t){tt.set(e,t)}function we(e){return tt.get(e)}function $e(e){tt.delete(e)}async function Pr(e){if(R()==="redis")await Fr(e);else await Xt(e)}async function H(e){if(R()==="redis")return Nr(e);return Zt(e)}async function Tr(){if(R()==="redis")return on();return en()}async function Cr(e,t){if(R()==="redis")await ot(e,t);else await nt(e,t)}async function qr(e,t){if(t===0)return!0;if(R()==="redis")try{let o=await G(),a=`${K}${e}`;return await o.hincrby(a,"total_jobs",t),await o.hincrby(a,"pending_jobs",t),o.close(),!0}catch{}let{db:n,sql:r}=await import("@stacksjs/database"),i=await n.updateTable("job_batches").set(Yt(r,t)).where("id","=",e).whereNull("cancelled_at").whereNull("finished_at").executeTakeFirst();return z(i)>0}function Yt(e,t){return{total_jobs:e`total_jobs + ${t}`,pending_jobs:e`pending_jobs + ${t}`}}async function Rr(e){if(R()==="redis")await sn(e);else await tn(e)}async function Dr(e){if(R()==="redis")return Qr(e);return nn(e)}function $r(e){return!!(e.then_handler||e.catch_handler||e.finally_handler)}async function Xt(e){let{db:t}=await import("@stacksjs/database"),n={id:e.id,name:e.name,total_jobs:e.total_jobs,pending_jobs:e.pending_jobs,failed_jobs:e.failed_jobs,failed_job_ids:e.failed_job_ids,options:e.options,cancelled_at:e.cancelled_at,created_at:e.created_at,finished_at:e.finished_at};if(!$r(e)){await t.insertInto("job_batches").values(n).execute();return}try{await t.insertInto("job_batches").values({...n,then_handler:e.then_handler??null,catch_handler:e.catch_handler??null,finally_handler:e.finally_handler??null}).execute()}catch(r){k.warn(`[Batch] Could not persist terminal handlers for batch ${e.id}: ${r?.message}. The job_batches table predates the then_handler/catch_handler/finally_handler columns; recreate it to enable handlers that survive a worker restart.`),await t.insertInto("job_batches").values(n).execute()}}async function Zt(e){let{db:t}=await import("@stacksjs/database");return await t.selectFrom("job_batches").where("id","=",e).selectAll().executeTakeFirst()||null}async function en(){let{db:e}=await import("@stacksjs/database");return await e.selectFrom("job_batches").selectAll().orderBy("created_at","desc").execute()}async function nt(e,t){let{db:n}=await import("@stacksjs/database");await n.updateTable("job_batches").set(t).where("id","=",e).execute()}async function tn(e){let{db:t}=await import("@stacksjs/database");await t.deleteFrom("job_batches").where("id","=",e).execute()}async function Or(e){await nt(e,{cancelled_at:new Date().toISOString().slice(0,19).replace("T"," "),finished_at:new Date().toISOString().slice(0,19).replace("T"," ")})}async function nn(e){let{db:t}=await import("@stacksjs/database"),n=new Date(Date.now()-e*60*60*1000).toISOString().slice(0,19).replace("T"," "),r=await t.deleteFrom("job_batches").whereNotNull("finished_at").where("finished_at","<",n).executeTakeFirst();return Number(r?.numDeletedRows??0)}async function Ar(){let{queue:e}=await import("@stacksjs/config"),t=e?.connections?.redis?.redis;if(t?.url)return t.url;let n=t?.password?`:${encodeURIComponent(t.password)}@`:"",r=t?.db?`/${t.db}`:"";return`redis://${n}${t?.host||"localhost"}:${t?.port||6379}${r}`}async function G(){let e=new Jr(await Ar());return await e.connect(),e}function rn(e){return{id:e.id,name:e.name,total_jobs:String(e.total_jobs),pending_jobs:String(e.pending_jobs),failed_jobs:String(e.failed_jobs),failed_job_ids:e.failed_job_ids,options:e.options,cancelled_at:e.cancelled_at||"",created_at:e.created_at,finished_at:e.finished_at||"",then_handler:e.then_handler||"",catch_handler:e.catch_handler||"",finally_handler:e.finally_handler||""}}async function Fr(e){try{let t=await G();await t.hset(`${K}${e.id}`,rn(e)),await t.sadd(rt,e.id),t.close()}catch{await Xt(e)}}function it(e){if(!e?.id)return null;return{id:e.id,name:e.name??"",total_jobs:Number(e.total_jobs),pending_jobs:Number(e.pending_jobs),failed_jobs:Number(e.failed_jobs),failed_job_ids:e.failed_job_ids??"",options:e.options??"",cancelled_at:e.cancelled_at||null,created_at:e.created_at??"",finished_at:e.finished_at||null,then_handler:e.then_handler||null,catch_handler:e.catch_handler||null,finally_handler:e.finally_handler||null}}async function Nr(e){try{let t=await G(),n=`${K}${e}`,r=await t.hgetall(n);return t.close(),it(r)}catch{return Zt(e)}}async function on(){try{let e=await G(),t=await e.smembers(rt),n=[];for(let r of t){let i=it(await e.hgetall(`${K}${r}`));if(i)n.push(i)}return e.close(),n}catch{return en()}}async function ot(e,t){try{let n=await G(),r=`${K}${e}`,i={};for(let[o,a]of Object.entries(t))i[o]=a===null?"":String(a);await n.hset(r,i),n.close()}catch{await nt(e,t)}}async function an(e,t,n){let r=await G();try{let i=`${K}${e}`,o=await r.hincrby(i,"pending_jobs",-1),a=Math.max(o,0);if(o<0)await r.hset(i,{pending_jobs:"0"});if(t)await r.hincrby(i,"failed_jobs",1);let s=!(await r.hgetall(i)).finished_at&&(!t||n?a===0:!0),u=new Date().toISOString().slice(0,19).replace("T"," "),c=s&&await r.hsetnx(i,"terminal_claimed","1");if(c)await r.hset(i,{finished_at:u});if(c&&t&&!n)await r.hset(i,{cancelled_at:u});return c}finally{r.close()}}async function sn(e){try{let t=await G();await t.del(`${K}${e}`),await t.srem(rt,e),t.close()}catch{await tn(e)}}async function Ir(e){let t=new Date().toISOString().slice(0,19).replace("T"," ");await ot(e,{cancelled_at:t,finished_at:t})}async function Qr(e){try{let t=await on(),n=Date.now()-e*60*60*1000,r=0;for(let i of t)if(i.finished_at){if(new Date(i.finished_at).getTime()<n)await sn(i.id),r++}return r}catch{return nn(e)}}function oe(e){if(!e)return null;try{let t=JSON.parse(e);if(t&&(t.kind==="job"||t.kind==="module"))return t;return k.warn(`[Batch] handler JSON has unknown kind '${t?.kind}' - skipping`),null}catch(t){return k.warn(`[Batch] failed to parse persistent handler: ${t.message}`),null}}async function ae(e,t){try{if(e.kind==="job"){let{Jobs:i}=await Promise.resolve().then(() => (ye(),Ae));await i.dispatch(e.name,{...e.payload??{},_batchId:t});return}let n=await import(e.module).catch((i)=>(k.warn(`[Batch] persistent handler module not found: ${e.module} (${i.message})`),null));if(!n)return;let r=n[e.export];if(typeof r!=="function"){k.warn(`[Batch] persistent handler export '${e.export}' is not a function on ${e.module}`);return}await r(e.payload,t)}catch(n){k.error(`[Batch] persistent handler threw for batch ${t}:`,n)}}async function at(e){let t=R(),n=!1;if(t==="redis")n=await an(e,!1,!1);else{let{db:o,sql:a}=await import("@stacksjs/database");await o.updateTable("job_batches").set({pending_jobs:a`GREATEST(pending_jobs - 1, 0)`}).where("id","=",e).where("pending_jobs",">",0).execute();let l=new Date().toISOString().slice(0,19).replace("T"," "),s=await o.updateTable("job_batches").set({finished_at:l}).where("id","=",e).where("pending_jobs","=",0).whereNull("finished_at").executeTakeFirst();n=z(s)>0}let r=we(e),i=new L(e);if(r)for(let o of r.progressCallbacks)try{await o(i)}catch(a){k.error(`[Batch] Error in progress callback for batch ${e}:`,a)}if(n){try{let{emitQueueEvent:c}=await Promise.resolve().then(() => (C(),Q));await c("batch:completed",{jobId:e})}catch{}let o=await H(e);if(!o){$e(e),k.info(`[Batch] Batch ${e} finished (record vanished)`);return}let a=JSON.parse(o.options||"{}"),s=!((o.failed_jobs||0)>0)||a.allowFailures;if(r){if(s)for(let c of r.thenCallbacks)try{await c(i)}catch(d){k.error(`[Batch] Error in then callback for batch ${e}:`,d)}for(let c of r.finallyCallbacks)try{await c(i)}catch(d){k.error(`[Batch] Error in finally callback for batch ${e}:`,d)}$e(e)}if(s){let c=oe(o.then_handler);if(c)await ae(c,e)}else{let c=oe(o.catch_handler);if(c)await ae(c,e)}let u=oe(o.finally_handler);if(u)await ae(u,e);k.info(`[Batch] Batch ${e} finished`)}}async function st(e,t,n){let r=await H(e);if(!r)return;let i=JSON.parse(r.options||"{}"),o=new L(e),a=we(e),l=!1;if(R()==="redis"){l=await an(e,!0,!!i.allowFailures);let c=await H(e);if(c){let d=[];try{d=JSON.parse(c.failed_job_ids||"[]")}catch{d=[]}d.push(t),await ot(e,{failed_job_ids:JSON.stringify(d)})}if(a)for(let d of a.catchCallbacks)try{await d(o,n)}catch(w){k.error(`[Batch] Error in catch callback for batch ${e}:`,w)}try{let{emitQueueEvent:d}=await Promise.resolve().then(() => (C(),Q));await d("batch:failed",{jobId:e,error:n})}catch{}}else{let{db:c,sql:d}=await import("@stacksjs/database");await c.updateTable("job_batches").set({pending_jobs:d`GREATEST(pending_jobs - 1, 0)`,failed_jobs:d`failed_jobs + 1`}).where("id","=",e).where("pending_jobs",">",0).execute();try{let p=await H(e);if(p){let x=[];try{x=JSON.parse(p.failed_job_ids||"[]")}catch{x=[]}x.push(t),await Cr(e,{failed_job_ids:JSON.stringify(x)})}}catch{}if(a)for(let p of a.catchCallbacks)try{await p(o,n)}catch(x){k.error(`[Batch] Error in catch callback for batch ${e}:`,x)}try{let{emitQueueEvent:p}=await Promise.resolve().then(() => (C(),Q));await p("batch:failed",{jobId:e,error:n})}catch{}let w=new Date().toISOString().slice(0,19).replace("T"," "),f=c.updateTable("job_batches").set(i.allowFailures?{finished_at:w}:{finished_at:w,cancelled_at:w}).where("id","=",e).whereNull("finished_at");if(i.allowFailures)f=f.where("pending_jobs","=",0);let g=await f.executeTakeFirst();l=z(g)>0}if(!l)return;let s=!!i.allowFailures;if(a){if(s)for(let c of a.thenCallbacks)try{await c(o)}catch(d){k.error(`[Batch] Error in then callback for batch ${e}:`,d)}for(let c of a.finallyCallbacks)try{await c(o)}catch(d){k.error(`[Batch] Error in finally callback for batch ${e}:`,d)}$e(e)}let u=await H(e);if(u){if(s){let d=oe(u.then_handler);if(d)await ae(d,e)}else{let d=oe(u.catch_handler);if(d)await ae(d,e)}let c=oe(u.finally_handler);if(c)await ae(c,e)}k.info(`[Batch] Batch ${e} finished with failure(s)`)}async function cn(e){return(await H(e))?.cancelled_at!==null}var tt,K="stacks:batch:",rt="stacks:batches";var B=D(()=>{Ce();tt=new Map});var Ae={};M(Ae,{Jobs:()=>hn,job:()=>dn,jobBatch:()=>pn,resolveJobFile:()=>fn,runJob:()=>Fe});import{appPath as Br,frameworkPath as Mr}from"@stacksjs/path";import{env as Hr}from"@stacksjs/env";import{enqueueAfterCommit as Lr,isInTransaction as Ur}from"@stacksjs/database";function Wr(){return Hr.QUEUE_DRIVER||"sync"}function zr(){if(un)return;un=!0,console.warn("[queue] .afterCommit() was called outside of `db.transaction(...)`. Dispatching immediately. Wrap the call in a transaction or drop .afterCommit() to silence this message.")}class A{name;payload;options={};txMode="auto";constructor(e,t){this.name=e;this.payload=t}onQueue(e){return this.options.queue=e,this}delay(e){return this.options.delay=e,this}tries(e){return this.options.tries=e,this}timeout(e){return this.options.timeout=e,this}backoff(e){return this.options.backoff=e,this}withContext(e){return this.options.context=e,this}withIdempotencyKey(e){return this.options.idempotencyKey=e,this}afterCommit(){return this.txMode="after",this}withoutCommit(){return this.txMode="immediate",this}async dispatch(){let{isFaked:e,getFakeQueue:t}=await Promise.resolve().then(() => Ue);if(e()){t()?.dispatch(this.name,this.payload,this.options);return}if(this.txMode!=="immediate"){if(Ur()){let r=this.runDispatchPipeline.bind(this);if(Lr(async()=>{await r()}))return}else if(this.txMode==="after")zr()}await this.runDispatchPipeline()}async runDispatchPipeline(){let e=!1;if(this.options.idempotencyKey){let t=await Ve(this.options.idempotencyKey,this.name,this.options.queue);if(t==="duplicate")return;e=t==="claimed"}try{if(await Pe(this.name,this.payload)){let n=O(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff});if(await Se({queue:this.options.queue||"default",payload:N(n),exception:`quarantined: ${this.name}`},"poison-detected"))return}let t=Wr();if(t==="database")await this.dispatchToDatabase();else if(t==="redis")await this.dispatchToRedis();else if(t==="sync")await Fe(this.name,{payload:this.payload,context:this.options.context,traceId:await ln()});else if(t==="sqs"||t==="memory"||t==="beanstalkd")throw Error(`[queue] Driver "${t}" is not implemented yet. Set QUEUE_DRIVER to one of: redis, database, sync.`);else throw Error(`[queue] Unknown QUEUE_DRIVER "${t}". Allowed values: redis, database, sync.`)}catch(t){if(e&&this.options.idempotencyKey)await Ke(this.options.idempotencyKey);throw t}}async dispatchIf(e){if(e)await this.dispatch()}async dispatchUnless(e){if(!e)await this.dispatch()}async dispatchToDatabase(){let e=Math.floor(Date.now()/1000),t=this.options.delay?e+this.options.delay:e,n=O(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff},await ln()),r=N(n),{db:i}=await import("@stacksjs/database");await i.insertInto("jobs").values({queue:this.options.queue||"default",payload:r,attempts:0,reserved_at:null,available_at:t,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(){let e=O(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff});de(e);let{RedisQueue:t}=await Promise.resolve().then(() => (fe(),pe)),{queue:n}=await import("@stacksjs/config"),r=n?.connections?.redis;if(!r)throw Error("Redis queue connection is not configured. Check config/queue.ts");await new t(this.options.queue||"default",r).add(e,{delay:this.options.delay,maxTries:this.options.tries,timeout:this.options.timeout,backoff:this.options.backoff})}async dispatchNow(){await Fe(this.name,{payload:this.payload,context:this.options.context})}}function dn(e,t){return new A(e,t)}function pn(e){let{PendingBatch:t}=(B(),er(U));return new t(e)}async function fn(e){let t=[Br(`Jobs/${e}.ts`),Mr(`defaults/app/Jobs/${e}.ts`)];try{let n=import.meta.resolve("@stacksjs/defaults/package.json"),r=new URL(".",n).pathname;t.push(`${r}app/Jobs/${e}.ts`)}catch{}for(let n of t)if(await Bun.file(n).exists())return n;return null}async function Fe(e,t={}){let{withTraceId:n}=await import("@stacksjs/router"),r=t.traceId??`job:${e}:${Math.random().toString(36).slice(2,10)}`;await n(r,async()=>{let i=await fn(e);if(!i)throw Error(`Job ${e} not found. Looked in app/Jobs/${e}.ts and the framework defaults (storage/framework/defaults/app/Jobs, @stacksjs/defaults).`);let a=(await import(i)).default;if(!a)throw Error(`Job ${e} does not export a default`);if(typeof a.handle==="function")await a.handle(t.payload);else if(typeof a.action==="string"){let{runAction:l}=await import("@stacksjs/actions");await l(a.action)}else if(typeof a.action==="function")await a.action();else if(typeof a==="function")await a(t.payload,t.context);else throw Error(`Job ${e} does not have a valid handler`)})}async function ln(){try{let{getTraceId:e}=await import("@stacksjs/router");return e()}catch{return}}var un=!1,hn;var ye=D(()=>{Ee();te();Ge();Te();hn={make(e,t){return new A(e,t)},async dispatch(e,t){await new A(e,t).dispatch()},async dispatchIf(e,t,n){if(e)await new A(t,n).dispatch()},async dispatchUnless(e,t,n){if(!e)await new A(t,n).dispatch()},async dispatchNow(e,t){await new A(e,t).dispatchNow()},dispatchAfter(e,t,n){return new A(t,n).delay(e)},async dispatchOnce(e,t,n){await new A(t,n).withIdempotencyKey(e).dispatch()},async dispatchAfterCommit(e,t){await new A(e,t).afterCommit().dispatch()}}});var Ne={};M(Ne,{isCircuitOpen:()=>bn,listCircuitState:()=>_n,pauseQueue:()=>yn,recordCircuitFailure:()=>wn,recordCircuitSuccess:()=>gn,resumeQueue:()=>vn});import{db as E}from"@stacksjs/database";function Y(){if(mn)return;mn=!0,console.warn("[queue/circuit-breaker] queue_circuit_state table missing - circuit breaker disabled. Run migrations to enable.")}async function ut(e,t){try{let n=await E.selectFrom("queue_circuit_state").where("queue_name","=",e).selectAll().executeTakeFirst();if(n)return n;return await E.insertInto("queue_circuit_state").values({queue_name:e,success_count:0,failure_count:0,window_start:t,paused_at:null,resume_at:null}).execute(),{queue_name:e,success_count:0,failure_count:0,window_start:t,paused_at:null,resume_at:null}}catch(n){if(_(n))return Y(),null;let r=n?.message??"";if(r.includes("UNIQUE constraint")||r.includes("Duplicate entry"))return await E.selectFrom("queue_circuit_state").where("queue_name","=",e).selectAll().executeTakeFirst();throw n}}async function bn(e){try{let t=await E.selectFrom("queue_circuit_state").where("queue_name","=",e).select(["paused_at","resume_at"]).executeTakeFirst();if(!t||!t.paused_at)return!1;if(t.resume_at){let n=Date.parse(t.resume_at.replace(" ","T")+"Z");if(Number.isFinite(n)&&Date.now()>=n)return await E.updateTable("queue_circuit_state").set({paused_at:null,resume_at:null,success_count:0,failure_count:0}).where("queue_name","=",e).execute(),!1}return!0}catch(t){if(_(t))return Y(),!1;throw t}}async function gn(e,t={}){let n=t.windowSeconds??300,r=new Date().toISOString().slice(0,19).replace("T"," "),i=await ut(e,r);if(!i)return;try{if(ct(i.window_start,n)){await E.updateTable("queue_circuit_state").set({success_count:1,failure_count:0,window_start:r}).where("queue_name","=",e).execute();return}await E.updateTable("queue_circuit_state").set({success_count:i.success_count+1}).where("queue_name","=",e).execute()}catch(o){if(_(o)){Y();return}throw o}}async function wn(e,t={}){let n=t.failureRateThreshold??0.5,r=t.windowSeconds??300,i=t.pauseSeconds??300,o=t.minObservations??10,a=new Date,l=a.toISOString().slice(0,19).replace("T"," "),s=await ut(e,l);if(!s)return!1;if(s.paused_at)return!1;try{let{success_count:u,failure_count:c}=s;if(ct(s.window_start,r))u=0,c=0;c+=1;let d=u+c,w=d===0?0:c/d;if(d>=o&&w>=n){let g=new Date(a.getTime()+i*1000).toISOString().slice(0,19).replace("T"," ");return await E.updateTable("queue_circuit_state").set({success_count:u,failure_count:c,window_start:l,paused_at:l,resume_at:g}).where("queue_name","=",e).execute(),!0}return await E.updateTable("queue_circuit_state").set({success_count:u,failure_count:c,window_start:ct(s.window_start,r)?l:s.window_start}).where("queue_name","=",e).execute(),!1}catch(u){if(_(u))return Y(),!1;throw u}}function ct(e,t){if(!e)return!0;let n=Date.parse(e.replace(" ","T")+"Z");if(!Number.isFinite(n))return!0;return Date.now()-n>t*1000}async function yn(e,t=300){let n=new Date,r=n.toISOString().slice(0,19).replace("T"," "),i=new Date(n.getTime()+t*1000).toISOString().slice(0,19).replace("T"," ");await ut(e,r);try{await E.updateTable("queue_circuit_state").set({paused_at:r,resume_at:i}).where("queue_name","=",e).execute()}catch(o){if(_(o)){Y();return}throw o}}async function vn(e){try{await E.updateTable("queue_circuit_state").set({paused_at:null,resume_at:null,success_count:0,failure_count:0}).where("queue_name","=",e).execute()}catch(t){if(_(t)){Y();return}throw t}}async function _n(){try{return await E.selectFrom("queue_circuit_state").selectAll().execute()??[]}catch(e){if(_(e))return Y(),[];throw e}}var mn=!1;var ve=()=>{};te();import{env as wr}from"@stacksjs/env";function Dt(){return wr.QUEUE_DRIVER||"sync"}class $t{name;description;action;handle;queue;rate;tries;timeout;backoff;backoffConfig;enabled;constructor(e){this.name=e.name,this.description=e.description,this.handle=e.handle,this.queue=e.queue,this.rate=e.rate,this.action=e.action,this.tries=e.tries,this.timeout=e.timeout,this.backoff=e.backoff,this.backoffConfig=e.backoffConfig,this.enabled=e.enabled}async dispatch(e){let{isFaked:t,getFakeQueue:n}=await Promise.resolve().then(() => Ue);if(t()){n()?.dispatch(this.name||"UnknownJob",e,{queue:this.queue,tries:this.tries,timeout:this.timeout});return}let r=Dt();if(r==="sync")return this.dispatchNow(e);if(r==="redis")return this.dispatchToRedis(e);if(r==="database")return this.dispatchToDatabase(e);if(r==="sqs"||r==="memory"||r==="beanstalkd")throw Error(`[queue] Driver "${r}" is not implemented yet. Set QUEUE_DRIVER to one of: redis, database, sync.`);throw Error(`[queue] Unknown QUEUE_DRIVER "${r}". Allowed values: redis, database, sync.`)}async dispatchIf(e,t){if(e)return this.dispatch(t)}async dispatchUnless(e,t){if(!e)return this.dispatch(t)}async dispatchAfter(e,t){let n=Dt();if(n==="redis")return this.dispatchToRedis(t,{delay:e});if(n==="database")return this.dispatchToDatabase(t,{delay:e});return await new Promise((r)=>setTimeout(r,e*1000)),await this.dispatchNow(t)}async dispatchNow(e){if(typeof this.handle==="function")await this.handle(e);else if(typeof this.action==="string"){let{runAction:t}=await import("@stacksjs/actions");await t(this.action)}else if(typeof this.action==="function")await this.action();else throw Error(`Job ${this.name} does not have a valid handler`)}async dispatchToDatabase(e,t){let n=Math.floor(Date.now()/1000),r=t?.delay?n+t.delay:n,i=O(this.name??this.constructor.name,e,{queue:this.queue??"default",tries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0}),o=N(i),{db:a}=await import("@stacksjs/database");await a.insertInto("jobs").values({queue:this.queue||"default",payload:o,attempts:0,reserved_at:null,available_at:r,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(e,t){let n=O(this.name??this.constructor.name,e,{queue:this.queue??"default",tries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0});de(n);let{RedisQueue:r}=await Promise.resolve().then(() => (fe(),pe)),{queue:i}=await import("@stacksjs/config"),o=i?.connections?.redis;if(!o)throw Error("Redis queue connection is not configured. Check config/queue.ts");await new r(this.queue||"default",o).add(n,{delay:t?.delay,maxTries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0})}}ye();Ge();Ee();Te();ve();te();var lt=(e)=>`__job_progress__:${e}`,dt=(e)=>`__job_cancel__:${e}`;function Vr(e){return Number.isFinite(e)?Math.max(0,Math.min(100,e)):0}async function Kr(e,t,n){let{cache:r}=await import("@stacksjs/cache");await r.set(lt(e),{percent:Vr(t),message:n,updatedAt:Date.now()},3600)}async function Gr(e){let{cache:t}=await import("@stacksjs/cache");return await t.get(lt(e))??null}async function Yr(e){let{cache:t}=await import("@stacksjs/cache");await t.set(dt(e),1,3600)}async function Xr(e){let{cache:t}=await import("@stacksjs/cache");return Boolean(await t.get(dt(e)))}async function Zr(e){let{cache:t}=await import("@stacksjs/cache");await Promise.all([t.del(lt(e)),t.del(dt(e))])}import{log as se}from"@stacksjs/logging";import*as kn from"@stacksjs/path";class xn{jobs=new Map;initialized=!1;register(e){this.jobs.set(e.name,e),se.debug(`Registered job: ${e.name} (${e.type})`)}get(e){return this.jobs.get(e)}all(){return Array.from(this.jobs.values())}byQueue(e){return this.all().filter((t)=>t.config.queue===e)}scheduled(){return this.all().filter((e)=>e.config.rate||e.config.schedule)}has(e){return this.jobs.has(e)}clear(){this.jobs.clear(),this.initialized=!1}setInitialized(e){this.initialized=e}isInitialized(){return this.initialized}}var X=new xn;async function ht(e){let t=e||kn.userJobsPath(),n=[];try{let r=new Bun.Glob("**/*.{ts,js}"),i={cwd:t,onlyFiles:!0,absolute:!0};for await(let o of r.scan(i)){if(o.includes(".test.")||o.includes(".spec.")||o.endsWith("index.ts")||o.endsWith("index.js"))continue;try{let a=await ei(o);if(a)n.push(a),X.register(a)}catch(a){se.warn(`Failed to load job from ${o}: ${a.message}`)}}return X.setInitialized(!0),se.info(`Discovered ${n.length} jobs from ${t}`),n}catch(r){return se.error(`Failed to discover jobs: ${r.message}`),[]}}async function ei(e){try{let t=await import(e),n=e.split("/").pop()?.replace(/\.(ts|js)$/,"")||"UnknownJob";if(t.default&&typeof t.default==="function"){let r=t.default;if(typeof r.handle==="function"||typeof r.prototype?.handle==="function"){let i=r.config||{};return{name:i.name||n,path:e,config:{name:i.name||n,description:i.description,queue:i.queue||"default",tries:i.retries||3,timeout:i.timeout,withoutOverlapping:i.withoutOverlapping,schedule:i.schedule,retryAfter:i.retryAfter},type:"class",module:r}}}if(t.default&&typeof t.default==="object"){let r=t.default;if(typeof r.handle==="function"||typeof r.action==="string")return{name:r.name||n,path:e,config:{name:r.name||n,description:r.description,queue:r.queue||"default",tries:r.tries||3,backoff:r.backoff,rate:r.rate,timeout:r.timeout||r.timeOut,backoffConfig:r.backoffConfig},type:"function",module:r}}return null}catch(t){return se.debug(`Could not load job from ${e}: ${t.message}`),null}}function ti(e){return X.get(e)}function ni(){return X.all()}function pt(){return X.scheduled()}async function ri(e,t){let n=X.get(e);if(!n)throw Error(`Job "${e}" not found. Did you run discoverJobs()?`);try{if(n.type==="class"){if(typeof n.module.handle==="function")return await n.module.handle(t);return await new n.module().handle(t)}else{if(typeof n.module.handle==="function")return await n.module.handle(t);throw Error(`Job "${e}" does not have a handle method`)}}catch(r){throw se.error(`Failed to execute job "${e}": ${r.message}`),r}}function ii(e){let t=e.config;return{name:t.name,queue:t.queue,tries:t.tries,backoff:t.backoff,timeout:t.timeout,backoffConfig:t.backoffConfig,rate:t.rate}}import{log as j}from"@stacksjs/logging";C();import{log as Jn}from"@stacksjs/logging";var jn=!1;async function Sn(){if(jn)return!0;try{let{db:e}=await import("@stacksjs/database");return await e.unsafe("CREATE TABLE IF NOT EXISTS scheduled_job_runs (job_name VARCHAR(255) PRIMARY KEY, last_run_at VARCHAR(64) NOT NULL)").execute(),jn=!0,!0}catch(e){return Jn.debug(`[scheduler] run-marker persistence unavailable, using in-memory lastRun: ${e instanceof Error?e.message:String(e)}`),!1}}async function En(e){if(!await Sn())return null;try{let{db:t}=await import("@stacksjs/database"),n=await t.selectFrom("scheduled_job_runs").where("job_name","=",e).select(["last_run_at"]).executeTakeFirst();if(!n?.last_run_at)return null;let r=new Date(n.last_run_at);return Number.isNaN(r.getTime())?null:r}catch{return null}}async function Pn(e,t){if(!await Sn())return;try{let{db:n}=await import("@stacksjs/database"),r=t.toISOString();await n.deleteFrom("scheduled_job_runs").where("job_name","=",e).execute(),await n.insertInto("scheduled_job_runs").values({job_name:e,last_run_at:r}).execute()}catch{}}function oi(e){return`%"jobName":"${e.replace(/[\\%_]/g,(n)=>`\\${n}`)}"%`}async function Tn(e){try{let{db:t}=await import("@stacksjs/database"),n=await t.unsafe("SELECT 1 AS present FROM jobs WHERE payload LIKE ? ESCAPE '\\' LIMIT 1",[oi(e)]).execute(),r=Array.isArray(n)?n:n?.rows??[];return Array.isArray(r)&&r.length>0}catch(t){return Jn.debug(`[scheduler] overlap check unavailable, dispatching anyway: ${t instanceof Error?t.message:String(t)}`),!1}}Ce();var $n={checkInterval:60000,preventOverlapping:!0},y={isRunning:!1,isShuttingDown:!1,checkInterval:null,jobs:new Map,config:{...$n}},Cn=new Set;function ai(e,t){if(Cn.has(e))return;Cn.add(e),j.warn(`[scheduler] Cron expression "${e}" specifies seconds="${t}" but the scheduler ticks at minute granularity - the seconds field is being ignored. Use a 5-field expression to avoid this warning, or wait for sub-minute scheduling support.`)}var qn=!1,Rn=new Map;function si(e){let t=Rn.get(e);if(!t)t=new Intl.DateTimeFormat("en-US",{timeZone:e,hour12:!1,month:"numeric",day:"numeric",hour:"numeric",minute:"numeric",weekday:"short"}),Rn.set(e,t);return t}function ft(e,t){if(t&&t!=="local"&&t!=="system")try{let n=si(t).formatToParts(e),r=(a)=>n.find((l)=>l.type===a)?.value??"",i={Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6},o=Number(r("hour"));if(o===24)o=0;return{minute:Number(r("minute")),hour:o,day:Number(r("day")),month:Number(r("month")),dayOfWeek:i[r("weekday")]??e.getDay()}}catch{if(!qn)qn=!0,j.warn(`[scheduler] Invalid timezone "${t}"; falling back to system local time.`)}return{minute:e.getMinutes(),hour:e.getHours(),day:e.getDate(),month:e.getMonth()+1,dayOfWeek:e.getDay()}}function ci(e,t,n){let r=ft(new Date,n),i=r.minute,o=r.hour,a=r.day,l=r.month,s=r.dayOfWeek;if(t){let p=ft(t,n);if(p.minute===i&&p.hour===o&&p.day===a)return!1}let u=e.trim().split(/\s+/);if(u.length===6){let p=u[0];if(p&&p!=="0"&&p!=="*")ai(e,p);u=u.slice(1)}if(u.length<5)return j.warn(`Invalid cron expression: ${e}`),!1;let[c,d,w,f,g]=u;return F(c,i,0,59)&&F(d,o,0,23)&&F(w,a,1,31)&&F(f,l,1,12)&&F(g,s,0,6)}function F(e,t,n,r){if(e==="*")return!0;if(e.includes(","))return e.split(",").map((a)=>Number.parseInt(a.trim(),10)).includes(t);if(e.includes("-")){let[o,a]=e.split("-").map((l)=>Number.parseInt(l.trim(),10));return t>=o&&t<=a}if(e.includes("/")){let[o,a]=e.split("/"),l=Number.parseInt(a,10);if(o==="*")return t%l===0;if(o.includes("-")){let[s,u]=o.split("-").map((c)=>Number.parseInt(c.trim(),10));return t>=s&&t<=u&&(t-s)%l===0}}let i=Number.parseInt(e,10);return!Number.isNaN(i)&&t===i}function On(e){let n={"@yearly":"0 0 1 1 *","@annually":"0 0 1 1 *","@monthly":"0 0 1 * *","@weekly":"0 0 * * 0","@daily":"0 0 * * *","@midnight":"0 0 * * *","@hourly":"0 * * * *"}[e.toLowerCase()];if(n)return n;let r=e.match(/^Every\.(\w+)$/i);if(r&&r[1]!==void 0){let o=r[1].toLowerCase();return{second:"* * * * *",fiveseconds:"* * * * *",tenseconds:"* * * * *",thirtyseconds:"* * * * *",minute:"* * * * *",fiveminutes:"*/5 * * * *",tenminutes:"*/10 * * * *",fifteenminutes:"*/15 * * * *",thirtyminutes:"*/30 * * * *",hour:"0 * * * *",twohours:"0 */2 * * *",sixhours:"0 */6 * * *",twelvehours:"0 */12 * * *",day:"0 0 * * *",week:"0 0 * * 0",month:"0 0 1 * *"}[o]||null}let i=e.split(/\s+/).length;if(i>=5&&i<=6)return e;return null}function An(e,t){let n=e.trim().split(/\s+/),r=n.length===6?n.slice(1):n;if(r.length<5)return null;let[i,o,a,l,s]=r,u=new Date;u.setSeconds(0,0);let c=527040;for(let d=1;d<=c;d++){let w=new Date(u.getTime()+d*60000),f=ft(w,t);if(F(i,f.minute,0,59)&&F(o,f.hour,0,23)&&F(a,f.day,1,31)&&F(l,f.month,1,12)&&F(s,f.dayOfWeek,0,6))return w}return null}async function ui(e={}){if(y.isRunning){j.warn("Scheduler is already running");return}y.config={...$n,...e},y.isRunning=!0,y.isShuttingDown=!1,await ht();let t=pt();for(let i of t){let o=i.config.rate||i.config.schedule;if(o){let a=On(o);if(a){let l=await En(i.name);y.jobs.set(i.name,{job:i,lastRun:l,nextRun:An(a,y.config.timezone),isRunning:!1}),j.info(`Registered scheduled job: ${i.name} (${a})`)}else j.warn(`Invalid schedule for job ${i.name}: ${o}`)}}if(y.jobs.size===0){j.info("No scheduled jobs found");return}j.info(`Scheduler started with ${y.jobs.size} job(s)`),process.on("SIGINT",()=>mt()),process.on("SIGTERM",()=>mt());let n=!1,r=()=>{if(y.isShuttingDown)return;let i=y.config.checkInterval,o=i-Date.now()%i;y.checkInterval=setTimeout(()=>{if(!y.isShuttingDown&&!n)n=!0,Dn().catch((a)=>j.error("Scheduler check failed:",a)).finally(()=>{n=!1});r()},o),y.checkInterval?.unref?.()};r(),await Dn()}async function Dn(){for(let[e,t]of y.jobs){let n=t.job.config.rate||t.job.config.schedule;if(!n)continue;let r=On(n);if(!r)continue;if(ci(r,t.lastRun,y.config.timezone)){if((y.config.preventOverlapping||t.job.config.withoutOverlapping)&&await Tn(e)){j.debug(`Skipping ${e}: previous execution still running`);continue}if(t.isRunning){j.debug(`Skipping ${e}: a dispatch for it is already in flight`);continue}try{t.isRunning=!0,t.lastRun=new Date,t.nextRun=An(r,y.config.timezone),await Pn(e,t.lastRun),j.info(`Dispatching scheduled job: ${e}`),await V("job:added",{jobId:`scheduled-${e}-${Date.now()}`,queueName:t.job.config.queue||"default",jobName:e}),await Ye(e,{queue:t.job.config.queue||"default",payload:{},maxTries:t.job.config.tries||3,timeout:t.job.config.timeout||60}),t.isRunning=!1,j.info(`Scheduled job ${e} dispatched to queue`)}catch(o){t.isRunning=!1,j.error(`Failed to dispatch scheduled job ${e}:`,o)}}}}async function mt(){if(!y.isRunning)return;if(j.info("Stopping scheduler..."),y.isShuttingDown=!0,y.checkInterval)clearTimeout(y.checkInterval),y.checkInterval=null;y.isRunning=!1,y.jobs.clear(),j.info("Scheduler stopped")}function li(){return{isRunning:y.isRunning,jobCount:y.jobs.size,jobs:Array.from(y.jobs.entries()).map(([e,t])=>({name:e,schedule:t.job.config.rate||t.job.config.schedule,lastRun:t.lastRun,nextRun:t.nextRun,isRunning:t.isRunning}))}}function di(){return y.isRunning}function hi(){return new Map(y.jobs)}async function pi(e){let t=y.jobs.get(e);if(!t)throw Error(`Scheduled job "${e}" not found`);j.info(`Manually triggering scheduled job: ${e}`),await Ye(e,{queue:t.job.config.queue||"default",payload:{},maxTries:t.job.config.tries||3,timeout:t.job.config.timeout||60})}C();C();import{log as fi}from"@stacksjs/logging";function Fn(e,t=Math.floor(Date.now()/1000)){if(e.reserved_at)return"processing";let n=typeof e.available_at==="number"?e.available_at:typeof e.available_at==="string"&&e.available_at.trim()?Number(e.available_at):Number.NaN;if(Number.isFinite(n)&&n>t)return"delayed";return"pending"}var mi={maxPendingWarning:1000,maxPendingCritical:5000,maxFailedWarning:10,maxFailedCritical:100,maxJobAgeWarning:3600,maxJobAgeCritical:86400,maxErrorRateWarning:0.1,maxErrorRateCritical:0.5};async function bt(e={}){let t={...mi,...e},n=[],r=new Date,i=Math.floor(r.getTime()/1000);try{let{db:o}=await import("@stacksjs/database"),a=await o.selectFrom("jobs").selectAll().execute(),l=await o.selectFrom("failed_jobs").selectAll().execute(),s=new Map;for(let m of a){let v=m.queue||"default";if(!s.has(v))s.set(v,{pending:0,processing:0,delayed:0});let S=s.get(v),P=Fn(m,i);if(P==="processing")S.processing++;else if(P==="delayed")S.delayed++;else if(S.pending++,m.created_at){let Kn=typeof m.created_at==="number"?m.created_at:Math.floor(new Date(m.created_at).getTime()/1000),kt=i-Kn;if(!S.oldestAge||kt>S.oldestAge)S.oldestAge=kt}}let u=new Map;for(let m of l){let v=m.queue||"default";u.set(v,(u.get(v)||0)+1)}let c=[],d=new Set([...s.keys(),...u.keys()]),w=t.queues?[...d].filter((m)=>t.queues.includes(m)):[...d],f=0,g=0,p=0,x=0;for(let m of w){let v=s.get(m)||{pending:0,processing:0,delayed:0},S=u.get(m)||0;f+=v.pending,g+=v.processing,p+=v.delayed,x+=S;let P="healthy";if(v.pending>=t.maxPendingCritical)P="unhealthy",n.push({level:"critical",message:`Queue "${m}" has ${v.pending} pending jobs (threshold: ${t.maxPendingCritical})`,queue:m,timestamp:r.toISOString()});else if(v.pending>=t.maxPendingWarning)P="degraded",n.push({level:"warning",message:`Queue "${m}" has ${v.pending} pending jobs (threshold: ${t.maxPendingWarning})`,queue:m,timestamp:r.toISOString()});if(S>=t.maxFailedCritical)P="unhealthy",n.push({level:"critical",message:`Queue "${m}" has ${S} failed jobs (threshold: ${t.maxFailedCritical})`,queue:m,timestamp:r.toISOString()});else if(S>=t.maxFailedWarning){if(P==="healthy")P="degraded";n.push({level:"warning",message:`Queue "${m}" has ${S} failed jobs (threshold: ${t.maxFailedWarning})`,queue:m,timestamp:r.toISOString()})}if(v.oldestAge){if(v.oldestAge>=t.maxJobAgeCritical)P="unhealthy",n.push({level:"critical",message:`Queue "${m}" has a job waiting for ${Math.floor(v.oldestAge/3600)} hours`,queue:m,timestamp:r.toISOString()});else if(v.oldestAge>=t.maxJobAgeWarning){if(P==="healthy")P="degraded";n.push({level:"warning",message:`Queue "${m}" has a job waiting for ${Math.floor(v.oldestAge/60)} minutes`,queue:m,timestamp:r.toISOString()})}}c.push({name:m,status:P,pending:v.pending,processing:v.processing,delayed:v.delayed,failed:S,oldestJobAge:v.oldestAge})}let vt=De().getMetrics(),Wn=vt.throughputPerMinute,zn=vt.averageDuration,_t=f+g+p+x,le=_t>0?x/_t:0,ee="healthy";if(c.some((m)=>m.status==="unhealthy"))ee="unhealthy";else if(c.some((m)=>m.status==="degraded"))ee="degraded";if(le>=t.maxErrorRateCritical)ee="unhealthy",n.push({level:"critical",message:`Overall error rate is ${(le*100).toFixed(1)}% (threshold: ${t.maxErrorRateCritical*100}%)`,timestamp:r.toISOString()});else if(le>=t.maxErrorRateWarning){if(ee==="healthy")ee="degraded";n.push({level:"warning",message:`Overall error rate is ${(le*100).toFixed(1)}% (threshold: ${t.maxErrorRateWarning*100}%)`,timestamp:r.toISOString()})}let Vn=ge().getAll().map((m)=>({id:m.id,status:m.status,queue:m.queue,processedCount:m.processedCount,failedCount:m.failedCount,lastActivityAt:m.lastActivityAt}));return{status:ee,timestamp:r.toISOString(),queues:c,workers:Vn,metrics:{totalPending:f,totalProcessing:g,totalDelayed:p,totalFailed:x,throughputPerMinute:Wn,averageProcessingTime:zn,errorRate:le},alerts:n}}catch(o){fi.error("Failed to perform queue health check:",o);let l=ge().getAll().map((s)=>({id:s.id,status:s.status,queue:s.queue,processedCount:s.processedCount,failedCount:s.failedCount,lastActivityAt:s.lastActivityAt}));return{status:"unhealthy",timestamp:r.toISOString(),queues:[],workers:l,metrics:{totalPending:0,totalProcessing:0,totalDelayed:0,totalFailed:0,throughputPerMinute:0,averageProcessingTime:0,errorRate:0},alerts:[{level:"critical",message:`Health check failed: ${o.message}`,timestamp:r.toISOString()}]}}}function bi(e={}){return async(t)=>{let n=await bt(e),r=n.status==="healthy"?200:n.status==="degraded"?207:503;return new Response(JSON.stringify(n,null,2),{status:r,headers:{"Content-Type":"application/json","Cache-Control":"no-store"}})}}async function gi(e={}){return(await bt(e)).status==="healthy"}import{log as q}from"@stacksjs/logging";var wi={"&":"&","<":"<",">":">",'"':""","'":"'"};function _e(e){return String(e).replace(/[&<>"']/g,(t)=>wi[t]??t)}class gt{config;notificationCount=0;lastResetTime=Date.now();pendingBatch=[];batchTimeout=null;activeFlush=null;constructor(e){this.config=e}async notify(e){if(this.config.filter&&!this.config.filter(e))return;if(this.config.rateLimit){let t=Date.now();if(t-this.lastResetTime>3600000)this.notificationCount=0,this.lastResetTime=t;if(this.notificationCount>=this.config.rateLimit){q.debug("Rate limit reached for failed job notifications");return}}if(this.config.batch){if(this.pendingBatch.push(e),!this.batchTimeout)this.batchTimeout=setTimeout(()=>{this.batchTimeout=null,this.activeFlush=this.flushBatch().catch((t)=>q.error("Failed to flush notification batch:",t)).finally(()=>{this.activeFlush=null})},this.config.batchInterval||60000),this.batchTimeout.unref?.();return}await this.sendNotifications([e])}async flushBatch(){if(this.pendingBatch.length===0)return;let e=[...this.pendingBatch];if(this.pendingBatch=[],this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;await this.sendNotifications(e)}async shutdown(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.activeFlush)await this.activeFlush.catch(()=>{});await this.flushBatch().catch((e)=>q.error("Failed to flush notification batch on shutdown:",e))}async sendNotifications(e){let t=[];for(let n of this.config.channels)switch(n){case"email":if(this.config.email)t.push(this.sendEmail(e));break;case"slack":if(this.config.slack)t.push(this.sendSlack(e));break;case"discord":if(this.config.discord)t.push(this.sendDiscord(e));break;case"webhook":if(this.config.webhook)t.push(this.sendWebhook(e));break;case"log":this.logFailures(e);break}try{await Promise.all(t)}catch(n){q.error("Failed to send job failure notifications:",n)}finally{this.notificationCount+=e.length}}async sendEmail(e){let t=this.config.email,n=t.subject||`[Queue] ${e.length} Job(s) Failed`,r=this.formatEmailBody(e);try{let{mail:i}=await import("@stacksjs/email");await i.send({to:Array.isArray(t.to)?t.to:[t.to],from:t.from,subject:n,html:r})}catch(i){q.error("Failed to send email notification:",i)}}formatEmailBody(e){let t=e.map((n)=>`
|
|
2
|
+
var{defineProperty:Ie,getOwnPropertyNames:Gn,getOwnPropertyDescriptor:Yn}=Object,Xn=Object.prototype.hasOwnProperty;function Zn(e){return this[e]}var er=(e)=>{var t=(xt??=new WeakMap).get(e),n;if(t)return t;if(t=Ie({},"__esModule",{value:!0}),e&&typeof e==="object"||typeof e==="function"){for(var r of Gn(e))if(!Xn.call(t,r))Ie(t,r,{get:Zn.bind(e,r),enumerable:!(n=Yn(e,r))||n.enumerable})}return xt.set(e,t),t},xt;var tr=(e)=>e;function nr(e,t){this[e]=tr.bind(null,t)}var M=(e,t)=>{for(var n in t)Ie(e,n,{get:t[n],enumerable:!0,configurable:!0,set:nr.bind(t,n)})};var D=(e,t)=>()=>(e&&(t=e(e=0)),t);var h=import.meta.require;function N(e,t,n,r){return{jobName:e,payload:t,options:n,envelopeVersion:1,dispatchedAt:new Date().toISOString(),...r?{traceId:r}:{}}}function F(e){try{return JSON.stringify(e)}catch(t){throw ir(e,t)}}function de(e){F(e)}function ir(e,t){if(or(t)){let i=null;try{i=Qe(e,"",new Set)}catch{}let o=i?`\`${i.path}\` is a BigInt (${i.value}n)`:"it holds a BigInt";return Error(`[queue] Job "${e.jobName}" was not dispatched: ${o}, and job envelopes travel as JSON, which cannot represent one. Convert it where you dispatch - \`String(value)\` keeps every digit, \`Number(value)\` is exact below 2^53 - and convert it back inside the job handler. See the JSON round-trip contract in @stacksjs/queue's src/envelope.ts.`,{cause:t})}if(ar(t))return Error(`[queue] Job "${e.jobName}" was not dispatched: its payload contains a circular reference, which JSON cannot represent. Send the ids of the things the payload points at rather than the objects themselves - that also keeps the job replayable once those objects have changed. See the JSON round-trip contract in @stacksjs/queue's src/envelope.ts.`,{cause:t});let r=t instanceof Error?t.message:String(t);return Error(`[queue] Job "${e.jobName}" was not dispatched: serializing its payload threw (${r}). JSON rejects only two things on its own and this was neither, so the throw came from the payload - a \`toJSON()\` or a property getter. See the JSON round-trip contract in @stacksjs/queue's src/envelope.ts.`,{cause:t})}function or(e){return e instanceof TypeError&&/bigint/i.test(e.message)}function ar(e){return e instanceof TypeError&&/circular|cyclic/i.test(e.message)}function Qe(e,t,n){if(typeof e==="bigint")return{path:t||"envelope",value:e};if(!e||typeof e!=="object")return null;if(n.has(e))return null;if(n.add(e),Array.isArray(e)){for(let r=0;r<e.length;r++){let i=Qe(e[r],`${t}[${r}]`,n);if(i)return i}return null}for(let[r,i]of Object.entries(e)){let o=Qe(i,t?`${t}.${r}`:r,n);if(o)return o}return null}function Jt(e,t){if(Be.has(e))return;Be.add(e),console.warn(t)}function sr(){Be.clear()}function xe(e){let t;if(typeof e==="string")try{t=JSON.parse(e)}catch{return{ok:!1,reason:"malformed",detail:"not valid JSON"}}else if(e&&typeof e==="object")t=e;else return{ok:!1,reason:"malformed",detail:`expected string or object, got ${typeof e}`};if(t.envelopeVersion===1){if(typeof t.jobName!=="string")return{ok:!1,reason:"missing-job-name"};return{ok:!0,envelope:{jobName:t.jobName,payload:t.payload,options:t.options??void 0,envelopeVersion:1,dispatchedAt:typeof t.dispatchedAt==="string"?t.dispatchedAt:new Date(0).toISOString(),...typeof t.traceId==="string"&&t.traceId?{traceId:t.traceId}:{}},source:"v1"}}if(typeof t.envelopeVersion==="number"&&t.envelopeVersion>1)return{ok:!1,reason:"unknown-version",detail:`envelopeVersion=${t.envelopeVersion}, this worker speaks v1`};if(typeof t.jobName==="string")return Jt("v0-implicit","[queue/envelope] Processing pre-#1884 job envelope without envelopeVersion. In-flight jobs from before the upgrade will continue to work; new dispatches use the v1 shape automatically."),{ok:!0,envelope:{jobName:t.jobName,payload:t.payload,options:t.options??void 0,envelopeVersion:1,dispatchedAt:typeof t.dispatchedAt==="string"?t.dispatchedAt:new Date(0).toISOString(),...typeof t.traceId==="string"&&t.traceId?{traceId:t.traceId}:{}},source:"v0-implicit"};if(typeof t.job==="string")return Jt("laravel-legacy","[queue/envelope] Processing Laravel-legacy job envelope (`{ job, data }` shape). Will continue to process but the queue table contains migration-era rows - consider flushing once they drain."),{ok:!0,envelope:{jobName:t.job.replace(/^App\\+Jobs\\+/,"").replace(/^.*\\/,""),payload:t.data,options:void 0,envelopeVersion:1,dispatchedAt:new Date(0).toISOString()},source:"laravel-legacy"};return{ok:!1,reason:"missing-job-name"}}var rr=1,Be;var te=D(()=>{Be=new Set});var Ue={};M(Ue,{QueueTester:()=>Je,createQueueTester:()=>Et,expectJobToFail:()=>Rt,fake:()=>He,getFakeQueue:()=>Pt,isFaked:()=>St,restore:()=>Le,runJob:()=>Tt});class jt{dispatchedJobs=[];pushedJobs=[];processedJobs=[];failedJobs=[];dispatch(e,t,n={}){this.dispatchedJobs.push({name:e,data:t,options:n,dispatchedAt:new Date,queue:n.queue||"default"})}push(e,t,n={}){this.pushedJobs.push({name:e,data:t,options:n,dispatchedAt:new Date,queue:n.queue||"default"})}dispatched(e){if(e)return this.dispatchedJobs.filter((t)=>t.name===e);return[...this.dispatchedJobs]}pushed(e){if(e)return this.pushedJobs.filter((t)=>t.name===e);return[...this.pushedJobs]}assertDispatched(e,t){let n=this.dispatched(e);if(n.length===0)throw Error(`Expected job "${e}" to be dispatched, but it was not.`);if(t){if(n.filter(t).length===0)throw Error(`Expected job "${e}" to be dispatched matching the callback, but no matching jobs were found.`)}}assertNotDispatched(e){let t=this.dispatched(e);if(t.length>0)throw Error(`Expected job "${e}" to not be dispatched, but it was dispatched ${t.length} time(s).`)}assertDispatchedTimes(e,t){let n=this.dispatched(e);if(n.length!==t)throw Error(`Expected job "${e}" to be dispatched ${t} time(s), but it was dispatched ${n.length} time(s).`)}assertNothingDispatched(){if(this.dispatchedJobs.length>0){let e=[...new Set(this.dispatchedJobs.map((t)=>t.name))].join(", ");throw Error(`Expected no jobs to be dispatched, but found: ${e}`)}}assertPushed(e,t){let n=this.pushed(e);if(n.length===0)throw Error(`Expected job "${e}" to be pushed, but it was not.`);if(t){if(n.filter(t).length===0)throw Error(`Expected job "${e}" to be pushed matching the callback, but no matching jobs were found.`)}}assertPushedWithDelay(e,t){if(this.pushed(e).filter((i)=>i.options?.delay===t).length===0)throw Error(`Expected job "${e}" to be pushed with delay ${t}ms, but no matching jobs were found.`)}assertPushedOn(e,t){if(this.pushed(t).filter((r)=>r.queue===e).length===0)throw Error(`Expected job "${t}" to be pushed on queue "${e}", but it was not.`)}async processJob(e,t){let n=this.dispatchedJobs.find((r)=>r.name===e);if(!n)throw Error(`No dispatched job found with name "${e}"`);try{await t(n.data),this.processedJobs.push(n)}catch(r){throw this.failedJobs.push({job:n,error:r}),r}}processed(e){if(e)return this.processedJobs.filter((t)=>t.name===e);return[...this.processedJobs]}failed(e){if(e)return this.failedJobs.filter((t)=>t.job.name===e);return[...this.failedJobs]}reset(){this.dispatchedJobs=[],this.pushedJobs=[],this.processedJobs=[],this.failedJobs=[]}}function He(){return he=new jt,he}function Pt(){return he}function St(){return he!==null}function Le(){he=null}class Je{queue;constructor(){this.queue=He()}dispatch(e,t,n={}){return this.queue.dispatch(e,t,n),this}push(e,t,n={}){return this.queue.push(e,t,n),this}assertDispatched(e,t){return this.queue.assertDispatched(e,t),this}assertNotDispatched(e){return this.queue.assertNotDispatched(e),this}assertDispatchedTimes(e,t){return this.queue.assertDispatchedTimes(e,t),this}assertNothingDispatched(){return this.queue.assertNothingDispatched(),this}dispatched(e){return this.queue.dispatched(e)}reset(){return this.queue.reset(),this}cleanup(){Le()}}function Et(){return new Je}async function Tt(e,t){return await e.handle(t)}async function Rt(e,t,n){try{throw await e.handle(t),Error("Expected job to fail, but it succeeded")}catch(r){if(r.message==="Expected job to fail, but it succeeded")throw r;if(n){let i=r.message;if(typeof n==="string"){if(!i.includes(n))throw Error(`Expected error to contain "${n}", got "${i}"`)}else if(!n.test(i))throw Error(`Expected error to match ${n}, got "${i}"`)}return r}}var he=null;var pe={};M(pe,{QueueManager:()=>mr,RedisJob:()=>qt,RedisQueue:()=>ne,StacksQueueManager:()=>Ct,batch:()=>ur,chain:()=>lr,createRedisDispatcher:()=>gr,dispatch:()=>dr,dispatchAfter:()=>hr,dispatchSync:()=>pr,getQueueManager:()=>fr,setQueueManager:()=>br});import{Queue as cr,batch as ur,chain as lr,dispatch as dr,dispatchAfter as hr,dispatchSync as pr,getQueueManager as fr,QueueManager as mr,setQueueManager as br}from"@stacksjs/bun-queue";import{log as We}from"@stacksjs/logging";class ne{queue;config;isProcessing=!1;constructor(e,t){this.config=t,this.queue=new cr(e,{driver:"redis",prefix:t.prefix,redis:t.redis?{url:t.redis.url||this.buildRedisUrl(t.redis)}:void 0,defaultJobOptions:t.defaultJobOptions?{delay:t.defaultJobOptions.delay,attempts:t.defaultJobOptions.attempts,backoff:t.defaultJobOptions.backoff,removeOnComplete:t.defaultJobOptions.removeOnComplete,removeOnFail:t.defaultJobOptions.removeOnFail,priority:t.defaultJobOptions.priority,lifo:t.defaultJobOptions.lifo,timeout:t.defaultJobOptions.timeout,jobId:t.defaultJobOptions.jobId,dependsOn:t.defaultJobOptions.dependsOn,keepJobs:t.defaultJobOptions.keepJobs,deadLetter:t.defaultJobOptions.deadLetter}:void 0,limiter:t.limiter,metrics:t.metrics,stalledJobCheckInterval:t.stalledJobCheckInterval,maxStalledJobRetries:t.maxStalledJobRetries,distributedLock:t.distributedLock,defaultDeadLetterOptions:t.defaultDeadLetterOptions,horizontalScaling:t.horizontalScaling,logLevel:t.logLevel}),We.debug(`Redis queue "${e}" initialized`)}buildRedisUrl(e){let t=e.host||"localhost",n=e.port||6379,r=e.password?`:${encodeURIComponent(e.password)}@`:"",i=Number.isFinite(e.db)&&e.db>=0?e.db:0;return`redis://${r}${t}:${n}/${i}`}async add(e,t){let n={delay:t?.delay?t.delay*1000:void 0,attempts:t?.maxTries,priority:t?.priority,timeout:t?.timeout?t.timeout*1000:void 0,backoff:Array.isArray(t?.backoff)?t.backoff.map((r)=>(Number(r)||1)*1000):typeof t?.backoff==="number"?{type:"fixed",delay:(t.backoff||1)*1000}:void 0};return this.queue.add(e,n)}process(e,t){if(this.isProcessing){We.warn("Queue is already processing");return}this.isProcessing=!0,this.queue.process(e,t),We.info(`Started processing queue with concurrency ${e}`)}async getJob(e){return this.queue.getJob(e)}async getJobs(e){return this.queue.getJobs(e)}async getJobCounts(){return this.queue.getJobCounts()}async removeJob(e){return this.queue.removeJob(e)}async pause(){return this.queue.pause()}async resume(){return this.queue.resume()}async empty(){return this.queue.empty()}async close(){return this.isProcessing=!1,this.queue.close()}async getMetrics(){return this.queue.getMetrics()}async ping(){return this.queue.ping()}async scheduleCron(e){return this.queue.scheduleCron({cronExpression:e.cron,data:e.data,timezone:e.tz,jobId:e.name})}async unscheduleCron(e){return this.queue.unscheduleCron(e)}async getDeadLetterJobs(){return this.queue.getDeadLetterJobs()}async republishDeadLetterJob(e){return this.queue.republishDeadLetterJob(e)}async clearDeadLetterQueue(){return this.queue.clearDeadLetterQueue()}async bulkRemove(e){return this.queue.bulkRemove(e)}async getClusterInfo(){return this.queue.getClusterInfo()}isLeader(){return this.queue.isLeader()}getQueue(){return this.queue}on(e,t){this.queue.events.on(e,t)}}class Ct{config;queues=new Map;defaultConnection="default";constructor(e){this.config=e;for(let[t,n]of Object.entries(e))if(n.driver==="redis")this.queues.set(t,new ne(t,n))}queue(e){let t=e||this.defaultConnection,n=this.queues.get(t);if(!n){let r=this.config[t];if(!r)throw Error(`Queue "${t}" not configured`);n=new ne(t,r),this.queues.set(t,n)}return n}setDefaultConnection(e){this.defaultConnection=e}async closeAll(){let e=Array.from(this.queues.values()).map((t)=>t.close());await Promise.all(e),this.queues.clear()}}function gr(e,t){let n=new ne(e,t);return async(r,i)=>n.add(r,i)}class qt{data;options={};queue;constructor(e,t,n){this.data=n;this.queue=new ne(e,t)}async dispatch(){await this.queue.add(this.data,this.options)}async dispatchNow(){await this.queue.add(this.data,{...this.options,immediate:!0})}delay(e){return this.options.delay=e,this}afterResponse(){return this.options.afterResponse=!0,this}chain(e){return this.options.chainedJobs=e,this}onQueue(e){return this.options.queue=e,this}priority(e){return this.options.priority=e,this}tries(e){return this.options.maxTries=e,this}timeout(e){return this.options.timeout=e,this}backoff(e){return this.options.backoff=e,this}}var fe=()=>{};function _(e){let t=e,n=t?.errno;if(typeof n==="string"&&n.toUpperCase()==="42P01")return!0;if(typeof n==="number"&&n===1146)return!0;if((typeof t?.code==="string"?t.code.toUpperCase():"")==="42P01")return!0;let i=t?.message??"";return i.includes("no such table")||i.includes("doesn't exist")||i.includes("does not exist")}var It={};M(It,{listDeadLetterJobs:()=>Ot,moveToDeadLetter:()=>Pe,purgeDeadLetterJobs:()=>Ft,retryDeadLetterJob:()=>At});import{db as re}from"@stacksjs/database";function je(){if(Nt)return;Nt=!0,console.warn("[queue/dlq] dead_letter_jobs table missing - DLQ disabled. Run migrations to enable poison-message isolation.")}async function Pe(e,t,n=1){let r=new Date().toISOString().slice(0,19).replace("T"," ");try{return await re.insertInto("dead_letter_jobs").values({uuid:e.uuid??crypto.randomUUID(),connection:e.connection??"database",queue:e.queue??"default",payload:e.payload??"{}",exception:e.exception??"unknown",reason:t,total_failures:n,first_failed_at:e.failed_at??r,last_failed_at:r,dead_lettered_at:r}).execute(),!0}catch(i){if(_(i))return je(),!1;throw i}}async function Ot(e={}){try{let t=re.selectFrom("dead_letter_jobs").selectAll();if(e.queue)t=t.where("queue","=",e.queue);if(e.reason)t=t.where("reason","=",e.reason);if(e.sinceCutoffMs){let r=new Date(e.sinceCutoffMs).toISOString().slice(0,19).replace("T"," ");t=t.where("dead_lettered_at",">=",r)}if(e.limit&&e.limit>0)t=t.limit(e.limit);return await t.execute()??[]}catch(t){if(_(t))return je(),[];throw t}}async function At(e){try{let t=await re.selectFrom("dead_letter_jobs").where("id","=",e).selectAll().executeTakeFirst();if(!t)return!1;let n=Math.floor(Date.now()/1000);return await re.insertInto("jobs").values({queue:t.queue,payload:t.payload,attempts:0,reserved_at:null,available_at:n,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),await re.deleteFrom("dead_letter_jobs").where("id","=",e).execute(),!0}catch(t){if(_(t))return je(),!1;throw t}}async function Ft(e=30){try{let t=new Date(Date.now()-e*24*60*60*1000).toISOString().slice(0,19).replace("T"," "),n=await re.deleteFrom("dead_letter_jobs").where("dead_lettered_at","<",t).execute();return Number(n?.numDeletedRows??n?.[0]?.numDeletedRows??n?.affectedRows??0)}catch(t){if(_(t))return je(),0;throw t}}var Nt=!1;var Se=()=>{};function ze(){if(Qt)return;Qt=!0,console.warn("[queue/idempotency] job_idempotency table missing - idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.")}async function yr(e){try{let{db:t}=await import("@stacksjs/database"),n=await t.selectFrom("job_idempotency").where("idempotency_key","=",e).select(["idempotency_key"]).executeTakeFirst();return Boolean(n)}catch(t){if(_(t))return ze(),!1;throw t}}async function vr(e,t,n){try{let{db:r}=await import("@stacksjs/database");await r.insertInto("job_idempotency").values({idempotency_key:e,job_name:t,queue:n??"default",dispatched_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch(r){if(_(r)){ze();return}let i=r?.message??"";if(i.includes("UNIQUE constraint")||i.includes("Duplicate entry"))return;throw r}}async function Ve(e,t,n){try{let{db:r}=await import("@stacksjs/database");return await r.insertInto("job_idempotency").values({idempotency_key:e,job_name:t,queue:n??"default",dispatched_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),"claimed"}catch(r){if(_(r))return ze(),"unenforced";let i=r?.message??"";if(i.includes("UNIQUE constraint")||i.includes("Duplicate entry"))return"duplicate";throw r}}async function Ke(e){try{let{db:t}=await import("@stacksjs/database");await t.deleteFrom("job_idempotency").where("idempotency_key","=",e).execute()}catch{}}var Qt=!1;var Ge=()=>{};var Wt={};M(Wt,{hashPayload:()=>be,isQuarantined:()=>Ee,listQuarantined:()=>Ut,quarantineJob:()=>Ht,recordFailureForPoison:()=>Mt,unquarantineJob:()=>Lt});import{createHash as _r}from"crypto";import{db as T}from"@stacksjs/database";function me(){if(Bt)return;Bt=!0,console.warn("[queue/poison] job_quarantine table missing - poison detection disabled. Run migrations to enable.")}function be(e){let t;try{t=typeof e==="string"?e:JSON.stringify(e??null)}catch{t=String(e)}return _r("sha256").update(t).digest("hex").slice(0,32)}async function Mt(e,t,n={}){let r=n.maxFailures??5,i=n.windowMinutes??60,o=be(t),a=new Date,l=a.toISOString().slice(0,19).replace("T"," ");try{let s=await T.selectFrom("job_quarantine").where("job_name","=",e).where("payload_hash","=",o).selectAll().executeTakeFirst();if(!s)return await T.insertInto("job_quarantine").values({job_name:e,payload_hash:o,failure_count:1,window_start:l,quarantined_at:null}).execute(),!1;if(s.quarantined_at)return!0;let u=Date.parse(s.window_start.replace(" ","T")+"Z"),c=a.getTime()-u,d=i*60*1000;if(Number.isFinite(u)&&c>d)return await T.updateTable("job_quarantine").set({failure_count:1,window_start:l}).where("id","=",s.id).execute(),!1;let w=s.failure_count+1;if(w>=r)return await T.updateTable("job_quarantine").set({failure_count:w,quarantined_at:l}).where("id","=",s.id).execute(),!0;return await T.updateTable("job_quarantine").set({failure_count:w}).where("id","=",s.id).execute(),!1}catch(s){if(_(s))return me(),!1;throw s}}async function Ee(e,t){let n=be(t);try{let r=await T.selectFrom("job_quarantine").where("job_name","=",e).where("payload_hash","in",[n,"*"]).whereNotNull("quarantined_at").select(["id"]).executeTakeFirst();return Boolean(r)}catch(r){if(_(r))return me(),!1;throw r}}async function Ht(e,t){let n=t===void 0?"*":be(t),r=new Date().toISOString().slice(0,19).replace("T"," ");try{let i=await T.selectFrom("job_quarantine").where("job_name","=",e).where("payload_hash","=",n).select(["id"]).executeTakeFirst();if(i)await T.updateTable("job_quarantine").set({quarantined_at:r}).where("id","=",i.id).execute();else await T.insertInto("job_quarantine").values({job_name:e,payload_hash:n,failure_count:0,window_start:r,quarantined_at:r}).execute()}catch(i){if(_(i)){me();return}throw i}}async function Lt(e){try{await T.deleteFrom("job_quarantine").where("job_name","=",e).execute()}catch(t){if(_(t)){me();return}throw t}}async function Ut(){try{return await T.selectFrom("job_quarantine").whereNotNull("quarantined_at").selectAll().execute()??[]}catch(e){if(_(e))return me(),[];throw e}}var Bt=!1;var Te=()=>{};function z(e){let t=e?.numUpdatedRows;if(t===null||t===void 0)return 0;if(typeof t==="object")return Number(t.changes??0);return Number(t)}function kr(e,t){let n=N(e,t.payload||{},{queue:t.queue,tries:t.maxTries,timeout:t.timeout,backoff:Array.isArray(t.backoff)?t.backoff:void 0});return{queue:t.queue||"default",payload:F(n),attempts:0,available_at:xr(t.delay||0),created_at:new Date().toISOString().slice(0,19).replace("T"," ")}}async function Ye(e,t){let n=kr(e,t),{db:r}=await import("@stacksjs/database");await r.insertInto("jobs").values(n).execute()}function xr(e){let t=Date.now();return Math.floor(t/1000+e)}var Re=D(()=>{te()});var Q={};M(Q,{OnQueueEvent:()=>Kt,QueueEvents:()=>Ce,QueueMetrics:()=>qe,emitQueueEvent:()=>V,getGlobalMetrics:()=>De,getQueueEvents:()=>ie,getWorkerTracker:()=>ge,onQueueEvent:()=>zt,withEvents:()=>Vt});import{log as I}from"@stacksjs/logging";class Ce{handlers=new Map;wildcardHandlers=new Set;listenerSubscriptions=new WeakMap;reclaim=new FinalizationRegistry((e)=>{e()});on(e,t){if(!this.handlers.has(e))this.handlers.set(e,new Set);return this.handlers.get(e).add(t),()=>{this.handlers.get(e)?.delete(t)}}subscribeListener(e,t,n){let r=new WeakRef(e),i=(s)=>{let u=r.deref();if(u===void 0){o();return}return n.call(u,s)},o=this.on(t,i),a=()=>{o();let s=r.deref();if(s===void 0)return;let u=this.listenerSubscriptions.get(s);if(!u)return;if(u.delete(a),u.size===0)this.listenerSubscriptions.delete(s),this.reclaim.unregister(s)},l=this.listenerSubscriptions.get(e);if(!l)l=new Set,this.listenerSubscriptions.set(e,l);return l.add(a),this.reclaim.register(e,o,e),a}unsubscribeListener(e){let t=this.listenerSubscriptions.get(e);if(!t)return 0;let n=Array.from(t);for(let r of n)r();return this.listenerSubscriptions.delete(e),this.reclaim.unregister(e),n.length}listenerCount(e){if(e==="*")return this.wildcardHandlers.size;return this.handlers.get(e)?.size??0}onAny(e){return this.wildcardHandlers.add(e),()=>{this.wildcardHandlers.delete(e)}}once(e,t){let n=async(r)=>{this.handlers.get(e)?.delete(n),await t(r)};return this.on(e,n)}async emit(e,t){let n={...t,timestamp:Date.now()};this.logEvent(e,n);let r=this.handlers.get(e);if(r)for(let i of r)try{await i(n)}catch(o){I.error(`Error in queue event handler for ${e}:`,o)}for(let i of this.wildcardHandlers)try{await i(e,n)}catch(o){I.error("Error in wildcard queue event handler:",o)}}logEvent(e,t){let n=t.jobId?`[${t.jobId}]`:"",r=t.queueName?`on ${t.queueName}`:"";switch(e){case"job:added":I.debug(`Job added ${n} ${r}`);break;case"job:processing":I.debug(`Job processing ${n} ${r}`);break;case"job:completed":I.info(`Job completed ${n} ${r} in ${t.duration}ms`);break;case"job:failed":I.error(`Job failed ${n} ${r}:`,t.error);break;case"job:retrying":I.warn(`Job retrying ${n} ${r} (attempt ${t.attemptsMade})`);break;case"job:stalled":I.warn(`Job stalled ${n} ${r}`);break;case"queue:error":I.error(`Queue error ${r}:`,t.error);break}}off(e){this.handlers.delete(e)}removeAllListeners(){this.handlers.clear(),this.wildcardHandlers.clear(),this.listenerSubscriptions=new WeakMap}}function ie(){if(!Xe)Xe=new Ce;return Xe}function zt(e,t){let n=ie();if(e==="*")return n.onAny(t);return n.on(e,t)}function V(e,t){return ie().emit(e,t)}function Vt(e,t){return async(...n)=>{let r=n[0]?.id||"unknown",i=Date.now();await V("job:processing",{jobId:r,queueName:e,data:n[0]?.data});try{let o=await t(...n);return await V("job:completed",{jobId:r,queueName:e,result:o,duration:Date.now()-i}),o}catch(o){throw await V("job:failed",{jobId:r,queueName:e,error:o,duration:Date.now()-i}),o}}}function Kt(e){return function(t,n){if(typeof n!=="object"||n===null)throw TypeError(`@OnQueueEvent('${e}') requires standard (TC39) decorators. Legacy decorators ('experimentalDecorators: true') give a method decorator no construction-time hook, so the handler cannot be bound to an instance.`);if(n.kind!=="method")throw TypeError(`@OnQueueEvent('${e}') can only decorate a class method, but it was applied to a ${n.kind}${n.name===void 0?"":` ('${String(n.name)}')`}. Move the handler into a method, or call onQueueEvent('${e}', handler) directly.`);if(n.static)throw TypeError(`@OnQueueEvent('${e}') cannot decorate the static method '${String(n.name)}': a static method has no instance, so it would subscribe at class-definition time and stay subscribed for the life of the process, even if the class is never used. Use an instance method, or subscribe explicitly with onQueueEvent('${e}', MyClass.${String(n.name)}).`);return n.addInitializer(function(){ie().subscribeListener(this,e,t)}),t}}class qe{jobCounts={added:0,completed:0,failed:0,processing:0};completions=[];errors=[];unsubscribe=[];constructor(){this.setupListeners()}setupListeners(){let e=ie();this.unsubscribe.push(e.on("job:added",()=>{this.jobCounts.added++}),e.on("job:processing",()=>{this.jobCounts.processing++}),e.on("job:completed",(t)=>{this.jobCounts.completed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1);let n=t.duration||0;if(this.completions.push({timestamp:Date.now(),duration:n}),this.completions.length>1000)this.completions.shift()}),e.on("job:failed",(t)=>{if(this.jobCounts.failed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1),t.error){if(this.errors.push({error:t.error,timestamp:Date.now()}),this.errors.length>100)this.errors.shift()}}))}getThroughputPerMinute(){let e=Date.now()-60000;return this.completions.filter((n)=>n.timestamp>=e).length}getAverageProcessingTime(){let e=Date.now()-60000,t=this.completions.filter((n)=>n.timestamp>=e);if(t.length===0)return 0;return t.reduce((n,r)=>n+r.duration,0)/t.length}getMetrics(){return{counts:{...this.jobCounts},averageDuration:this.getAverageProcessingTime(),recentErrors:[...this.errors],throughputPerMinute:this.getThroughputPerMinute()}}reset(){this.jobCounts={added:0,completed:0,failed:0,processing:0},this.completions=[],this.errors=[]}stop(){this.unsubscribe.forEach((e)=>e()),this.unsubscribe=[]}}function De(){if(!Ze)Ze=new qe;return Ze}class Gt{workers=new Map;register(e,t){this.workers.set(e,{id:e,status:"idle",queue:t,processedCount:0,failedCount:0,lastActivityAt:new Date().toISOString(),startedAt:new Date().toISOString()})}markActive(e){let t=this.workers.get(e);if(t)t.status="active",t.lastActivityAt=new Date().toISOString()}markIdle(e){let t=this.workers.get(e);if(t)t.status="idle",t.lastActivityAt=new Date().toISOString()}recordCompletion(e){let t=this.workers.get(e);if(t)t.processedCount++,t.lastActivityAt=new Date().toISOString()}recordFailure(e){let t=this.workers.get(e);if(t)t.failedCount++,t.lastActivityAt=new Date().toISOString()}unregister(e){let t=this.workers.get(e);if(t)t.status="stopped"}getAll(){return Array.from(this.workers.values())}clear(){this.workers.clear()}}function ge(){return Jr}var Xe=null,Ze=null,Jr;var R=D(()=>{Jr=new Gt});var U={};M(U,{Batch:()=>et,DispatchedBatch:()=>L,PendingBatch:()=>Ne,batchCounterIncrements:()=>Yt,batchRecordFromHash:()=>it,batchRecordToHash:()=>rn,getBatchCallbacks:()=>we,isBatchCancelled:()=>cn,recordBatchJobCompletion:()=>at,recordBatchJobFailure:()=>st});var{RedisClient:jr}=globalThis.Bun;import{log as k}from"@stacksjs/logging";import{env as Pr}from"@stacksjs/env";function q(){return Pr.QUEUE_DRIVER||"sync"}class Ne{jobs;options={thenCallbacks:[],catchCallbacks:[],finallyCallbacks:[],progressCallbacks:[]};constructor(e){this.jobs=e.map((t)=>("job"in t)?t:{job:t})}name(e){return this.options.name=e,this}onQueue(e){return this.options.queue=e,this}allowFailures(){return this.options.allowFailures=!0,this}then(e){return this.options.thenCallbacks.push(e),this}catch(e){return this.options.catchCallbacks.push(e),this}finally(e){return this.options.finallyCallbacks.push(e),this}progress(e){return this.options.progressCallbacks.push(e),this}thenHandler(e){return this.options.thenHandler=e,this}catchHandler(e){return this.options.catchHandler=e,this}finallyHandler(e){return this.options.finallyHandler=e,this}async dispatch(){let e=crypto.randomUUID(),t=this.jobs.length;if(t===0)throw Error("Cannot dispatch an empty batch");let n=q();await Er({id:e,name:this.options.name||"",total_jobs:t,pending_jobs:t,failed_jobs:0,failed_job_ids:"[]",options:JSON.stringify({queue:this.options.queue,allowFailures:this.options.allowFailures||!1}),cancelled_at:null,created_at:new Date().toISOString().slice(0,19).replace("T"," "),finished_at:null,then_handler:this.options.thenHandler?JSON.stringify(this.options.thenHandler):null,catch_handler:this.options.catchHandler?JSON.stringify(this.options.catchHandler):null,finally_handler:this.options.finallyHandler?JSON.stringify(this.options.finallyHandler):null}),Sr(e,this.options);try{let{emitQueueEvent:r}=await Promise.resolve().then(() => (R(),Q));await r("batch:added",{jobId:e,data:{name:this.options.name,totalJobs:t}})}catch{}for(let r=0;r<this.jobs.length;r++){let i=this.jobs[r];if(!i)continue;let{job:o,payload:a}=i,l={...a,_batchId:e,_batchIndex:r};if(this.options.queue&&!o.queue)o.queue=this.options.queue;if(n==="sync")try{await o.dispatchNow(l),await at(e)}catch(s){await st(e,`${e}:${r}`,s)}else await o.dispatch(l)}return k.info(`[Batch] Dispatched batch "${this.options.name||e}" with ${t} jobs`),new L(e)}getJobs(){return[...this.jobs]}getOptions(){return this.options}}class L{id;constructor(e){this.id=e}async fresh(){return H(this.id)}async getName(){return(await this.fresh())?.name||""}async totalJobs(){return(await this.fresh())?.total_jobs||0}async pendingJobs(){return(await this.fresh())?.pending_jobs||0}async failedJobs(){return(await this.fresh())?.failed_jobs||0}async completedJobs(){let e=await this.fresh();if(!e)return 0;return e.total_jobs-e.pending_jobs}async progress(){let e=await this.fresh();if(!e||e.total_jobs===0)return 0;let t=e.total_jobs-e.pending_jobs;return Math.round(t/e.total_jobs*100)}async finished(){return(await this.fresh())?.finished_at!==null}async cancelled(){return(await this.fresh())?.cancelled_at!==null}async hasFailures(){return((await this.fresh())?.failed_jobs||0)>0}async failedJobIds(){let e=await this.fresh();if(!e)return[];try{return JSON.parse(e.failed_job_ids||"[]")}catch{return[]}}async cancel(){if(q()==="redis")await Ir(this.id);else await Nr(this.id);k.info(`[Batch] Cancelled batch ${this.id}`);let t=we(this.id);if(t)for(let n of t.finallyCallbacks)try{await n(this)}catch(r){k.error(`[Batch] Error in finally callback for batch ${this.id}:`,r)}}async add(e){let t=await this.fresh();if(!t)throw Error(`Batch ${this.id} not found`);if(t.cancelled_at)throw Error(`Batch ${this.id} has been cancelled`);if(t.finished_at)throw Error(`Batch ${this.id} has already finished`);let n=e.map((o)=>("job"in o)?o:{job:o});if(!await Cr(this.id,n.length))throw Error(`Batch ${this.id} was cancelled, finished or deleted before the jobs could be added`);let i=JSON.parse(t.options||"{}");for(let o=0;o<n.length;o++){let a=n[o];if(!a)continue;let{job:l,payload:s}=a,u={...s,_batchId:this.id,_batchIndex:t.total_jobs+o};if(i.queue&&!l.queue)l.queue=i.queue;await l.dispatch(u)}k.info(`[Batch] Added ${n.length} jobs to batch ${this.id}`)}async delete(){await qr(this.id),$e(this.id)}}class et{static create(e){return new Ne(e)}static async find(e){if(!await H(e))return null;return new L(e)}static async all(){return(await Tr()).map((t)=>new L(t.id))}static async prune(e=24){return Dr(e)}}function Sr(e,t){tt.set(e,t)}function we(e){return tt.get(e)}function $e(e){tt.delete(e)}async function Er(e){if(q()==="redis")await Ar(e);else await Xt(e)}async function H(e){if(q()==="redis")return Fr(e);return Zt(e)}async function Tr(){if(q()==="redis")return on();return en()}async function Rr(e,t){if(q()==="redis")await ot(e,t);else await nt(e,t)}async function Cr(e,t){if(t===0)return!0;if(q()==="redis")try{let o=await G(),a=`${K}${e}`;return await o.hincrby(a,"total_jobs",t),await o.hincrby(a,"pending_jobs",t),o.close(),!0}catch{}let{db:n,sql:r}=await import("@stacksjs/database"),i=await n.updateTable("job_batches").set(Yt(r,t)).where("id","=",e).whereNull("cancelled_at").whereNull("finished_at").executeTakeFirst();return z(i)>0}function Yt(e,t){return{total_jobs:e`total_jobs + ${t}`,pending_jobs:e`pending_jobs + ${t}`}}async function qr(e){if(q()==="redis")await sn(e);else await tn(e)}async function Dr(e){if(q()==="redis")return Qr(e);return nn(e)}function $r(e){return!!(e.then_handler||e.catch_handler||e.finally_handler)}async function Xt(e){let{db:t}=await import("@stacksjs/database"),n={id:e.id,name:e.name,total_jobs:e.total_jobs,pending_jobs:e.pending_jobs,failed_jobs:e.failed_jobs,failed_job_ids:e.failed_job_ids,options:e.options,cancelled_at:e.cancelled_at,created_at:e.created_at,finished_at:e.finished_at};if(!$r(e)){await t.insertInto("job_batches").values(n).execute();return}try{await t.insertInto("job_batches").values({...n,then_handler:e.then_handler??null,catch_handler:e.catch_handler??null,finally_handler:e.finally_handler??null}).execute()}catch(r){k.warn(`[Batch] Could not persist terminal handlers for batch ${e.id}: ${r?.message}. The job_batches table predates the then_handler/catch_handler/finally_handler columns; recreate it to enable handlers that survive a worker restart.`),await t.insertInto("job_batches").values(n).execute()}}async function Zt(e){let{db:t}=await import("@stacksjs/database");return await t.selectFrom("job_batches").where("id","=",e).selectAll().executeTakeFirst()||null}async function en(){let{db:e}=await import("@stacksjs/database");return await e.selectFrom("job_batches").selectAll().orderBy("created_at","desc").execute()}async function nt(e,t){let{db:n}=await import("@stacksjs/database");await n.updateTable("job_batches").set(t).where("id","=",e).execute()}async function tn(e){let{db:t}=await import("@stacksjs/database");await t.deleteFrom("job_batches").where("id","=",e).execute()}async function Nr(e){await nt(e,{cancelled_at:new Date().toISOString().slice(0,19).replace("T"," "),finished_at:new Date().toISOString().slice(0,19).replace("T"," ")})}async function nn(e){let{db:t}=await import("@stacksjs/database"),n=new Date(Date.now()-e*60*60*1000).toISOString().slice(0,19).replace("T"," "),r=await t.deleteFrom("job_batches").whereNotNull("finished_at").where("finished_at","<",n).executeTakeFirst();return Number(r?.numDeletedRows??0)}async function Or(){let{queue:e}=await import("@stacksjs/config"),t=e?.connections?.redis?.redis;if(t?.url)return t.url;let n=t?.password?`:${encodeURIComponent(t.password)}@`:"",r=t?.db?`/${t.db}`:"";return`redis://${n}${t?.host||"localhost"}:${t?.port||6379}${r}`}async function G(){let e=new jr(await Or());return await e.connect(),e}function rn(e){return{id:e.id,name:e.name,total_jobs:String(e.total_jobs),pending_jobs:String(e.pending_jobs),failed_jobs:String(e.failed_jobs),failed_job_ids:e.failed_job_ids,options:e.options,cancelled_at:e.cancelled_at||"",created_at:e.created_at,finished_at:e.finished_at||"",then_handler:e.then_handler||"",catch_handler:e.catch_handler||"",finally_handler:e.finally_handler||""}}async function Ar(e){try{let t=await G();await t.hset(`${K}${e.id}`,rn(e)),await t.sadd(rt,e.id),t.close()}catch{await Xt(e)}}function it(e){if(!e?.id)return null;return{id:e.id,name:e.name??"",total_jobs:Number(e.total_jobs),pending_jobs:Number(e.pending_jobs),failed_jobs:Number(e.failed_jobs),failed_job_ids:e.failed_job_ids??"",options:e.options??"",cancelled_at:e.cancelled_at||null,created_at:e.created_at??"",finished_at:e.finished_at||null,then_handler:e.then_handler||null,catch_handler:e.catch_handler||null,finally_handler:e.finally_handler||null}}async function Fr(e){try{let t=await G(),n=`${K}${e}`,r=await t.hgetall(n);return t.close(),it(r)}catch{return Zt(e)}}async function on(){try{let e=await G(),t=await e.smembers(rt),n=[];for(let r of t){let i=it(await e.hgetall(`${K}${r}`));if(i)n.push(i)}return e.close(),n}catch{return en()}}async function ot(e,t){try{let n=await G(),r=`${K}${e}`,i={};for(let[o,a]of Object.entries(t))i[o]=a===null?"":String(a);await n.hset(r,i),n.close()}catch{await nt(e,t)}}async function an(e,t,n){let r=await G();try{let i=`${K}${e}`,o=await r.hincrby(i,"pending_jobs",-1),a=Math.max(o,0);if(o<0)await r.hset(i,{pending_jobs:"0"});if(t)await r.hincrby(i,"failed_jobs",1);let s=!(await r.hgetall(i)).finished_at&&(!t||n?a===0:!0),u=new Date().toISOString().slice(0,19).replace("T"," "),c=s&&await r.hsetnx(i,"terminal_claimed","1");if(c)await r.hset(i,{finished_at:u});if(c&&t&&!n)await r.hset(i,{cancelled_at:u});return c}finally{r.close()}}async function sn(e){try{let t=await G();await t.del(`${K}${e}`),await t.srem(rt,e),t.close()}catch{await tn(e)}}async function Ir(e){let t=new Date().toISOString().slice(0,19).replace("T"," ");await ot(e,{cancelled_at:t,finished_at:t})}async function Qr(e){try{let t=await on(),n=Date.now()-e*60*60*1000,r=0;for(let i of t)if(i.finished_at){if(new Date(i.finished_at).getTime()<n)await sn(i.id),r++}return r}catch{return nn(e)}}function oe(e){if(!e)return null;try{let t=JSON.parse(e);if(t&&(t.kind==="job"||t.kind==="module"))return t;return k.warn(`[Batch] handler JSON has unknown kind '${t?.kind}' - skipping`),null}catch(t){return k.warn(`[Batch] failed to parse persistent handler: ${t.message}`),null}}async function ae(e,t){try{if(e.kind==="job"){let{Jobs:i}=await Promise.resolve().then(() => (ye(),Oe));await i.dispatch(e.name,{...e.payload??{},_batchId:t});return}let n=await import(e.module).catch((i)=>(k.warn(`[Batch] persistent handler module not found: ${e.module} (${i.message})`),null));if(!n)return;let r=n[e.export];if(typeof r!=="function"){k.warn(`[Batch] persistent handler export '${e.export}' is not a function on ${e.module}`);return}await r(e.payload,t)}catch(n){k.error(`[Batch] persistent handler threw for batch ${t}:`,n)}}async function at(e){let t=q(),n=!1;if(t==="redis")n=await an(e,!1,!1);else{let{db:o,sql:a}=await import("@stacksjs/database");await o.updateTable("job_batches").set({pending_jobs:a`GREATEST(pending_jobs - 1, 0)`}).where("id","=",e).where("pending_jobs",">",0).execute();let l=new Date().toISOString().slice(0,19).replace("T"," "),s=await o.updateTable("job_batches").set({finished_at:l}).where("id","=",e).where("pending_jobs","=",0).whereNull("finished_at").executeTakeFirst();n=z(s)>0}let r=we(e),i=new L(e);if(r)for(let o of r.progressCallbacks)try{await o(i)}catch(a){k.error(`[Batch] Error in progress callback for batch ${e}:`,a)}if(n){try{let{emitQueueEvent:c}=await Promise.resolve().then(() => (R(),Q));await c("batch:completed",{jobId:e})}catch{}let o=await H(e);if(!o){$e(e),k.info(`[Batch] Batch ${e} finished (record vanished)`);return}let a=JSON.parse(o.options||"{}"),s=!((o.failed_jobs||0)>0)||a.allowFailures;if(r){if(s)for(let c of r.thenCallbacks)try{await c(i)}catch(d){k.error(`[Batch] Error in then callback for batch ${e}:`,d)}for(let c of r.finallyCallbacks)try{await c(i)}catch(d){k.error(`[Batch] Error in finally callback for batch ${e}:`,d)}$e(e)}if(s){let c=oe(o.then_handler);if(c)await ae(c,e)}else{let c=oe(o.catch_handler);if(c)await ae(c,e)}let u=oe(o.finally_handler);if(u)await ae(u,e);k.info(`[Batch] Batch ${e} finished`)}}async function st(e,t,n){let r=await H(e);if(!r)return;let i=JSON.parse(r.options||"{}"),o=new L(e),a=we(e),l=!1;if(q()==="redis"){l=await an(e,!0,!!i.allowFailures);let c=await H(e);if(c){let d=[];try{d=JSON.parse(c.failed_job_ids||"[]")}catch{d=[]}d.push(t),await ot(e,{failed_job_ids:JSON.stringify(d)})}if(a)for(let d of a.catchCallbacks)try{await d(o,n)}catch(w){k.error(`[Batch] Error in catch callback for batch ${e}:`,w)}try{let{emitQueueEvent:d}=await Promise.resolve().then(() => (R(),Q));await d("batch:failed",{jobId:e,error:n})}catch{}}else{let{db:c,sql:d}=await import("@stacksjs/database");await c.updateTable("job_batches").set({pending_jobs:d`GREATEST(pending_jobs - 1, 0)`,failed_jobs:d`failed_jobs + 1`}).where("id","=",e).where("pending_jobs",">",0).execute();try{let p=await H(e);if(p){let x=[];try{x=JSON.parse(p.failed_job_ids||"[]")}catch{x=[]}x.push(t),await Rr(e,{failed_job_ids:JSON.stringify(x)})}}catch{}if(a)for(let p of a.catchCallbacks)try{await p(o,n)}catch(x){k.error(`[Batch] Error in catch callback for batch ${e}:`,x)}try{let{emitQueueEvent:p}=await Promise.resolve().then(() => (R(),Q));await p("batch:failed",{jobId:e,error:n})}catch{}let w=new Date().toISOString().slice(0,19).replace("T"," "),f=c.updateTable("job_batches").set(i.allowFailures?{finished_at:w}:{finished_at:w,cancelled_at:w}).where("id","=",e).whereNull("finished_at");if(i.allowFailures)f=f.where("pending_jobs","=",0);let g=await f.executeTakeFirst();l=z(g)>0}if(!l)return;let s=!!i.allowFailures;if(a){if(s)for(let c of a.thenCallbacks)try{await c(o)}catch(d){k.error(`[Batch] Error in then callback for batch ${e}:`,d)}for(let c of a.finallyCallbacks)try{await c(o)}catch(d){k.error(`[Batch] Error in finally callback for batch ${e}:`,d)}$e(e)}let u=await H(e);if(u){if(s){let d=oe(u.then_handler);if(d)await ae(d,e)}else{let d=oe(u.catch_handler);if(d)await ae(d,e)}let c=oe(u.finally_handler);if(c)await ae(c,e)}k.info(`[Batch] Batch ${e} finished with failure(s)`)}async function cn(e){return(await H(e))?.cancelled_at!==null}var tt,K="stacks:batch:",rt="stacks:batches";var B=D(()=>{Re();tt=new Map});var Oe={};M(Oe,{Jobs:()=>hn,job:()=>dn,jobBatch:()=>pn,resolveJobFile:()=>fn,runJob:()=>Ae});import{appPath as Br,frameworkPath as Mr}from"@stacksjs/path";import{env as Hr}from"@stacksjs/env";import{enqueueAfterCommit as Lr,isInTransaction as Ur}from"@stacksjs/database";function Wr(){return Hr.QUEUE_DRIVER||"sync"}function zr(){if(un)return;un=!0,console.warn("[queue] .afterCommit() was called outside of `db.transaction(...)`. Dispatching immediately. Wrap the call in a transaction or drop .afterCommit() to silence this message.")}class O{name;payload;options={};txMode="auto";constructor(e,t){this.name=e;this.payload=t}onQueue(e){return this.options.queue=e,this}delay(e){return this.options.delay=e,this}tries(e){return this.options.tries=e,this}timeout(e){return this.options.timeout=e,this}backoff(e){return this.options.backoff=e,this}withContext(e){return this.options.context=e,this}withIdempotencyKey(e){return this.options.idempotencyKey=e,this}afterCommit(){return this.txMode="after",this}withoutCommit(){return this.txMode="immediate",this}async dispatch(){let{isFaked:e,getFakeQueue:t}=await Promise.resolve().then(() => Ue);if(e()){t()?.dispatch(this.name,this.payload,this.options);return}if(this.txMode!=="immediate"){if(Ur()){let r=this.runDispatchPipeline.bind(this);if(Lr(async()=>{await r()}))return}else if(this.txMode==="after")zr()}await this.runDispatchPipeline()}async runDispatchPipeline(){let e=!1;if(this.options.idempotencyKey){let t=await Ve(this.options.idempotencyKey,this.name,this.options.queue);if(t==="duplicate")return;e=t==="claimed"}try{if(await Ee(this.name,this.payload)){let n=N(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff});if(await Pe({queue:this.options.queue||"default",payload:F(n),exception:`quarantined: ${this.name}`},"poison-detected"))return}let t=Wr();if(t==="database")await this.dispatchToDatabase();else if(t==="redis")await this.dispatchToRedis();else if(t==="sync")await Ae(this.name,{payload:this.payload,context:this.options.context,traceId:await ln()});else if(t==="sqs"||t==="memory"||t==="beanstalkd")throw Error(`[queue] Driver "${t}" is not implemented yet. Set QUEUE_DRIVER to one of: redis, database, sync.`);else throw Error(`[queue] Unknown QUEUE_DRIVER "${t}". Allowed values: redis, database, sync.`)}catch(t){if(e&&this.options.idempotencyKey)await Ke(this.options.idempotencyKey);throw t}}async dispatchIf(e){if(e)await this.dispatch()}async dispatchUnless(e){if(!e)await this.dispatch()}async dispatchToDatabase(){let e=Math.floor(Date.now()/1000),t=this.options.delay?e+this.options.delay:e,n=N(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff},await ln()),r=F(n),{db:i}=await import("@stacksjs/database");await i.insertInto("jobs").values({queue:this.options.queue||"default",payload:r,attempts:0,reserved_at:null,available_at:t,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(){let e=N(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff});de(e);let{RedisQueue:t}=await Promise.resolve().then(() => (fe(),pe)),{queue:n}=await import("@stacksjs/config"),r=n?.connections?.redis;if(!r)throw Error("Redis queue connection is not configured. Check config/queue.ts");await new t(this.options.queue||"default",r).add(e,{delay:this.options.delay,maxTries:this.options.tries,timeout:this.options.timeout,backoff:this.options.backoff})}async dispatchNow(){await Ae(this.name,{payload:this.payload,context:this.options.context})}}function dn(e,t){return new O(e,t)}function pn(e){let{PendingBatch:t}=(B(),er(U));return new t(e)}async function fn(e){let t=[Br(`Jobs/${e}.ts`),Mr(`defaults/app/Jobs/${e}.ts`)];try{let n=import.meta.resolve("@stacksjs/defaults/package.json"),r=new URL(".",n).pathname;t.push(`${r}app/Jobs/${e}.ts`)}catch{}for(let n of t)if(await Bun.file(n).exists())return n;return null}async function Ae(e,t={}){let{withTraceId:n}=await import("@stacksjs/router"),r=t.traceId??`job:${e}:${Math.random().toString(36).slice(2,10)}`;await n(r,async()=>{let i=await fn(e);if(!i)throw Error(`Job ${e} not found. Looked in app/Jobs/${e}.ts and the framework defaults (storage/framework/defaults/app/Jobs, @stacksjs/defaults).`);let a=(await import(i)).default;if(!a)throw Error(`Job ${e} does not export a default`);if(typeof a.handle==="function")await a.handle(t.payload);else if(typeof a.action==="string"){let{runAction:l}=await import("@stacksjs/actions");await l(a.action)}else if(typeof a.action==="function")await a.action();else if(typeof a==="function")await a(t.payload,t.context);else throw Error(`Job ${e} does not have a valid handler`)})}async function ln(){try{let{getTraceId:e}=await import("@stacksjs/router");return e()}catch{return}}var un=!1,hn;var ye=D(()=>{Se();te();Ge();Te();hn={make(e,t){return new O(e,t)},async dispatch(e,t){await new O(e,t).dispatch()},async dispatchIf(e,t,n){if(e)await new O(t,n).dispatch()},async dispatchUnless(e,t,n){if(!e)await new O(t,n).dispatch()},async dispatchNow(e,t){await new O(e,t).dispatchNow()},dispatchAfter(e,t,n){return new O(t,n).delay(e)},async dispatchOnce(e,t,n){await new O(t,n).withIdempotencyKey(e).dispatch()},async dispatchAfterCommit(e,t){await new O(e,t).afterCommit().dispatch()}}});var Fe={};M(Fe,{isCircuitOpen:()=>bn,listCircuitState:()=>_n,pauseQueue:()=>yn,recordCircuitFailure:()=>wn,recordCircuitSuccess:()=>gn,resumeQueue:()=>vn});import{db as S}from"@stacksjs/database";function Y(){if(mn)return;mn=!0,console.warn("[queue/circuit-breaker] queue_circuit_state table missing - circuit breaker disabled. Run migrations to enable.")}async function ut(e,t){try{let n=await S.selectFrom("queue_circuit_state").where("queue_name","=",e).selectAll().executeTakeFirst();if(n)return n;return await S.insertInto("queue_circuit_state").values({queue_name:e,success_count:0,failure_count:0,window_start:t,paused_at:null,resume_at:null}).execute(),{queue_name:e,success_count:0,failure_count:0,window_start:t,paused_at:null,resume_at:null}}catch(n){if(_(n))return Y(),null;let r=n?.message??"";if(r.includes("UNIQUE constraint")||r.includes("Duplicate entry"))return await S.selectFrom("queue_circuit_state").where("queue_name","=",e).selectAll().executeTakeFirst();throw n}}async function bn(e){try{let t=await S.selectFrom("queue_circuit_state").where("queue_name","=",e).select(["paused_at","resume_at"]).executeTakeFirst();if(!t||!t.paused_at)return!1;if(t.resume_at){let n=Date.parse(t.resume_at.replace(" ","T")+"Z");if(Number.isFinite(n)&&Date.now()>=n)return await S.updateTable("queue_circuit_state").set({paused_at:null,resume_at:null,success_count:0,failure_count:0}).where("queue_name","=",e).execute(),!1}return!0}catch(t){if(_(t))return Y(),!1;throw t}}async function gn(e,t={}){let n=t.windowSeconds??300,r=new Date().toISOString().slice(0,19).replace("T"," "),i=await ut(e,r);if(!i)return;try{if(ct(i.window_start,n)){await S.updateTable("queue_circuit_state").set({success_count:1,failure_count:0,window_start:r}).where("queue_name","=",e).execute();return}await S.updateTable("queue_circuit_state").set({success_count:i.success_count+1}).where("queue_name","=",e).execute()}catch(o){if(_(o)){Y();return}throw o}}async function wn(e,t={}){let n=t.failureRateThreshold??0.5,r=t.windowSeconds??300,i=t.pauseSeconds??300,o=t.minObservations??10,a=new Date,l=a.toISOString().slice(0,19).replace("T"," "),s=await ut(e,l);if(!s)return!1;if(s.paused_at)return!1;try{let{success_count:u,failure_count:c}=s;if(ct(s.window_start,r))u=0,c=0;c+=1;let d=u+c,w=d===0?0:c/d;if(d>=o&&w>=n){let g=new Date(a.getTime()+i*1000).toISOString().slice(0,19).replace("T"," ");return await S.updateTable("queue_circuit_state").set({success_count:u,failure_count:c,window_start:l,paused_at:l,resume_at:g}).where("queue_name","=",e).execute(),!0}return await S.updateTable("queue_circuit_state").set({success_count:u,failure_count:c,window_start:ct(s.window_start,r)?l:s.window_start}).where("queue_name","=",e).execute(),!1}catch(u){if(_(u))return Y(),!1;throw u}}function ct(e,t){if(!e)return!0;let n=Date.parse(e.replace(" ","T")+"Z");if(!Number.isFinite(n))return!0;return Date.now()-n>t*1000}async function yn(e,t=300){let n=new Date,r=n.toISOString().slice(0,19).replace("T"," "),i=new Date(n.getTime()+t*1000).toISOString().slice(0,19).replace("T"," ");await ut(e,r);try{await S.updateTable("queue_circuit_state").set({paused_at:r,resume_at:i}).where("queue_name","=",e).execute()}catch(o){if(_(o)){Y();return}throw o}}async function vn(e){try{await S.updateTable("queue_circuit_state").set({paused_at:null,resume_at:null,success_count:0,failure_count:0}).where("queue_name","=",e).execute()}catch(t){if(_(t)){Y();return}throw t}}async function _n(){try{return await S.selectFrom("queue_circuit_state").selectAll().execute()??[]}catch(e){if(_(e))return Y(),[];throw e}}var mn=!1;var ve=()=>{};te();import{env as wr}from"@stacksjs/env";function Dt(){return wr.QUEUE_DRIVER||"sync"}class $t{name;description;action;handle;queue;rate;tries;timeout;backoff;backoffConfig;enabled;constructor(e){this.name=e.name,this.description=e.description,this.handle=e.handle,this.queue=e.queue,this.rate=e.rate,this.action=e.action,this.tries=e.tries,this.timeout=e.timeout,this.backoff=e.backoff,this.backoffConfig=e.backoffConfig,this.enabled=e.enabled}async dispatch(...[e]){let{isFaked:t,getFakeQueue:n}=await Promise.resolve().then(() => Ue);if(t()){n()?.dispatch(this.name||"UnknownJob",e,{queue:this.queue,tries:this.tries,timeout:this.timeout});return}let r=Dt();if(r==="sync")return this.dispatchNow(...[e]);if(r==="redis")return this.dispatchToRedis(e);if(r==="database")return this.dispatchToDatabase(e);if(r==="sqs"||r==="memory"||r==="beanstalkd")throw Error(`[queue] Driver "${r}" is not implemented yet. Set QUEUE_DRIVER to one of: redis, database, sync.`);throw Error(`[queue] Unknown QUEUE_DRIVER "${r}". Allowed values: redis, database, sync.`)}async dispatchIf(e,...[t]){if(e)return this.dispatch(...[t])}async dispatchUnless(e,...[t]){if(!e)return this.dispatch(...[t])}async dispatchAfter(e,...[t]){let n=Dt();if(n==="redis")return this.dispatchToRedis(t,{delay:e});if(n==="database")return this.dispatchToDatabase(t,{delay:e});return await new Promise((r)=>setTimeout(r,e*1000)),await this.dispatchNow(...[t])}async dispatchNow(...[e]){if(typeof this.handle==="function")await this.handle(e);else if(typeof this.action==="string"){let{runAction:t}=await import("@stacksjs/actions");await t(this.action)}else if(typeof this.action==="function")await this.action();else throw Error(`Job ${this.name} does not have a valid handler`)}async dispatchToDatabase(e,t){let n=Math.floor(Date.now()/1000),r=t?.delay?n+t.delay:n,i=N(this.name??this.constructor.name,e,{queue:this.queue??"default",tries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0}),o=F(i),{db:a}=await import("@stacksjs/database");await a.insertInto("jobs").values({queue:this.queue||"default",payload:o,attempts:0,reserved_at:null,available_at:r,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(e,t){let n=N(this.name??this.constructor.name,e,{queue:this.queue??"default",tries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0});de(n);let{RedisQueue:r}=await Promise.resolve().then(() => (fe(),pe)),{queue:i}=await import("@stacksjs/config"),o=i?.connections?.redis;if(!o)throw Error("Redis queue connection is not configured. Check config/queue.ts");await new r(this.queue||"default",o).add(n,{delay:t?.delay,maxTries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0})}}ye();Ge();Se();Te();ve();te();var lt=(e)=>`__job_progress__:${e}`,dt=(e)=>`__job_cancel__:${e}`;function Vr(e){return Number.isFinite(e)?Math.max(0,Math.min(100,e)):0}async function Kr(e,t,n){let{cache:r}=await import("@stacksjs/cache");await r.set(lt(e),{percent:Vr(t),message:n,updatedAt:Date.now()},3600)}async function Gr(e){let{cache:t}=await import("@stacksjs/cache");return await t.get(lt(e))??null}async function Yr(e){let{cache:t}=await import("@stacksjs/cache");await t.set(dt(e),1,3600)}async function Xr(e){let{cache:t}=await import("@stacksjs/cache");return Boolean(await t.get(dt(e)))}async function Zr(e){let{cache:t}=await import("@stacksjs/cache");await Promise.all([t.del(lt(e)),t.del(dt(e))])}import{log as se}from"@stacksjs/logging";import*as kn from"@stacksjs/path";class xn{jobs=new Map;initialized=!1;register(e){this.jobs.set(e.name,e),se.debug(`Registered job: ${e.name} (${e.type})`)}get(e){return this.jobs.get(e)}all(){return Array.from(this.jobs.values())}byQueue(e){return this.all().filter((t)=>t.config.queue===e)}scheduled(){return this.all().filter((e)=>e.config.rate||e.config.schedule)}has(e){return this.jobs.has(e)}clear(){this.jobs.clear(),this.initialized=!1}setInitialized(e){this.initialized=e}isInitialized(){return this.initialized}}var X=new xn;async function ht(e){let t=e||kn.userJobsPath(),n=[];try{let r=new Bun.Glob("**/*.{ts,js}"),i={cwd:t,onlyFiles:!0,absolute:!0};for await(let o of r.scan(i)){if(o.includes(".test.")||o.includes(".spec.")||o.endsWith("index.ts")||o.endsWith("index.js"))continue;try{let a=await ei(o);if(a)n.push(a),X.register(a)}catch(a){se.warn(`Failed to load job from ${o}: ${a.message}`)}}return X.setInitialized(!0),se.info(`Discovered ${n.length} jobs from ${t}`),n}catch(r){return se.error(`Failed to discover jobs: ${r.message}`),[]}}async function ei(e){try{let t=await import(e),n=e.split("/").pop()?.replace(/\.(ts|js)$/,"")||"UnknownJob";if(t.default&&typeof t.default==="function"){let r=t.default;if(typeof r.handle==="function"||typeof r.prototype?.handle==="function"){let i=r.config||{};return{name:i.name||n,path:e,config:{name:i.name||n,description:i.description,queue:i.queue||"default",tries:i.retries||3,timeout:i.timeout,withoutOverlapping:i.withoutOverlapping,schedule:i.schedule,retryAfter:i.retryAfter},type:"class",module:r}}}if(t.default&&typeof t.default==="object"){let r=t.default;if(typeof r.handle==="function"||typeof r.action==="string")return{name:r.name||n,path:e,config:{name:r.name||n,description:r.description,queue:r.queue||"default",tries:r.tries||3,backoff:r.backoff,rate:r.rate,timeout:r.timeout||r.timeOut,backoffConfig:r.backoffConfig},type:"function",module:r}}return null}catch(t){return se.debug(`Could not load job from ${e}: ${t.message}`),null}}function ti(e){return X.get(e)}function ni(){return X.all()}function pt(){return X.scheduled()}async function ri(e,t){let n=X.get(e);if(!n)throw Error(`Job "${e}" not found. Did you run discoverJobs()?`);try{if(n.type==="class"){if(typeof n.module.handle==="function")return await n.module.handle(t);return await new n.module().handle(t)}else{if(typeof n.module.handle==="function")return await n.module.handle(t);throw Error(`Job "${e}" does not have a handle method`)}}catch(r){throw se.error(`Failed to execute job "${e}": ${r.message}`),r}}function ii(e){let t=e.config;return{name:t.name,queue:t.queue,tries:t.tries,backoff:t.backoff,timeout:t.timeout,backoffConfig:t.backoffConfig,rate:t.rate}}import{log as J}from"@stacksjs/logging";R();import{log as jn}from"@stacksjs/logging";var Jn=!1;async function Pn(){if(Jn)return!0;try{let{db:e}=await import("@stacksjs/database");return await e.unsafe("CREATE TABLE IF NOT EXISTS scheduled_job_runs (job_name VARCHAR(255) PRIMARY KEY, last_run_at VARCHAR(64) NOT NULL)").execute(),Jn=!0,!0}catch(e){return jn.debug(`[scheduler] run-marker persistence unavailable, using in-memory lastRun: ${e instanceof Error?e.message:String(e)}`),!1}}async function Sn(e){if(!await Pn())return null;try{let{db:t}=await import("@stacksjs/database"),n=await t.selectFrom("scheduled_job_runs").where("job_name","=",e).select(["last_run_at"]).executeTakeFirst();if(!n?.last_run_at)return null;let r=new Date(n.last_run_at);return Number.isNaN(r.getTime())?null:r}catch{return null}}async function En(e,t){if(!await Pn())return;try{let{db:n}=await import("@stacksjs/database"),r=t.toISOString();await n.deleteFrom("scheduled_job_runs").where("job_name","=",e).execute(),await n.insertInto("scheduled_job_runs").values({job_name:e,last_run_at:r}).execute()}catch{}}function oi(e){return`%"jobName":"${e.replace(/[\\%_]/g,(n)=>`\\${n}`)}"%`}async function Tn(e){try{let{db:t}=await import("@stacksjs/database"),r=await t.unsafe("SELECT 1 AS present FROM jobs WHERE payload LIKE ? ESCAPE '\\' LIMIT 1",[oi(e)]).execute(),i=Array.isArray(r)?r:r?.rows??[];return Array.isArray(i)&&i.length>0}catch(t){return jn.debug(`[scheduler] overlap check unavailable, dispatching anyway: ${t instanceof Error?t.message:String(t)}`),!1}}Re();var $n={checkInterval:60000,preventOverlapping:!0},y={isRunning:!1,isShuttingDown:!1,checkInterval:null,jobs:new Map,config:{...$n}},Rn=new Set;function ai(e,t){if(Rn.has(e))return;Rn.add(e),J.warn(`[scheduler] Cron expression "${e}" specifies seconds="${t}" but the scheduler ticks at minute granularity - the seconds field is being ignored. Use a 5-field expression to avoid this warning, or wait for sub-minute scheduling support.`)}var Cn=!1,qn=new Map;function si(e){let t=qn.get(e);if(!t)t=new Intl.DateTimeFormat("en-US",{timeZone:e,hour12:!1,month:"numeric",day:"numeric",hour:"numeric",minute:"numeric",weekday:"short"}),qn.set(e,t);return t}function ft(e,t){if(t&&t!=="local"&&t!=="system")try{let n=si(t).formatToParts(e),r=(a)=>n.find((l)=>l.type===a)?.value??"",i={Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6},o=Number(r("hour"));if(o===24)o=0;return{minute:Number(r("minute")),hour:o,day:Number(r("day")),month:Number(r("month")),dayOfWeek:i[r("weekday")]??e.getDay()}}catch{if(!Cn)Cn=!0,J.warn(`[scheduler] Invalid timezone "${t}"; falling back to system local time.`)}return{minute:e.getMinutes(),hour:e.getHours(),day:e.getDate(),month:e.getMonth()+1,dayOfWeek:e.getDay()}}function ci(e,t,n){let r=ft(new Date,n),i=r.minute,o=r.hour,a=r.day,l=r.month,s=r.dayOfWeek;if(t){let p=ft(t,n);if(p.minute===i&&p.hour===o&&p.day===a)return!1}let u=e.trim().split(/\s+/);if(u.length===6){let p=u[0];if(p&&p!=="0"&&p!=="*")ai(e,p);u=u.slice(1)}if(u.length<5)return J.warn(`Invalid cron expression: ${e}`),!1;let[c,d,w,f,g]=u;return A(c,i,0,59)&&A(d,o,0,23)&&A(w,a,1,31)&&A(f,l,1,12)&&A(g,s,0,6)}function A(e,t,n,r){if(e==="*")return!0;if(e.includes(","))return e.split(",").map((a)=>Number.parseInt(a.trim(),10)).includes(t);if(e.includes("-")){let[o,a]=e.split("-").map((l)=>Number.parseInt(l.trim(),10));return t>=o&&t<=a}if(e.includes("/")){let[o,a]=e.split("/"),l=Number.parseInt(a,10);if(o==="*")return t%l===0;if(o.includes("-")){let[s,u]=o.split("-").map((c)=>Number.parseInt(c.trim(),10));return t>=s&&t<=u&&(t-s)%l===0}}let i=Number.parseInt(e,10);return!Number.isNaN(i)&&t===i}function Nn(e){let n={"@yearly":"0 0 1 1 *","@annually":"0 0 1 1 *","@monthly":"0 0 1 * *","@weekly":"0 0 * * 0","@daily":"0 0 * * *","@midnight":"0 0 * * *","@hourly":"0 * * * *"}[e.toLowerCase()];if(n)return n;let r=e.match(/^Every\.(\w+)$/i);if(r&&r[1]!==void 0){let o=r[1].toLowerCase();return{second:"* * * * *",fiveseconds:"* * * * *",tenseconds:"* * * * *",thirtyseconds:"* * * * *",minute:"* * * * *",fiveminutes:"*/5 * * * *",tenminutes:"*/10 * * * *",fifteenminutes:"*/15 * * * *",thirtyminutes:"*/30 * * * *",hour:"0 * * * *",twohours:"0 */2 * * *",sixhours:"0 */6 * * *",twelvehours:"0 */12 * * *",day:"0 0 * * *",week:"0 0 * * 0",month:"0 0 1 * *"}[o]||null}let i=e.split(/\s+/).length;if(i>=5&&i<=6)return e;return null}function On(e,t){let n=e.trim().split(/\s+/),r=n.length===6?n.slice(1):n;if(r.length<5)return null;let[i,o,a,l,s]=r,u=new Date;u.setSeconds(0,0);let c=527040;for(let d=1;d<=c;d++){let w=new Date(u.getTime()+d*60000),f=ft(w,t);if(A(i,f.minute,0,59)&&A(o,f.hour,0,23)&&A(a,f.day,1,31)&&A(l,f.month,1,12)&&A(s,f.dayOfWeek,0,6))return w}return null}async function ui(e={}){if(y.isRunning){J.warn("Scheduler is already running");return}y.config={...$n,...e},y.isRunning=!0,y.isShuttingDown=!1,await ht();let t=pt();for(let i of t){let o=i.config.rate||i.config.schedule;if(o){let a=Nn(o);if(a){let l=await Sn(i.name);y.jobs.set(i.name,{job:i,lastRun:l,nextRun:On(a,y.config.timezone),isRunning:!1}),J.info(`Registered scheduled job: ${i.name} (${a})`)}else J.warn(`Invalid schedule for job ${i.name}: ${o}`)}}if(y.jobs.size===0){J.info("No scheduled jobs found");return}J.info(`Scheduler started with ${y.jobs.size} job(s)`),process.on("SIGINT",()=>mt()),process.on("SIGTERM",()=>mt());let n=!1,r=()=>{if(y.isShuttingDown)return;let i=y.config.checkInterval,o=i-Date.now()%i;y.checkInterval=setTimeout(()=>{if(!y.isShuttingDown&&!n)n=!0,Dn().catch((a)=>J.error("Scheduler check failed:",a)).finally(()=>{n=!1});r()},o),y.checkInterval?.unref?.()};r(),await Dn()}async function Dn(){for(let[e,t]of y.jobs){let n=t.job.config.rate||t.job.config.schedule;if(!n)continue;let r=Nn(n);if(!r)continue;if(ci(r,t.lastRun,y.config.timezone)){if((y.config.preventOverlapping||t.job.config.withoutOverlapping)&&await Tn(e)){J.debug(`Skipping ${e}: previous execution still running`);continue}if(t.isRunning){J.debug(`Skipping ${e}: a dispatch for it is already in flight`);continue}try{t.isRunning=!0,t.lastRun=new Date,t.nextRun=On(r,y.config.timezone),await En(e,t.lastRun),J.info(`Dispatching scheduled job: ${e}`),await V("job:added",{jobId:`scheduled-${e}-${Date.now()}`,queueName:t.job.config.queue||"default",jobName:e}),await Ye(e,{queue:t.job.config.queue||"default",payload:{},maxTries:t.job.config.tries||3,timeout:t.job.config.timeout||60}),t.isRunning=!1,J.info(`Scheduled job ${e} dispatched to queue`)}catch(o){t.isRunning=!1,J.error(`Failed to dispatch scheduled job ${e}:`,o)}}}}async function mt(){if(!y.isRunning)return;if(J.info("Stopping scheduler..."),y.isShuttingDown=!0,y.checkInterval)clearTimeout(y.checkInterval),y.checkInterval=null;y.isRunning=!1,y.jobs.clear(),J.info("Scheduler stopped")}function li(){return{isRunning:y.isRunning,jobCount:y.jobs.size,jobs:Array.from(y.jobs.entries()).map(([e,t])=>({name:e,schedule:t.job.config.rate||t.job.config.schedule,lastRun:t.lastRun,nextRun:t.nextRun,isRunning:t.isRunning}))}}function di(){return y.isRunning}function hi(){return new Map(y.jobs)}async function pi(e){let t=y.jobs.get(e);if(!t)throw Error(`Scheduled job "${e}" not found`);J.info(`Manually triggering scheduled job: ${e}`),await Ye(e,{queue:t.job.config.queue||"default",payload:{},maxTries:t.job.config.tries||3,timeout:t.job.config.timeout||60})}R();R();import{log as fi}from"@stacksjs/logging";function An(e,t=Math.floor(Date.now()/1000)){if(e.reserved_at)return"processing";let n=typeof e.available_at==="number"?e.available_at:typeof e.available_at==="string"&&e.available_at.trim()?Number(e.available_at):Number.NaN;if(Number.isFinite(n)&&n>t)return"delayed";return"pending"}var mi={maxPendingWarning:1000,maxPendingCritical:5000,maxFailedWarning:10,maxFailedCritical:100,maxJobAgeWarning:3600,maxJobAgeCritical:86400,maxErrorRateWarning:0.1,maxErrorRateCritical:0.5};async function bt(e={}){let t={...mi,...e},n=[],r=new Date,i=Math.floor(r.getTime()/1000);try{let{db:o}=await import("@stacksjs/database"),a=await o.selectFrom("jobs").selectAll().execute(),l=await o.selectFrom("failed_jobs").selectAll().execute(),s=new Map;for(let m of a){let v=m.queue||"default";if(!s.has(v))s.set(v,{pending:0,processing:0,delayed:0});let P=s.get(v),E=An(m,i);if(E==="processing")P.processing++;else if(E==="delayed")P.delayed++;else if(P.pending++,m.created_at){let Kn=typeof m.created_at==="number"?m.created_at:Math.floor(new Date(m.created_at).getTime()/1000),kt=i-Kn;if(!P.oldestAge||kt>P.oldestAge)P.oldestAge=kt}}let u=new Map;for(let m of l){let v=m.queue||"default";u.set(v,(u.get(v)||0)+1)}let c=[],d=new Set([...s.keys(),...u.keys()]),w=t.queues?[...d].filter((m)=>t.queues.includes(m)):[...d],f=0,g=0,p=0,x=0;for(let m of w){let v=s.get(m)||{pending:0,processing:0,delayed:0},P=u.get(m)||0;f+=v.pending,g+=v.processing,p+=v.delayed,x+=P;let E="healthy";if(v.pending>=t.maxPendingCritical)E="unhealthy",n.push({level:"critical",message:`Queue "${m}" has ${v.pending} pending jobs (threshold: ${t.maxPendingCritical})`,queue:m,timestamp:r.toISOString()});else if(v.pending>=t.maxPendingWarning)E="degraded",n.push({level:"warning",message:`Queue "${m}" has ${v.pending} pending jobs (threshold: ${t.maxPendingWarning})`,queue:m,timestamp:r.toISOString()});if(P>=t.maxFailedCritical)E="unhealthy",n.push({level:"critical",message:`Queue "${m}" has ${P} failed jobs (threshold: ${t.maxFailedCritical})`,queue:m,timestamp:r.toISOString()});else if(P>=t.maxFailedWarning){if(E==="healthy")E="degraded";n.push({level:"warning",message:`Queue "${m}" has ${P} failed jobs (threshold: ${t.maxFailedWarning})`,queue:m,timestamp:r.toISOString()})}if(v.oldestAge){if(v.oldestAge>=t.maxJobAgeCritical)E="unhealthy",n.push({level:"critical",message:`Queue "${m}" has a job waiting for ${Math.floor(v.oldestAge/3600)} hours`,queue:m,timestamp:r.toISOString()});else if(v.oldestAge>=t.maxJobAgeWarning){if(E==="healthy")E="degraded";n.push({level:"warning",message:`Queue "${m}" has a job waiting for ${Math.floor(v.oldestAge/60)} minutes`,queue:m,timestamp:r.toISOString()})}}c.push({name:m,status:E,pending:v.pending,processing:v.processing,delayed:v.delayed,failed:P,oldestJobAge:v.oldestAge})}let vt=De().getMetrics(),Wn=vt.throughputPerMinute,zn=vt.averageDuration,_t=f+g+p+x,le=_t>0?x/_t:0,ee="healthy";if(c.some((m)=>m.status==="unhealthy"))ee="unhealthy";else if(c.some((m)=>m.status==="degraded"))ee="degraded";if(le>=t.maxErrorRateCritical)ee="unhealthy",n.push({level:"critical",message:`Overall error rate is ${(le*100).toFixed(1)}% (threshold: ${t.maxErrorRateCritical*100}%)`,timestamp:r.toISOString()});else if(le>=t.maxErrorRateWarning){if(ee==="healthy")ee="degraded";n.push({level:"warning",message:`Overall error rate is ${(le*100).toFixed(1)}% (threshold: ${t.maxErrorRateWarning*100}%)`,timestamp:r.toISOString()})}let Vn=ge().getAll().map((m)=>({id:m.id,status:m.status,queue:m.queue,processedCount:m.processedCount,failedCount:m.failedCount,lastActivityAt:m.lastActivityAt}));return{status:ee,timestamp:r.toISOString(),queues:c,workers:Vn,metrics:{totalPending:f,totalProcessing:g,totalDelayed:p,totalFailed:x,throughputPerMinute:Wn,averageProcessingTime:zn,errorRate:le},alerts:n}}catch(o){fi.error("Failed to perform queue health check:",o);let l=ge().getAll().map((s)=>({id:s.id,status:s.status,queue:s.queue,processedCount:s.processedCount,failedCount:s.failedCount,lastActivityAt:s.lastActivityAt}));return{status:"unhealthy",timestamp:r.toISOString(),queues:[],workers:l,metrics:{totalPending:0,totalProcessing:0,totalDelayed:0,totalFailed:0,throughputPerMinute:0,averageProcessingTime:0,errorRate:0},alerts:[{level:"critical",message:`Health check failed: ${o.message}`,timestamp:r.toISOString()}]}}}function bi(e={}){return async(t)=>{let n=await bt(e),r=n.status==="healthy"?200:n.status==="degraded"?207:503;return new Response(JSON.stringify(n,null,2),{status:r,headers:{"Content-Type":"application/json","Cache-Control":"no-store"}})}}async function gi(e={}){return(await bt(e)).status==="healthy"}import{log as C}from"@stacksjs/logging";var wi={"&":"&","<":"<",">":">",'"':""","'":"'"};function _e(e){return String(e).replace(/[&<>"']/g,(t)=>wi[t]??t)}class gt{config;notificationCount=0;lastResetTime=Date.now();pendingBatch=[];batchTimeout=null;activeFlush=null;constructor(e){this.config=e}async notify(e){if(this.config.filter&&!this.config.filter(e))return;if(this.config.rateLimit){let t=Date.now();if(t-this.lastResetTime>3600000)this.notificationCount=0,this.lastResetTime=t;if(this.notificationCount>=this.config.rateLimit){C.debug("Rate limit reached for failed job notifications");return}}if(this.config.batch){if(this.pendingBatch.push(e),!this.batchTimeout)this.batchTimeout=setTimeout(()=>{this.batchTimeout=null,this.activeFlush=this.flushBatch().catch((t)=>C.error("Failed to flush notification batch:",t)).finally(()=>{this.activeFlush=null})},this.config.batchInterval||60000),this.batchTimeout.unref?.();return}await this.sendNotifications([e])}async flushBatch(){if(this.pendingBatch.length===0)return;let e=[...this.pendingBatch];if(this.pendingBatch=[],this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;await this.sendNotifications(e)}async shutdown(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.activeFlush)await this.activeFlush.catch(()=>{});await this.flushBatch().catch((e)=>C.error("Failed to flush notification batch on shutdown:",e))}async sendNotifications(e){let t=[];for(let n of this.config.channels)switch(n){case"email":if(this.config.email)t.push(this.sendEmail(e));break;case"slack":if(this.config.slack)t.push(this.sendSlack(e));break;case"discord":if(this.config.discord)t.push(this.sendDiscord(e));break;case"webhook":if(this.config.webhook)t.push(this.sendWebhook(e));break;case"log":this.logFailures(e);break}try{await Promise.all(t)}catch(n){C.error("Failed to send job failure notifications:",n)}finally{this.notificationCount+=e.length}}async sendEmail(e){let t=this.config.email,n=t.subject||`[Queue] ${e.length} Job(s) Failed`,r=this.formatEmailBody(e);try{let{mail:i}=await import("@stacksjs/email");await i.send({to:Array.isArray(t.to)?t.to:[t.to],from:t.from?{address:t.from}:void 0,subject:n,html:r})}catch(i){C.error("Failed to send email notification:",i)}}formatEmailBody(e){let t=e.map((n)=>`
|
|
3
3
|
<tr>
|
|
4
4
|
<td style="padding: 8px; border: 1px solid #ddd;">${_e(String(n.id))}</td>
|
|
5
5
|
<td style="padding: 8px; border: 1px solid #ddd;">${_e(n.name)}</td>
|
|
@@ -28,4 +28,4 @@ var{defineProperty:Ie,getOwnPropertyNames:Gn,getOwnPropertyDescriptor:Yn}=Object
|
|
|
28
28
|
</table>
|
|
29
29
|
`}async sendSlack(e){let t=this.config.slack,n=[{type:"header",text:{type:"plain_text",text:`\uD83D\uDEA8 ${e.length} Job(s) Failed`,emoji:!0}},...e.slice(0,10).map((r)=>({type:"section",text:{type:"mrkdwn",text:`*${r.name}*
|
|
30
30
|
Queue: ${r.queue} | Attempts: ${r.attempts}/${r.maxAttempts}
|
|
31
|
-
\`\`\`${r.exception.slice(0,200)}\`\`\``}}))];if(e.length>10)n.push({type:"section",text:{type:"mrkdwn",text:`_...and ${e.length-10} more failed jobs_`}});try{let r=await fetch(t.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel:t.channel,username:t.username||"Queue Monitor",icon_emoji:t.iconEmoji||":warning:",blocks:n})});if(!r.ok)q.error(`Slack notification failed with status ${r.status}`)}catch(r){q.error("Failed to send Slack notification:",r)}}async sendDiscord(e){let t=this.config.discord,n=e.slice(0,10).map((r)=>({title:`\u274C ${r.name}`,color:15158332,fields:[{name:"Queue",value:r.queue,inline:!0},{name:"Attempts",value:`${r.attempts}/${r.maxAttempts}`,inline:!0},{name:"Failed At",value:r.failedAt.toISOString(),inline:!0},{name:"Exception",value:`\`\`\`${r.exception.slice(0,500)}\`\`\``}],timestamp:r.failedAt.toISOString()}));try{let r=await fetch(t.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t.username||"Queue Monitor",avatar_url:t.avatarUrl,content:`\uD83D\uDEA8 **${e.length} Job(s) Failed**`,embeds:n})});if(!r.ok)q.error(`Discord notification failed with status ${r.status}`)}catch(r){q.error("Failed to send Discord notification:",r)}}async sendWebhook(e){let t=this.config.webhook,n={event:"jobs.failed",timestamp:new Date().toISOString(),count:e.length,jobs:e.map((i)=>({id:i.id,name:i.name,queue:i.queue,attempts:i.attempts,maxAttempts:i.maxAttempts,exception:i.exception,failedAt:i.failedAt.toISOString()}))},r={"Content-Type":"application/json",...t.headers};if(t.secret){let i=JSON.stringify(n),o=await this.generateSignature(i,t.secret);r["X-Signature"]=o}try{let i=await fetch(t.url,{method:"POST",headers:r,body:JSON.stringify(n)});if(!i.ok)q.error(`Webhook notification failed with status ${i.status}`)}catch(i){q.error("Failed to send webhook notification:",i)}}async generateSignature(e,t){let n=new TextEncoder,r=await crypto.subtle.importKey("raw",n.encode(t),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",r,n.encode(e));return Array.from(new Uint8Array(i)).map((o)=>o.toString(16).padStart(2,"0")).join("")}logFailures(e){for(let t of e)q.error(`[Queue] Job "${t.name}" failed on queue "${t.queue}" after ${t.attempts} attempts: ${t.exception}`)}async cleanup(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.pendingBatch.length>0)await this.flushBatch()}}var ke=null;function yi(e){return ke=new gt(e),ke}function vi(){return ke}async function _i(e){if(ke)await ke.notify(e)}B();te();Ce();import{err as ki,ok as Nn}from"@stacksjs/error-handling";import{log as b}from"@stacksjs/logging";import ce from"process";import{env as ji}from"@stacksjs/env";var In=!1;function xi(){if(In)return;In=!0,ce.on("unhandledRejection",(e,t)=>{b.error(`Unhandled Rejection: ${e}`)}),ce.on("uncaughtException",(e)=>{b.error(`Uncaught Exception: ${e.message}`)})}var W=0,ue=!1,J="",Z=new Set,Qn=new Map;function Hn(e){return Z.add(e),e.finally(()=>Z.delete(e)),e}function Ji(){let e=ce.env.STACKS_QUEUE_RESERVATION_TTL_SEC,t=e===void 0?Number.NaN:Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:3600}function Si(){let e=ce.env.STACKS_QUEUE_RETRY_JITTER;if(e===void 0)return 0.2;let t=Number(e);if(!Number.isFinite(t)||t<0)return 0.2;return Math.min(t,1)}function Ei(e,t,n=Math.random){if(!(e>0)||t<=0)return e;return Math.round(e+e*t*n())}function Pi(){let e=ce.env.STACKS_QUEUE_SWEEP_INTERVAL_SEC,t=e===void 0?Number.NaN:Number.parseInt(e,10);return(Number.isFinite(t)&&t>0?t:60)*1000}async function Bn(){let e=Ji(),t=Math.floor(Date.now()/1000)-e,n=Math.floor(Date.now()/1000);try{let{db:r}=await import("@stacksjs/database"),i=await r.updateTable("jobs").set({reserved_at:null,available_at:n}).where("reserved_at","<=",t).executeTakeFirst(),o=z(i);if(o>0)b.warn(`[queue] Requeued ${o} job(s) whose reservation exceeded the ${e}s TTL - likely victims of a worker crash. Set STACKS_QUEUE_RESERVATION_TTL_SEC to tune.`);return o}catch(r){return b.error("[queue] Reservation sweep failed",{reason:r instanceof Error?r.message:String(r)}),0}}function Ti(e){if(!e||typeof e!=="object")return!1;let t=e.status??e.statusCode;if(typeof t==="number"&&t>=400&&t<500)return!0;let n=e.name;if(typeof n==="string"&&(n==="ValidationError"||n==="ModelNotFoundError"))return!0;return!1}function Ci(){return ji.QUEUE_DRIVER||"sync"}async function qi(e,t={}){try{b.info("Starting queue processor..."),xi(),ue=!0,J=`worker-${ce.pid}-${Date.now()}`;let n=t.concurrency||1,r=Ci(),{getWorkerTracker:i,getGlobalMetrics:o}=await Promise.resolve().then(() => (C(),Q));if(o(),i().register(J,e||"default"),r==="redis")return b.info("Using Redis queue driver (bun-queue)"),await Ii(e||"default",n),Nn(void 0);let a;if(e)a=[e];else if(a=await Ln(),a.length===0)a=["default"];return b.info(`Processing queues: ${a.join(", ")}`),await Ri(a,n),Nn(void 0)}catch(n){return ue=!1,ki(n)}}async function Ln(){try{let{db:e}=await import("@stacksjs/database"),n=(await e.selectFrom("jobs").select("queue").distinct().execute()).map((r)=>r.queue).filter((r)=>Boolean(r));return n.length>0?n:["default"]}catch{return["default"]}}async function Ri(e,t){b.info("Listening for jobs...");let n=e,r=Date.now(),i=1e4,o=Date.now(),a=Pi();await Bn();while(ue)try{let l=Date.now();if(l-r>i){try{let s=await Ln();if(s.length>0)n=s}catch{}r=l}if(l-o>a)await Bn(),o=l;for(let s of n){try{let{isCircuitOpen:c}=await Promise.resolve().then(() => (ve(),Ne));if(await c(s))continue}catch{}let u=[];try{u=await Di(s,t)}catch(c){let d=Date.now(),w=Qn.get(s)??0;if(d-w>60000)Qn.set(s,d),b.error(`[queue] Could not reserve jobs on "${s}" - retrying each cycle:`,c);continue}await Promise.all(u.map(async(c)=>{try{b.info(`Processing job ${c.id} from queue "${s}"`),await Hn($i(c))}catch{b.error(`Unexpected error processing job ${c.id}`)}}))}await yt(1000)}catch{await yt(3000)}}async function Di(e,t){let n=Math.floor(Date.now()/1000),{db:r}=await import("@stacksjs/database"),i=[];for(let o=0;o<t;o++){let a=await r.selectFrom("jobs").where("queue","=",e).whereNull("reserved_at").where("available_at","<=",n).orderBy("id","asc").limit(1).selectAll().executeTakeFirst();if(!a)break;let l=await r.updateTable("jobs").set({reserved_at:n,attempts:(a.attempts||0)+1}).where("id","=",a.id).whereNull("reserved_at").executeTakeFirst();if(z(l)>0)i.push(a)}return i}async function $i(e){let t=e.id,n=e.queue||"default";W++;let r=Date.now(),{emitQueueEvent:i,getWorkerTracker:o}=await Promise.resolve().then(() => (C(),Q)),a=o();a.markActive(J);let l;try{l=JSON.parse(e.payload||"{}").jobName}catch{}await i("job:processing",{jobId:String(t),queueName:n,jobName:l});try{let c=JSON.parse(e.payload||"{}").payload?._batchId;if(c){let{isBatchCancelled:d}=await Promise.resolve().then(() => (B(),U));if(await d(c)){b.info(`[Queue] Skipping job ${t} - batch ${c} has been cancelled`),await wt(t),W--,a.markIdle(J);return}}}catch{}let s=null;try{let u=JSON.parse(e.payload||"{}"),c=Fi(u);if(c===void 0)await Mn(u);else await Ni(Mn(u),c*1000,`Job ${t} exceeded ${c}s timeout`)}catch(u){s=u instanceof Error?u:Error(String(u))}if(!s)try{await wt(t),b.info(`[Queue] Job ${t} completed`),a.recordCompletion(J);try{let{recordCircuitSuccess:u}=await Promise.resolve().then(() => (ve(),Ne));await u(e.queue??"default")}catch{}await i("job:completed",{jobId:String(t),queueName:n,duration:Date.now()-r});try{let c=JSON.parse(e.payload||"{}").payload?._batchId;if(c){let{recordBatchJobCompletion:d}=await Promise.resolve().then(() => (B(),U));await d(c)}}catch{}}catch{b.info(`[Queue] Failed to delete completed job ${t}`)}else{let u=s.message;b.info(`[Queue] Job ${t} failed: ${u}`),a.recordFailure(J),await i("job:failed",{jobId:String(t),queueName:n,error:s,duration:Date.now()-r,attemptsMade:(e.attempts||0)+1});let c=1,d={};try{d=JSON.parse(e.payload||"{}"),c=d.options?.tries||1}catch{}let w=(e.attempts||0)+1;if(w>=c){b.info(`[Queue] Job ${t} exceeded max attempts (${w}/${c})`);let f=!1;if(d?._retriedFromFailed===!0)try{let{moveToDeadLetter:g}=await Promise.resolve().then(() => (Ee(),It));if(f=await g({queue:e.queue,payload:e.payload,exception:s.stack||s.message},"repeat-failure",2),f)b.info(`[Queue] Job ${t} re-failed after retry - moved to dead_letter_jobs`)}catch{f=!1}if(!f){b.info(`[Queue] Moving job ${t} to failed_jobs`);try{f=await Ai(e,s,{attempts:w,maxAttempts:c,durationMs:Date.now()-r})}catch{f=!1}}if(f)try{await wt(t)}catch{b.info(`[Queue] Failed to delete failed job ${t}`)}else b.error(`[Queue] Job ${t} exhausted its retries but could NOT be persisted to failed_jobs - leaving it in the queue to avoid data loss (the reservation sweep will retry it). Check that the failed_jobs table exists and is writable.`);try{let{recordFailureForPoison:g}=await Promise.resolve().then(() => (Te(),Wt)),{recordCircuitFailure:p}=await Promise.resolve().then(() => (ve(),Ne)),x=d?.jobName??"unknown";await g(x,d?.payload),await p(e.queue??"default")}catch{}try{let g=d.payload?._batchId;if(g){let{recordBatchJobFailure:p}=await Promise.resolve().then(() => (B(),U));await p(g,String(t),s)}}catch{}}else{let f=d.options?.backoff,g=30;if(Array.isArray(f)&&f.length>0){let p=Math.min(w-1,f.length-1);g=f[p]}else if(typeof f==="number"&&f>0)g=f;if(g=Number(g),!Number.isFinite(g)||g<0)g=30;g=Ei(g,Si()),b.info(`[Queue] Job ${t} will be retried in ${g}s (attempt ${w}/${c})`);try{await Oi(t,g),b.info(`[Queue] Job ${t} released for retry`)}catch{b.info(`[Queue] Failed to release job ${t} for retry`)}}}W--,a.markIdle(J)}async function wt(e){let{db:t}=await import("@stacksjs/database");await t.deleteFrom("jobs").where("id","=",e).execute()}async function Oi(e,t=30){let n=Math.floor(Date.now()/1000)+t;b.debug(`Releasing job ${e} for retry at ${n}`);try{let{db:r}=await import("@stacksjs/database");await r.updateTable("jobs").set({reserved_at:null,available_at:n}).where("id","=",e).execute(),b.debug(`Job ${e} released successfully`)}catch{b.error(`Failed to release job ${e}`)}}async function Ai(e,t,n){try{let r=new Date().toISOString().slice(0,19).replace("T"," "),i=crypto.randomUUID(),o=t.stack||t.message,{db:a}=await import("@stacksjs/database");return await a.insertInto("failed_jobs").values({uuid:i,connection:"database",queue:e.queue,payload:e.payload,exception:o,attempts:n.attempts,max_attempts:n.maxAttempts,duration_ms:n.durationMs,failed_at:r}).execute(),!0}catch(r){return b.error("Failed to log failed job:",r),!1}}function Fi(e){if(!e||typeof e!=="object")return;let n=e.options?.timeout;if(typeof n!=="number"||!Number.isFinite(n)||n<=0)return;return n}async function Ni(e,t,n){let r,i=new Promise((o,a)=>{r=setTimeout(()=>a(Error(n)),t)});try{return await Promise.race([e,i])}finally{if(r!==void 0)clearTimeout(r)}}async function Mn(e){let t=xe(e);if(!t.ok)throw Error(`[queue] Cannot deserialize job envelope: ${t.reason}`+(t.detail?` (${t.detail})`:""));let{runJob:n}=await Promise.resolve().then(() => (ye(),Ae));await n(t.envelope.jobName,{payload:t.envelope.payload,traceId:t.envelope.traceId})}async function Ii(e,t){let{RedisQueue:n}=await Promise.resolve().then(() => (fe(),pe)),{queue:r}=await import("@stacksjs/config"),i=r?.connections?.redis;if(!i)throw Error("Redis queue connection is not configured. Check config/queue.ts");let o=new n(e,i),{emitQueueEvent:a,getWorkerTracker:l}=await Promise.resolve().then(() => (C(),Q)),s=l(),u=async(c)=>{W++,s.markActive(J);let d=Date.now(),w=xe(c.data);if(!w.ok){W--,s.markIdle(J),b.error(`[Queue] Skipping Redis job ${c.id} - unparseable envelope: ${w.reason}${w.detail?` (${w.detail})`:""}`);return}let f=w.envelope,g=f.payload?._batchId;if(g)try{let{isBatchCancelled:p}=await Promise.resolve().then(() => (B(),U));if(await p(g)){b.info(`[Queue] Skipping Redis job ${c.id} - batch ${g} has been cancelled`),W--,s.markIdle(J);return}}catch{}await a("job:processing",{jobId:String(c.id),queueName:e});try{let{runJob:p}=await Promise.resolve().then(() => (ye(),Ae));if(await p(f.jobName,{payload:f.payload,traceId:f.traceId}),s.recordCompletion(J),await a("job:completed",{jobId:String(c.id),queueName:e,duration:Date.now()-d}),g)try{let{recordBatchJobCompletion:x}=await Promise.resolve().then(() => (B(),U));await x(g)}catch{}b.info(`[Queue] Redis job ${c.id} completed`)}catch(p){if(s.recordFailure(J),await a("job:failed",{jobId:String(c.id),queueName:e,error:p instanceof Error?p:Error(String(p)),duration:Date.now()-d}),g)try{let{recordBatchJobFailure:x}=await Promise.resolve().then(() => (B(),U));await x(g,String(c.id),p instanceof Error?p:Error(String(p)))}catch{}if(b.error(`[Queue] Redis job ${c.id} failed: ${p}`),Ti(p)){b.info(`[Queue] Redis job ${c.id} hit a non-retryable error - skipping retry`);return}throw p}finally{W--,s.markIdle(J)}};o.process(t,(c)=>Hn(u(c))),b.info(`Listening for Redis jobs on queue "${e}" with concurrency ${t}...`);while(ue)await yt(1000);await o.close()}async function Qi(e={}){let t=e.graceMs??1e4;if(ue=!1,Z.size>0&&t>0){b.info(`[queue] Draining ${Z.size} in-flight job(s) (grace ${t}ms)`);let n=Promise.allSettled([...Z]),r=new Promise((o)=>setTimeout(()=>o("timeout"),t));if(await Promise.race([n.then(()=>"drained"),r])==="timeout"&&Z.size>0)b.warn(`[queue] Drain timed out with ${Z.size} job(s) still active. Their reservations will be reclaimed by the next worker's sweep (Q-2).`)}if(J){let{getWorkerTracker:n}=await Promise.resolve().then(() => (C(),Q));n().unregister(J)}b.info("Queue processor stopped")}async function Bi(){let{db:e}=await import("@stacksjs/database"),t=await e.selectFrom("failed_jobs").selectAll().execute();for(let n of t)await Un(Number(n.id))}async function Un(e){let t=Math.floor(Date.now()/1000),n=new Date().toISOString().slice(0,19).replace("T"," "),{db:r}=await import("@stacksjs/database"),o=(await r.selectFrom("failed_jobs").where("id","=",e).selectAll().execute())[0];if(!o)throw Error(`Failed job ${e} not found`);let a=o.payload;try{let l=JSON.parse(o.payload||"{}");l._retriedFromFailed=!0,a=JSON.stringify(l)}catch{}await r.insertInto("jobs").values({queue:o.queue,payload:a,attempts:0,reserved_at:null,available_at:t,created_at:n}).execute(),await r.deleteFrom("failed_jobs").where("id","=",e).execute(),b.info(`Failed job ${e} has been re-queued`)}function Mi(){return W}function Hi(){return ue}function yt(e){return new Promise((t)=>setTimeout(t,e))}async function zo(){let{RedisQueue:e}=await Promise.resolve().then(() => (fe(),pe));return e}export{et as Batch,L as DispatchedBatch,gt as FailedJobNotifier,rr as JOB_ENVELOPE_VERSION,$t as Job,hn as Jobs,Kt as OnQueueEvent,Oe as PendingBatch,qe as QueueEvents,Re as QueueMetrics,je as QueueTester,de as assertEnvelopeSerializable,Yr as cancelJob,bt as checkQueueHealth,Ve as claimDispatchKey,sr as clearEnvelopeWarnings,Zr as clearJobState,yi as configureFailedJobNotifications,O as createEnvelope,bi as createHealthCheckHandler,Pt as createQueueTester,ht as discoverJobs,V as emitQueueEvent,Bi as executeFailedJobs,ri as executeJob,Ct as expectJobToFail,He as fake,Mi as getActiveJobCount,ni as getAllJobs,we as getBatchCallbacks,vi as getFailedJobNotifier,St as getFakeQueue,De as getGlobalMetrics,ti as getJob,Gr as getJobProgress,ie as getQueueEvents,zo as getRedisQueue,hi as getRegisteredJobs,pt as getScheduledJobs,li as getSchedulerStatus,ge as getWorkerTracker,yr as hasDispatchedKey,be as hashPayload,cn as isBatchCancelled,bn as isCircuitOpen,Et as isFaked,Xr as isJobCancelled,Pe as isQuarantined,gi as isQueueHealthy,di as isSchedulerRunning,Hi as isWorkerRunning,dn as job,pn as jobBatch,X as jobRegistry,_n as listCircuitState,At as listDeadLetterJobs,Ut as listQuarantined,Se as moveToDeadLetter,_i as notifyJobFailed,zt as onQueueEvent,xe as parseEnvelope,yn as pauseQueue,Nt as purgeDeadLetterJobs,Ht as quarantineJob,Fn as queuedJobState,at as recordBatchJobCompletion,st as recordBatchJobFailure,wn as recordCircuitFailure,gn as recordCircuitSuccess,vr as recordDispatchedKey,Mt as recordFailureForPoison,Ke as releaseDispatchKey,Le as restore,vn as resumeQueue,Ft as retryDeadLetterJob,Un as retryFailedJob,Fe as runJob,Tt as runTestJob,N as serializeEnvelope,Kr as setJobProgress,qi as startProcessor,ui as startScheduler,Qi as stopProcessor,mt as stopScheduler,ii as toJobOptions,pi as triggerJob,Lt as unquarantineJob,Vt as withEvents};
|
|
31
|
+
\`\`\`${r.exception.slice(0,200)}\`\`\``}}))];if(e.length>10)n.push({type:"section",text:{type:"mrkdwn",text:`_...and ${e.length-10} more failed jobs_`}});try{let r=await fetch(t.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel:t.channel,username:t.username||"Queue Monitor",icon_emoji:t.iconEmoji||":warning:",blocks:n})});if(!r.ok)C.error(`Slack notification failed with status ${r.status}`)}catch(r){C.error("Failed to send Slack notification:",r)}}async sendDiscord(e){let t=this.config.discord,n=e.slice(0,10).map((r)=>({title:`\u274C ${r.name}`,color:15158332,fields:[{name:"Queue",value:r.queue,inline:!0},{name:"Attempts",value:`${r.attempts}/${r.maxAttempts}`,inline:!0},{name:"Failed At",value:r.failedAt.toISOString(),inline:!0},{name:"Exception",value:`\`\`\`${r.exception.slice(0,500)}\`\`\``}],timestamp:r.failedAt.toISOString()}));try{let r=await fetch(t.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t.username||"Queue Monitor",avatar_url:t.avatarUrl,content:`\uD83D\uDEA8 **${e.length} Job(s) Failed**`,embeds:n})});if(!r.ok)C.error(`Discord notification failed with status ${r.status}`)}catch(r){C.error("Failed to send Discord notification:",r)}}async sendWebhook(e){let t=this.config.webhook,n={event:"jobs.failed",timestamp:new Date().toISOString(),count:e.length,jobs:e.map((i)=>({id:i.id,name:i.name,queue:i.queue,attempts:i.attempts,maxAttempts:i.maxAttempts,exception:i.exception,failedAt:i.failedAt.toISOString()}))},r={"Content-Type":"application/json",...t.headers};if(t.secret){let i=JSON.stringify(n),o=await this.generateSignature(i,t.secret);r["X-Signature"]=o}try{let i=await fetch(t.url,{method:"POST",headers:r,body:JSON.stringify(n)});if(!i.ok)C.error(`Webhook notification failed with status ${i.status}`)}catch(i){C.error("Failed to send webhook notification:",i)}}async generateSignature(e,t){let n=new TextEncoder,r=await crypto.subtle.importKey("raw",n.encode(t),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",r,n.encode(e));return Array.from(new Uint8Array(i)).map((o)=>o.toString(16).padStart(2,"0")).join("")}logFailures(e){for(let t of e)C.error(`[Queue] Job "${t.name}" failed on queue "${t.queue}" after ${t.attempts} attempts: ${t.exception}`)}async cleanup(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.pendingBatch.length>0)await this.flushBatch()}}var ke=null;function yi(e){return ke=new gt(e),ke}function vi(){return ke}async function _i(e){if(ke)await ke.notify(e)}B();te();Re();import{err as ki,ok as Fn}from"@stacksjs/error-handling";import{log as b}from"@stacksjs/logging";import ce from"process";import{env as Ji}from"@stacksjs/env";var In=!1;function xi(){if(In)return;In=!0,ce.on("unhandledRejection",(e,t)=>{b.error(`Unhandled Rejection: ${e}`)}),ce.on("uncaughtException",(e)=>{b.error(`Uncaught Exception: ${e.message}`)})}var W=0,ue=!1,j="",Z=new Set,Qn=new Map;function Hn(e){return Z.add(e),e.finally(()=>Z.delete(e)),e}function ji(){let e=ce.env.STACKS_QUEUE_RESERVATION_TTL_SEC,t=e===void 0?Number.NaN:Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:3600}function Pi(){let e=ce.env.STACKS_QUEUE_RETRY_JITTER;if(e===void 0)return 0.2;let t=Number(e);if(!Number.isFinite(t)||t<0)return 0.2;return Math.min(t,1)}function Si(e,t,n=Math.random){if(!(e>0)||t<=0)return e;return Math.round(e+e*t*n())}function Ei(){let e=ce.env.STACKS_QUEUE_SWEEP_INTERVAL_SEC,t=e===void 0?Number.NaN:Number.parseInt(e,10);return(Number.isFinite(t)&&t>0?t:60)*1000}async function Bn(){let e=ji(),t=Math.floor(Date.now()/1000)-e,n=Math.floor(Date.now()/1000);try{let{db:r}=await import("@stacksjs/database"),i=await r.updateTable("jobs").set({reserved_at:null,available_at:n}).where("reserved_at","<=",t).executeTakeFirst(),o=z(i);if(o>0)b.warn(`[queue] Requeued ${o} job(s) whose reservation exceeded the ${e}s TTL - likely victims of a worker crash. Set STACKS_QUEUE_RESERVATION_TTL_SEC to tune.`);return o}catch(r){return b.error("[queue] Reservation sweep failed",{reason:r instanceof Error?r.message:String(r)}),0}}function Ti(e){if(!e||typeof e!=="object")return!1;let t=e.status??e.statusCode;if(typeof t==="number"&&t>=400&&t<500)return!0;let n=e.name;if(typeof n==="string"&&(n==="ValidationError"||n==="ModelNotFoundError"))return!0;return!1}function Ri(){return Ji.QUEUE_DRIVER||"sync"}async function Ci(e,t={}){try{b.info("Starting queue processor..."),xi(),ue=!0,j=`worker-${ce.pid}-${Date.now()}`;let n=t.concurrency||1,r=Ri(),{getWorkerTracker:i,getGlobalMetrics:o}=await Promise.resolve().then(() => (R(),Q));if(o(),i().register(j,e||"default"),r==="redis")return b.info("Using Redis queue driver (bun-queue)"),await Ii(e||"default",n),Fn(void 0);let a;if(e)a=[e];else if(a=await Ln(),a.length===0)a=["default"];return b.info(`Processing queues: ${a.join(", ")}`),await qi(a,n),Fn(void 0)}catch(n){return ue=!1,ki(n)}}async function Ln(){try{let{db:e}=await import("@stacksjs/database"),n=(await e.selectFrom("jobs").select("queue").distinct().execute()).map((r)=>r.queue).filter((r)=>Boolean(r));return n.length>0?n:["default"]}catch{return["default"]}}async function qi(e,t){b.info("Listening for jobs...");let n=e,r=Date.now(),i=1e4,o=Date.now(),a=Ei();await Bn();while(ue)try{let l=Date.now();if(l-r>i){try{let s=await Ln();if(s.length>0)n=s}catch{}r=l}if(l-o>a)await Bn(),o=l;for(let s of n){try{let{isCircuitOpen:c}=await Promise.resolve().then(() => (ve(),Fe));if(await c(s))continue}catch{}let u=[];try{u=await Di(s,t)}catch(c){let d=Date.now(),w=Qn.get(s)??0;if(d-w>60000)Qn.set(s,d),b.error(`[queue] Could not reserve jobs on "${s}" - retrying each cycle:`,c);continue}await Promise.all(u.map(async(c)=>{try{b.info(`Processing job ${c.id} from queue "${s}"`),await Hn($i(c))}catch{b.error(`Unexpected error processing job ${c.id}`)}}))}await yt(1000)}catch{await yt(3000)}}async function Di(e,t){let n=Math.floor(Date.now()/1000),{db:r}=await import("@stacksjs/database"),i=[];for(let o=0;o<t;o++){let a=await r.selectFrom("jobs").where("queue","=",e).whereNull("reserved_at").where("available_at","<=",n).orderBy("id","asc").limit(1).selectAll().executeTakeFirst();if(!a)break;let l=await r.updateTable("jobs").set({reserved_at:n,attempts:Number(a.attempts||0)+1}).where("id","=",a.id).whereNull("reserved_at").executeTakeFirst();if(z(l)>0)i.push(a)}return i}async function $i(e){let t=e.id,n=e.queue||"default";W++;let r=Date.now(),{emitQueueEvent:i,getWorkerTracker:o}=await Promise.resolve().then(() => (R(),Q)),a=o();a.markActive(j);let l;try{l=JSON.parse(e.payload||"{}").jobName}catch{}await i("job:processing",{jobId:String(t),queueName:n,jobName:l});try{let c=JSON.parse(e.payload||"{}").payload?._batchId;if(c){let{isBatchCancelled:d}=await Promise.resolve().then(() => (B(),U));if(await d(c)){b.info(`[Queue] Skipping job ${t} - batch ${c} has been cancelled`),await wt(t),W--,a.markIdle(j);return}}}catch{}let s=null;try{let u=JSON.parse(e.payload||"{}"),c=Ai(u);if(c===void 0)await Mn(u);else await Fi(Mn(u),c*1000,`Job ${t} exceeded ${c}s timeout`)}catch(u){s=u instanceof Error?u:Error(String(u))}if(!s)try{await wt(t),b.info(`[Queue] Job ${t} completed`),a.recordCompletion(j);try{let{recordCircuitSuccess:u}=await Promise.resolve().then(() => (ve(),Fe));await u(e.queue??"default")}catch{}await i("job:completed",{jobId:String(t),queueName:n,duration:Date.now()-r});try{let c=JSON.parse(e.payload||"{}").payload?._batchId;if(c){let{recordBatchJobCompletion:d}=await Promise.resolve().then(() => (B(),U));await d(c)}}catch{}}catch{b.info(`[Queue] Failed to delete completed job ${t}`)}else{let u=s.message;b.info(`[Queue] Job ${t} failed: ${u}`),a.recordFailure(j),await i("job:failed",{jobId:String(t),queueName:n,error:s,duration:Date.now()-r,attemptsMade:(e.attempts||0)+1});let c=1,d={};try{d=JSON.parse(e.payload||"{}"),c=d.options?.tries||1}catch{}let w=(e.attempts||0)+1;if(w>=c){b.info(`[Queue] Job ${t} exceeded max attempts (${w}/${c})`);let f=!1;if(d?._retriedFromFailed===!0)try{let{moveToDeadLetter:g}=await Promise.resolve().then(() => (Se(),It));if(f=await g({queue:e.queue,payload:e.payload,exception:s.stack||s.message},"repeat-failure",2),f)b.info(`[Queue] Job ${t} re-failed after retry - moved to dead_letter_jobs`)}catch{f=!1}if(!f){b.info(`[Queue] Moving job ${t} to failed_jobs`);try{f=await Oi(e,s,{attempts:w,maxAttempts:c,durationMs:Date.now()-r})}catch{f=!1}}if(f)try{await wt(t)}catch{b.info(`[Queue] Failed to delete failed job ${t}`)}else b.error(`[Queue] Job ${t} exhausted its retries but could NOT be persisted to failed_jobs - leaving it in the queue to avoid data loss (the reservation sweep will retry it). Check that the failed_jobs table exists and is writable.`);try{let{recordFailureForPoison:g}=await Promise.resolve().then(() => (Te(),Wt)),{recordCircuitFailure:p}=await Promise.resolve().then(() => (ve(),Fe)),x=d?.jobName??"unknown";await g(x,d?.payload),await p(e.queue??"default")}catch{}try{let g=d.payload?._batchId;if(g){let{recordBatchJobFailure:p}=await Promise.resolve().then(() => (B(),U));await p(g,String(t),s)}}catch{}}else{let f=d.options?.backoff,g=30;if(Array.isArray(f)&&f.length>0){let p=Math.min(w-1,f.length-1);g=f[p]}else if(typeof f==="number"&&f>0)g=f;if(g=Number(g),!Number.isFinite(g)||g<0)g=30;g=Si(g,Pi()),b.info(`[Queue] Job ${t} will be retried in ${g}s (attempt ${w}/${c})`);try{await Ni(t,g),b.info(`[Queue] Job ${t} released for retry`)}catch{b.info(`[Queue] Failed to release job ${t} for retry`)}}}W--,a.markIdle(j)}async function wt(e){let{db:t}=await import("@stacksjs/database");await t.deleteFrom("jobs").where("id","=",e).execute()}async function Ni(e,t=30){let n=Math.floor(Date.now()/1000)+t;b.debug(`Releasing job ${e} for retry at ${n}`);try{let{db:r}=await import("@stacksjs/database");await r.updateTable("jobs").set({reserved_at:null,available_at:n}).where("id","=",e).execute(),b.debug(`Job ${e} released successfully`)}catch{b.error(`Failed to release job ${e}`)}}async function Oi(e,t,n){try{let r=new Date().toISOString().slice(0,19).replace("T"," "),i=crypto.randomUUID(),o=t.stack||t.message,{db:a}=await import("@stacksjs/database");return await a.insertInto("failed_jobs").values({uuid:i,connection:"database",queue:e.queue,payload:e.payload,exception:o,attempts:n.attempts,max_attempts:n.maxAttempts,duration_ms:n.durationMs,failed_at:r}).execute(),!0}catch(r){return b.error("Failed to log failed job:",r),!1}}function Ai(e){if(!e||typeof e!=="object")return;let n=e.options?.timeout;if(typeof n!=="number"||!Number.isFinite(n)||n<=0)return;return n}async function Fi(e,t,n){let r,i=new Promise((o,a)=>{r=setTimeout(()=>a(Error(n)),t)});try{return await Promise.race([e,i])}finally{if(r!==void 0)clearTimeout(r)}}async function Mn(e){let t=xe(e);if(!t.ok)throw Error(`[queue] Cannot deserialize job envelope: ${t.reason}`+(t.detail?` (${t.detail})`:""));let{runJob:n}=await Promise.resolve().then(() => (ye(),Oe));await n(t.envelope.jobName,{payload:t.envelope.payload,traceId:t.envelope.traceId})}async function Ii(e,t){let{RedisQueue:n}=await Promise.resolve().then(() => (fe(),pe)),{queue:r}=await import("@stacksjs/config"),i=r?.connections?.redis;if(!i)throw Error("Redis queue connection is not configured. Check config/queue.ts");let o=new n(e,i),{emitQueueEvent:a,getWorkerTracker:l}=await Promise.resolve().then(() => (R(),Q)),s=l(),u=async(c)=>{W++,s.markActive(j);let d=Date.now(),w=xe(c.data);if(!w.ok){W--,s.markIdle(j),b.error(`[Queue] Skipping Redis job ${c.id} - unparseable envelope: ${w.reason}${w.detail?` (${w.detail})`:""}`);return}let f=w.envelope,g=f.payload?._batchId;if(g)try{let{isBatchCancelled:p}=await Promise.resolve().then(() => (B(),U));if(await p(g)){b.info(`[Queue] Skipping Redis job ${c.id} - batch ${g} has been cancelled`),W--,s.markIdle(j);return}}catch{}await a("job:processing",{jobId:String(c.id),queueName:e});try{let{runJob:p}=await Promise.resolve().then(() => (ye(),Oe));if(await p(f.jobName,{payload:f.payload,traceId:f.traceId}),s.recordCompletion(j),await a("job:completed",{jobId:String(c.id),queueName:e,duration:Date.now()-d}),g)try{let{recordBatchJobCompletion:x}=await Promise.resolve().then(() => (B(),U));await x(g)}catch{}b.info(`[Queue] Redis job ${c.id} completed`)}catch(p){if(s.recordFailure(j),await a("job:failed",{jobId:String(c.id),queueName:e,error:p instanceof Error?p:Error(String(p)),duration:Date.now()-d}),g)try{let{recordBatchJobFailure:x}=await Promise.resolve().then(() => (B(),U));await x(g,String(c.id),p instanceof Error?p:Error(String(p)))}catch{}if(b.error(`[Queue] Redis job ${c.id} failed: ${p}`),Ti(p)){b.info(`[Queue] Redis job ${c.id} hit a non-retryable error - skipping retry`);return}throw p}finally{W--,s.markIdle(j)}};o.process(t,(c)=>Hn(u(c))),b.info(`Listening for Redis jobs on queue "${e}" with concurrency ${t}...`);while(ue)await yt(1000);await o.close()}async function Qi(e={}){let t=e.graceMs??1e4;if(ue=!1,Z.size>0&&t>0){b.info(`[queue] Draining ${Z.size} in-flight job(s) (grace ${t}ms)`);let n=Promise.allSettled([...Z]),r=new Promise((o)=>setTimeout(()=>o("timeout"),t));if(await Promise.race([n.then(()=>"drained"),r])==="timeout"&&Z.size>0)b.warn(`[queue] Drain timed out with ${Z.size} job(s) still active. Their reservations will be reclaimed by the next worker's sweep (Q-2).`)}if(j){let{getWorkerTracker:n}=await Promise.resolve().then(() => (R(),Q));n().unregister(j)}b.info("Queue processor stopped")}async function Bi(){let{db:e}=await import("@stacksjs/database"),t=await e.selectFrom("failed_jobs").selectAll().execute();for(let n of t)await Un(Number(n.id))}async function Un(e){let t=Math.floor(Date.now()/1000),n=new Date().toISOString().slice(0,19).replace("T"," "),{db:r}=await import("@stacksjs/database"),o=(await r.selectFrom("failed_jobs").where("id","=",e).selectAll().execute())[0];if(!o)throw Error(`Failed job ${e} not found`);let a=typeof o.payload==="string"?o.payload:"",l=a;try{let s=JSON.parse(a||"{}");s._retriedFromFailed=!0,l=JSON.stringify(s)}catch{}await r.insertInto("jobs").values({queue:o.queue,payload:l,attempts:0,reserved_at:null,available_at:t,created_at:n}).execute(),await r.deleteFrom("failed_jobs").where("id","=",e).execute(),b.info(`Failed job ${e} has been re-queued`)}function Mi(){return W}function Hi(){return ue}function yt(e){return new Promise((t)=>setTimeout(t,e))}async function zo(){let{RedisQueue:e}=await Promise.resolve().then(() => (fe(),pe));return e}export{et as Batch,L as DispatchedBatch,gt as FailedJobNotifier,rr as JOB_ENVELOPE_VERSION,$t as Job,hn as Jobs,Kt as OnQueueEvent,Ne as PendingBatch,Ce as QueueEvents,qe as QueueMetrics,Je as QueueTester,de as assertEnvelopeSerializable,Yr as cancelJob,bt as checkQueueHealth,Ve as claimDispatchKey,sr as clearEnvelopeWarnings,Zr as clearJobState,yi as configureFailedJobNotifications,N as createEnvelope,bi as createHealthCheckHandler,Et as createQueueTester,ht as discoverJobs,V as emitQueueEvent,Bi as executeFailedJobs,ri as executeJob,Rt as expectJobToFail,He as fake,Mi as getActiveJobCount,ni as getAllJobs,we as getBatchCallbacks,vi as getFailedJobNotifier,Pt as getFakeQueue,De as getGlobalMetrics,ti as getJob,Gr as getJobProgress,ie as getQueueEvents,zo as getRedisQueue,hi as getRegisteredJobs,pt as getScheduledJobs,li as getSchedulerStatus,ge as getWorkerTracker,yr as hasDispatchedKey,be as hashPayload,cn as isBatchCancelled,bn as isCircuitOpen,St as isFaked,Xr as isJobCancelled,Ee as isQuarantined,gi as isQueueHealthy,di as isSchedulerRunning,Hi as isWorkerRunning,dn as job,pn as jobBatch,X as jobRegistry,_n as listCircuitState,Ot as listDeadLetterJobs,Ut as listQuarantined,Pe as moveToDeadLetter,_i as notifyJobFailed,zt as onQueueEvent,xe as parseEnvelope,yn as pauseQueue,Ft as purgeDeadLetterJobs,Ht as quarantineJob,An as queuedJobState,at as recordBatchJobCompletion,st as recordBatchJobFailure,wn as recordCircuitFailure,gn as recordCircuitSuccess,vr as recordDispatchedKey,Mt as recordFailureForPoison,Ke as releaseDispatchKey,Le as restore,vn as resumeQueue,At as retryDeadLetterJob,Un as retryFailedJob,Ae as runJob,Tt as runTestJob,F as serializeEnvelope,Kr as setJobProgress,Ci as startProcessor,ui as startScheduler,Qi as stopProcessor,mt as stopScheduler,ii as toJobOptions,pi as triggerJob,Lt as unquarantineJob,Vt as withEvents};
|
package/dist/job.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Job } from './action';
|
|
2
|
+
export declare function job<const N extends JobName>(name: N, payload?: JobPayload<N>): JobBuilder;
|
|
2
3
|
/**
|
|
3
4
|
* Create a job batch for dispatching multiple jobs together
|
|
4
5
|
*
|
|
@@ -61,35 +62,35 @@ export declare function runJob(name: string, options?: { payload?: any; context?
|
|
|
61
62
|
* @defaultValue
|
|
62
63
|
* ```ts
|
|
63
64
|
* {
|
|
64
|
-
* make: (name:
|
|
65
|
-
* dispatch: (name:
|
|
66
|
-
* dispatchIf: (condition: boolean, name:
|
|
67
|
-
* dispatchUnless: (condition: boolean, name:
|
|
68
|
-
* dispatchNow: (name:
|
|
69
|
-
* dispatchAfter: (seconds: number, name:
|
|
70
|
-
* dispatchOnce: (key: string, name:
|
|
65
|
+
* make: <const N extends JobName>(name: N, payload?: JobPayload<N>) => JobBuilder,
|
|
66
|
+
* dispatch: <const N extends JobName>(name: N, payload?: JobPayload<N>) => Promise<void>,
|
|
67
|
+
* dispatchIf: <const N extends JobName>(condition: boolean, name: N, payload?: JobPayload<N>) => Promise<void>,
|
|
68
|
+
* dispatchUnless: <const N extends JobName>(condition: boolean, name: N, payload?: JobPayload<N>) => Promise<void>,
|
|
69
|
+
* dispatchNow: <const N extends JobName>(name: N, payload?: JobPayload<N>) => Promise<void>,
|
|
70
|
+
* dispatchAfter: <const N extends JobName>(seconds: number, name: N, payload?: JobPayload<N>) => JobBuilder,
|
|
71
|
+
* dispatchOnce: <const N extends JobName>(key: string, name: N, payload?: JobPayload<N>) => Promise<void>
|
|
71
72
|
* }
|
|
72
73
|
* ```
|
|
73
74
|
*/
|
|
74
75
|
export declare const Jobs: {
|
|
75
76
|
/** Construct a builder without dispatching — for chained option calls. */
|
|
76
|
-
make: (name:
|
|
77
|
+
make: <const N extends JobName>(name: N, payload?: JobPayload<N>) => JobBuilder;
|
|
77
78
|
/** Dispatch a job in one call. Equivalent to `job(name, payload).dispatch()`. */
|
|
78
|
-
dispatch: (name:
|
|
79
|
+
dispatch: <const N extends JobName>(name: N, payload?: JobPayload<N>) => Promise<void>;
|
|
79
80
|
/** Dispatch only if `condition` is truthy. */
|
|
80
|
-
dispatchIf: (condition: boolean, name:
|
|
81
|
+
dispatchIf: <const N extends JobName>(condition: boolean, name: N, payload?: JobPayload<N>) => Promise<void>;
|
|
81
82
|
/** Dispatch unless `condition` is truthy. */
|
|
82
|
-
dispatchUnless: (condition: boolean, name:
|
|
83
|
+
dispatchUnless: <const N extends JobName>(condition: boolean, name: N, payload?: JobPayload<N>) => Promise<void>;
|
|
83
84
|
/** Run the job synchronously, bypassing the queue. */
|
|
84
|
-
dispatchNow: (name:
|
|
85
|
+
dispatchNow: <const N extends JobName>(name: N, payload?: JobPayload<N>) => Promise<void>;
|
|
85
86
|
/** Schedule the job to run after `seconds` of delay. */
|
|
86
|
-
dispatchAfter: (seconds: number, name:
|
|
87
|
+
dispatchAfter: <const N extends JobName>(seconds: number, name: N, payload?: JobPayload<N>) => JobBuilder;
|
|
87
88
|
/**
|
|
88
89
|
* Dispatch with an idempotency key in one call
|
|
89
90
|
* (stacksjs/stacks#1872 Q-8). Equivalent to
|
|
90
91
|
* `Jobs.make(name, payload).withIdempotencyKey(key).dispatch()`.
|
|
91
92
|
*/
|
|
92
|
-
dispatchOnce: (key: string, name:
|
|
93
|
+
dispatchOnce: <const N extends JobName>(key: string, name: N, payload?: JobPayload<N>) => Promise<void>
|
|
93
94
|
};
|
|
94
95
|
/**
|
|
95
96
|
* Options for job dispatch
|
|
@@ -155,11 +156,26 @@ declare type TransactionMode = 'auto' | 'after' | 'immediate';
|
|
|
155
156
|
* resolves to nothing.
|
|
156
157
|
*/
|
|
157
158
|
export type JobName = keyof Jobs extends never ? string : keyof Jobs & string;
|
|
159
|
+
/**
|
|
160
|
+
* The payload the named job's handler declares.
|
|
161
|
+
*
|
|
162
|
+
* The jobs registry is a real re-export barrel rather than a name-to-path map,
|
|
163
|
+
* so `Jobs[N]` is the job instance itself and its payload type can be read off
|
|
164
|
+
* it. Dispatching by name therefore gets the same checking as importing the
|
|
165
|
+
* job and calling `.dispatch()` - which matters more here, because by-name is
|
|
166
|
+
* the form that cannot be caught by a rename.
|
|
167
|
+
*
|
|
168
|
+
* Falls back to `unknown` for a job that declares no payload, and for any name
|
|
169
|
+
* while the registry is empty.
|
|
170
|
+
*/
|
|
171
|
+
export type JobPayload<N extends JobName> = N extends keyof Jobs
|
|
172
|
+
? (Jobs[N] extends Job<infer P> ? P : unknown)
|
|
173
|
+
: unknown;
|
|
158
174
|
/**
|
|
159
175
|
* Fluent job builder for file-based jobs
|
|
160
176
|
*/
|
|
161
177
|
declare class JobBuilder {
|
|
162
|
-
constructor(name: string, payload?:
|
|
178
|
+
constructor(name: string, payload?: unknown);
|
|
163
179
|
onQueue(queue: string): this;
|
|
164
180
|
delay(seconds: number): this;
|
|
165
181
|
tries(count: number): this;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/queue",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.73.0",
|
|
6
6
|
"description": "The Stacks Queue system powered by bun-queue.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "bun build.ts",
|
|
61
61
|
"typecheck": "bun tsc --noEmit",
|
|
62
|
+
"typecheck:types": "bun tsc --noEmit -p tsconfig.type-tests.json --pretty false",
|
|
62
63
|
"prepublishOnly": "bun run build"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|