@stacksjs/queue 0.70.45 → 0.70.54

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2023 Open Web Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // @bun
2
- var{defineProperty:_0,getOwnPropertyNames:B1,getOwnPropertyDescriptor:R1}=Object,J1=Object.prototype.hasOwnProperty;function w1(z){return this[z]}var C1=(z)=>{var Z=(g0??=new WeakMap).get(z),$;if(Z)return Z;if(Z=_0({},"__esModule",{value:!0}),z&&typeof z==="object"||typeof z==="function"){for(var U of B1(z))if(!J1.call(Z,U))_0(Z,U,{get:w1.bind(z,U),enumerable:!($=R1(z,U))||$.enumerable})}return g0.set(z,Z),Z},g0;var q1=(z)=>z;function v1(z,Z){this[z]=q1.bind(null,Z)}var n=(z,Z)=>{for(var $ in Z)_0(z,$,{get:Z[$],enumerable:!0,configurable:!0,set:v1.bind(Z,$)})};var K0=(z,Z)=>()=>(z&&(Z=z(z=0)),Z);var W=import.meta.require;var A0={};n(A0,{runJob:()=>c0,restore:()=>H0,isFaked:()=>l0,getFakeQueue:()=>b0,fake:()=>M0,expectJobToFail:()=>d0,createQueueTester:()=>i0,QueueTester:()=>L0});class u0{dispatchedJobs=[];pushedJobs=[];processedJobs=[];failedJobs=[];dispatch(z,Z,$={}){this.dispatchedJobs.push({name:z,data:Z,options:$,dispatchedAt:new Date,queue:$.queue||"default"})}push(z,Z,$={}){this.pushedJobs.push({name:z,data:Z,options:$,dispatchedAt:new Date,queue:$.queue||"default"})}dispatched(z){if(z)return this.dispatchedJobs.filter((Z)=>Z.name===z);return[...this.dispatchedJobs]}pushed(z){if(z)return this.pushedJobs.filter((Z)=>Z.name===z);return[...this.pushedJobs]}assertDispatched(z,Z){let $=this.dispatched(z);if($.length===0)throw Error(`Expected job "${z}" to be dispatched, but it was not.`);if(Z){if($.filter(Z).length===0)throw Error(`Expected job "${z}" to be dispatched matching the callback, but no matching jobs were found.`)}}assertNotDispatched(z){let Z=this.dispatched(z);if(Z.length>0)throw Error(`Expected job "${z}" to not be dispatched, but it was dispatched ${Z.length} time(s).`)}assertDispatchedTimes(z,Z){let $=this.dispatched(z);if($.length!==Z)throw Error(`Expected job "${z}" to be dispatched ${Z} time(s), but it was dispatched ${$.length} time(s).`)}assertNothingDispatched(){if(this.dispatchedJobs.length>0){let z=[...new Set(this.dispatchedJobs.map((Z)=>Z.name))].join(", ");throw Error(`Expected no jobs to be dispatched, but found: ${z}`)}}assertPushed(z,Z){let $=this.pushed(z);if($.length===0)throw Error(`Expected job "${z}" to be pushed, but it was not.`);if(Z){if($.filter(Z).length===0)throw Error(`Expected job "${z}" to be pushed matching the callback, but no matching jobs were found.`)}}assertPushedWithDelay(z,Z){if(this.pushed(z).filter((V)=>V.options.delay===Z).length===0)throw Error(`Expected job "${z}" to be pushed with delay ${Z}ms, but no matching jobs were found.`)}assertPushedOn(z,Z){if(this.pushed(Z).filter((U)=>U.queue===z).length===0)throw Error(`Expected job "${Z}" to be pushed on queue "${z}", but it was not.`)}async processJob(z,Z){let $=this.dispatchedJobs.find((U)=>U.name===z);if(!$)throw Error(`No dispatched job found with name "${z}"`);try{await Z($.data),this.processedJobs.push($)}catch(U){throw this.failedJobs.push({job:$,error:U}),U}}processed(z){if(z)return this.processedJobs.filter((Z)=>Z.name===z);return[...this.processedJobs]}failed(z){if(z)return this.failedJobs.filter((Z)=>Z.job.name===z);return[...this.failedJobs]}reset(){this.dispatchedJobs=[],this.pushedJobs=[],this.processedJobs=[],this.failedJobs=[]}}function M0(){return s=new u0,s}function b0(){return s}function l0(){return s!==null}function H0(){s=null}class L0{queue;constructor(){this.queue=M0()}dispatch(z,Z,$={}){return this.queue.dispatch(z,Z,$),this}push(z,Z,$={}){return this.queue.push(z,Z,$),this}assertDispatched(z,Z){return this.queue.assertDispatched(z,Z),this}assertNotDispatched(z){return this.queue.assertNotDispatched(z),this}assertDispatchedTimes(z,Z){return this.queue.assertDispatchedTimes(z,Z),this}assertNothingDispatched(){return this.queue.assertNothingDispatched(),this}dispatched(z){return this.queue.dispatched(z)}reset(){return this.queue.reset(),this}cleanup(){H0()}}function i0(){return new L0}async function c0(z,Z){return await z.handle(Z)}async function d0(z,Z,$){try{throw await z.handle(Z),Error("Expected job to fail, but it succeeded")}catch(U){if(U.message==="Expected job to fail, but it succeeded")throw U;if($){let V=U.message;if(typeof $==="string"){if(!V.includes($))throw Error(`Expected error to contain "${$}", got "${V}"`)}else if(!$.test(V))throw Error(`Expected error to match ${$}, got "${V}"`)}return U}}var s=null;var r={};n(r,{setQueueManager:()=>u1,getQueueManager:()=>j1,dispatchSync:()=>p1,dispatchAfter:()=>h1,dispatch:()=>m1,createRedisDispatcher:()=>b1,chain:()=>f1,batch:()=>I1,StacksQueueManager:()=>n0,RedisQueue:()=>g,RedisJob:()=>s0,QueueManager:()=>g1});import{Queue as y1,batch as I1,chain as f1,dispatch as m1,dispatchAfter as h1,dispatchSync as p1,getQueueManager as j1,QueueManager as g1,setQueueManager as u1}from"bun-queue";import{log as x0}from"@stacksjs/logging";class g{queue;config;isProcessing=!1;constructor(z,Z){this.config=Z,this.queue=new y1(z,{driver:"redis",prefix:Z.prefix,redis:Z.redis?{url:Z.redis.url||this.buildRedisUrl(Z.redis)}:void 0,defaultJobOptions:Z.defaultJobOptions?{delay:Z.defaultJobOptions.delay,attempts:Z.defaultJobOptions.attempts,backoff:Z.defaultJobOptions.backoff,removeOnComplete:Z.defaultJobOptions.removeOnComplete,removeOnFail:Z.defaultJobOptions.removeOnFail,priority:Z.defaultJobOptions.priority,lifo:Z.defaultJobOptions.lifo,timeout:Z.defaultJobOptions.timeout,jobId:Z.defaultJobOptions.jobId,dependsOn:Z.defaultJobOptions.dependsOn,keepJobs:Z.defaultJobOptions.keepJobs,deadLetter:Z.defaultJobOptions.deadLetter}:void 0,limiter:Z.limiter,metrics:Z.metrics,stalledJobCheckInterval:Z.stalledJobCheckInterval,maxStalledJobRetries:Z.maxStalledJobRetries,distributedLock:Z.distributedLock,defaultDeadLetterOptions:Z.defaultDeadLetterOptions,horizontalScaling:Z.horizontalScaling,logLevel:Z.logLevel}),x0.debug(`Redis queue "${z}" initialized`)}buildRedisUrl(z){let Z=z.host||"localhost",$=z.port||6379,U=z.password?`:${encodeURIComponent(z.password)}@`:"",V=Number.isFinite(z.db)&&z.db>=0?z.db:0;return`redis://${U}${Z}:${$}/${V}`}async add(z,Z){let $={delay:Z?.delay?Z.delay*1000:void 0,attempts:Z?.maxTries,priority:Z?.priority,timeout:Z?.timeout?Z.timeout*1000:void 0,backoff:Array.isArray(Z?.backoff)?{type:"fixed",delay:(Z.backoff[0]||1)*1000}:Z?.backoff};return this.queue.add(z,$)}process(z,Z){if(this.isProcessing){x0.warn("Queue is already processing");return}this.isProcessing=!0,this.queue.process(z,Z),x0.info(`Started processing queue with concurrency ${z}`)}async getJob(z){return this.queue.getJob(z)}async getJobs(z){return this.queue.getJobs(z)}async getJobCounts(){return this.queue.getJobCounts()}async removeJob(z){return this.queue.removeJob(z)}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(z){return this.queue.scheduleCron({cron:z.cron,data:z.data,tz:z.tz,name:z.name})}async unscheduleCron(z){return this.queue.unscheduleCron(z)}async getDeadLetterJobs(){return this.queue.getDeadLetterJobs()}async republishDeadLetterJob(z){return this.queue.republishDeadLetterJob(z)}async clearDeadLetterQueue(){return this.queue.clearDeadLetterQueue()}async bulkRemove(z){return this.queue.bulkRemove(z)}async getClusterInfo(){return this.queue.getClusterInfo()}isLeader(){return this.queue.isLeader()}getQueue(){return this.queue}on(z,Z){this.queue.events.on(z,Z)}}class n0{config;queues=new Map;defaultConnection="default";constructor(z){this.config=z;for(let[Z,$]of Object.entries(z))if($.driver==="redis")this.queues.set(Z,new g(Z,$))}queue(z){let Z=z||this.defaultConnection,$=this.queues.get(Z);if(!$){let U=this.config[Z];if(!U)throw Error(`Queue "${Z}" not configured`);$=new g(Z,U),this.queues.set(Z,$)}return $}setDefaultConnection(z){this.defaultConnection=z}async closeAll(){let z=Array.from(this.queues.values()).map((Z)=>Z.close());await Promise.all(z),this.queues.clear()}}function b1(z,Z){let $=new g(z,Z);return async(U,V)=>{return $.add(U,V)}}class s0{data;options={};queue;constructor(z,Z,$){this.data=$;this.queue=new g(z,Z)}async dispatch(){await this.queue.add(this.data,this.options)}async dispatchNow(){await this.queue.add(this.data,{...this.options,immediate:!0})}delay(z){return this.options.delay=z,this}afterResponse(){return this.options.afterResponse=!0,this}chain(z){return this.options.chainedJobs=z,this}onQueue(z){return this.options.queue=z,this}priority(z){return this.options.priority=z,this}tries(z){return this.options.maxTries=z,this}timeout(z){return this.options.timeout=z,this}backoff(z){return this.options.backoff=z,this}}var t=()=>{};var C={};n(C,{withEvents:()=>o0,onQueueEvent:()=>a0,getWorkerTracker:()=>a,getQueueEvents:()=>u,getGlobalMetrics:()=>X0,emitQueueEvent:()=>f,QueueMetrics:()=>G0,QueueEvents:()=>Y0,OnQueueEvent:()=>e0});import{log as J}from"@stacksjs/logging";class Y0{handlers=new Map;wildcardHandlers=new Set;on(z,Z){if(!this.handlers.has(z))this.handlers.set(z,new Set);return this.handlers.get(z).add(Z),()=>{this.handlers.get(z)?.delete(Z)}}onAny(z){return this.wildcardHandlers.add(z),()=>{this.wildcardHandlers.delete(z)}}once(z,Z){let $=async(U)=>{this.handlers.get(z)?.delete($),await Z(U)};return this.on(z,$)}async emit(z,Z){let $={...Z,timestamp:Date.now()};this.logEvent(z,$);let U=this.handlers.get(z);if(U)for(let V of U)try{await V($)}catch(K){J.error(`Error in queue event handler for ${z}:`,K)}for(let V of this.wildcardHandlers)try{await V(z,$)}catch(K){J.error("Error in wildcard queue event handler:",K)}}logEvent(z,Z){let $=Z.jobId?`[${Z.jobId}]`:"",U=Z.queueName?`on ${Z.queueName}`:"";switch(z){case"job:added":J.debug(`Job added ${$} ${U}`);break;case"job:processing":J.debug(`Job processing ${$} ${U}`);break;case"job:completed":J.info(`Job completed ${$} ${U} in ${Z.duration}ms`);break;case"job:failed":J.error(`Job failed ${$} ${U}:`,Z.error);break;case"job:retrying":J.warn(`Job retrying ${$} ${U} (attempt ${Z.attemptsMade})`);break;case"job:stalled":J.warn(`Job stalled ${$} ${U}`);break;case"queue:error":J.error(`Queue error ${U}:`,Z.error);break}}off(z){this.handlers.delete(z)}removeAllListeners(){this.handlers.clear(),this.wildcardHandlers.clear()}}function u(){if(!F0)F0=new Y0;return F0}function a0(z,Z){let $=u();if(z==="*")return $.onAny(Z);return $.on(z,Z)}function f(z,Z){return u().emit(z,Z)}function o0(z,Z){return async(...$)=>{let U=$[0]?.id||"unknown",V=Date.now();await f("job:processing",{jobId:U,queueName:z,data:$[0]?.data});try{let K=await Z(...$);return await f("job:completed",{jobId:U,queueName:z,result:K,duration:Date.now()-V}),K}catch(K){throw await f("job:failed",{jobId:U,queueName:z,error:K,duration:Date.now()-V}),K}}}function e0(z){return function(Z,$,U){let V=U.value;return u().on(z,V),U}}class G0{jobCounts={added:0,completed:0,failed:0,processing:0};completions=[];errors=[];unsubscribe=[];constructor(){this.setupListeners()}setupListeners(){let z=u();this.unsubscribe.push(z.on("job:added",()=>{this.jobCounts.added++}),z.on("job:processing",()=>{this.jobCounts.processing++}),z.on("job:completed",(Z)=>{this.jobCounts.completed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1);let $=Z.duration||0;if(this.completions.push({timestamp:Date.now(),duration:$}),this.completions.length>1000)this.completions.shift()}),z.on("job:failed",(Z)=>{if(this.jobCounts.failed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1),Z.error){if(this.errors.push({error:Z.error,timestamp:Date.now()}),this.errors.length>100)this.errors.shift()}}))}getThroughputPerMinute(){let z=Date.now()-60000;return this.completions.filter(($)=>$.timestamp>=z).length}getAverageProcessingTime(){let z=Date.now()-60000,Z=this.completions.filter(($)=>$.timestamp>=z);if(Z.length===0)return 0;return Z.reduce(($,U)=>$+U.duration,0)/Z.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((z)=>z()),this.unsubscribe=[]}}function X0(){if(!T0)T0=new G0;return T0}class z1{workers=new Map;register(z,Z){this.workers.set(z,{id:z,status:"idle",queue:Z,processedCount:0,failedCount:0,lastActivityAt:new Date().toISOString(),startedAt:new Date().toISOString()})}markActive(z){let Z=this.workers.get(z);if(Z)Z.status="active",Z.lastActivityAt=new Date().toISOString()}markIdle(z){let Z=this.workers.get(z);if(Z)Z.status="idle",Z.lastActivityAt=new Date().toISOString()}recordCompletion(z){let Z=this.workers.get(z);if(Z)Z.processedCount++,Z.lastActivityAt=new Date().toISOString()}recordFailure(z){let Z=this.workers.get(z);if(Z)Z.failedCount++,Z.lastActivityAt=new Date().toISOString()}unregister(z){let Z=this.workers.get(z);if(Z)Z.status="stopped"}getAll(){return Array.from(this.workers.values())}clear(){this.workers.clear()}}function a(){return i1}var F0=null,T0=null,i1;var R=K0(()=>{i1=new z1});var k={};n(k,{recordBatchJobFailure:()=>B0,recordBatchJobCompletion:()=>N0,isBatchCancelled:()=>X1,getBatchCallbacks:()=>o,PendingBatch:()=>W0,DispatchedBatch:()=>q,Batch:()=>P0});import{log as E}from"@stacksjs/logging";import{env as c1}from"@stacksjs/env";function v(){return c1.QUEUE_DRIVER||"sync"}class W0{jobs;options={thenCallbacks:[],catchCallbacks:[],finallyCallbacks:[],progressCallbacks:[]};constructor(z){this.jobs=z.map((Z)=>("job"in Z)?Z:{job:Z})}name(z){return this.options.name=z,this}onQueue(z){return this.options.queue=z,this}allowFailures(){return this.options.allowFailures=!0,this}then(z){return this.options.thenCallbacks.push(z),this}catch(z){return this.options.catchCallbacks.push(z),this}finally(z){return this.options.finallyCallbacks.push(z),this}progress(z){return this.options.progressCallbacks.push(z),this}async dispatch(){let z=crypto.randomUUID(),Z=this.jobs.length;if(Z===0)throw Error("Cannot dispatch an empty batch");let $=v();await n1({id:z,name:this.options.name||"",total_jobs:Z,pending_jobs:Z,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}),d1(z,this.options);try{let{emitQueueEvent:U}=await Promise.resolve().then(() => (R(),C));await U("batch:added",{jobId:z,data:{name:this.options.name,totalJobs:Z}})}catch{}for(let U=0;U<this.jobs.length;U++){let V=this.jobs[U];if(!V)continue;let{job:K,payload:L}=V,G={...L,_batchId:z,_batchIndex:U};if(this.options.queue&&!K.queue)K.queue=this.options.queue;if($==="sync")try{await K.dispatchNow(G),await N0(z)}catch(Y){await B0(z,`${z}:${U}`,Y)}else await K.dispatch(G)}return E.info(`[Batch] Dispatched batch "${this.options.name||z}" with ${Z} jobs`),new q(z)}getJobs(){return[...this.jobs]}getOptions(){return this.options}}class q{id;constructor(z){this.id=z}async fresh(){return b(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 z=await this.fresh();if(!z)return 0;return z.total_jobs-z.pending_jobs}async progress(){let z=await this.fresh();if(!z||z.total_jobs===0)return 0;let Z=z.total_jobs-z.pending_jobs;return Math.round(Z/z.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 z=await this.fresh();if(!z)return[];try{return JSON.parse(z.failed_job_ids||"[]")}catch{return[]}}async cancel(){if(v()==="redis")await z9(this.id);else await a1(this.id);E.info(`[Batch] Cancelled batch ${this.id}`);let Z=o(this.id);if(Z)for(let $ of Z.finallyCallbacks)try{await $(this)}catch(U){E.error(`[Batch] Error in finally callback for batch ${this.id}:`,U)}}async add(z){let Z=await this.fresh();if(!Z)throw Error(`Batch ${this.id} not found`);if(Z.cancelled_at)throw Error(`Batch ${this.id} has been cancelled`);if(Z.finished_at)throw Error(`Batch ${this.id} has already finished`);let $=z.map((L)=>("job"in L)?L:{job:L}),U=Z.total_jobs+$.length,V=Z.pending_jobs+$.length;await S0(this.id,{total_jobs:U,pending_jobs:V});let K=JSON.parse(Z.options||"{}");for(let L=0;L<$.length;L++){let G=$[L];if(!G)continue;let{job:Y,payload:X}=G,M={...X,_batchId:this.id,_batchIndex:Z.total_jobs+L};if(K.queue&&!Y.queue)Y.queue=K.queue;await Y.dispatch(M)}E.info(`[Batch] Added ${$.length} jobs to batch ${this.id}`)}async delete(){await r1(this.id),O0(this.id)}}class P0{static create(z){return new W0(z)}static async find(z){if(!await b(z))return null;return new q(z)}static async all(){return(await s1()).map((Z)=>new q(Z.id))}static async prune(z=24){return t1(z)}}function d1(z,Z){D0.set(z,Z)}function o(z){return D0.get(z)}function O0(z){D0.delete(z)}async function n1(z){if(v()==="redis")await o1(z);else await Z1(z)}async function b(z){if(v()==="redis")return e1(z);return $1(z)}async function s1(){if(v()==="redis")return L1();return U1()}async function S0(z,Z){if(v()==="redis")await Y1(z,Z);else await Q0(z,Z)}async function r1(z){if(v()==="redis")await G1(z);else await V1(z)}async function t1(z){if(v()==="redis")return Z9(z);return K1(z)}async function Z1(z){let{db:Z}=await import("@stacksjs/database");await Z.insertInto("job_batches").values({id:z.id,name:z.name,total_jobs:z.total_jobs,pending_jobs:z.pending_jobs,failed_jobs:z.failed_jobs,failed_job_ids:z.failed_job_ids,options:z.options,cancelled_at:z.cancelled_at,created_at:z.created_at,finished_at:z.finished_at}).execute()}async function $1(z){let{db:Z}=await import("@stacksjs/database");return await Z.selectFrom("job_batches").where("id","=",z).selectAll().executeTakeFirst()||null}async function U1(){let{db:z}=await import("@stacksjs/database");return await z.selectFrom("job_batches").selectAll().orderBy("created_at","desc").execute()}async function Q0(z,Z){let{db:$}=await import("@stacksjs/database");await $.updateTable("job_batches").set(Z).where("id","=",z).execute()}async function V1(z){let{db:Z}=await import("@stacksjs/database");await Z.deleteFrom("job_batches").where("id","=",z).execute()}async function a1(z){await Q0(z,{cancelled_at:new Date().toISOString().slice(0,19).replace("T"," "),finished_at:new Date().toISOString().slice(0,19).replace("T"," ")})}async function K1(z){let{db:Z}=await import("@stacksjs/database"),$=new Date(Date.now()-z*60*60*1000).toISOString().slice(0,19).replace("T"," "),U=await Z.deleteFrom("job_batches").where("finished_at","is not",null).where("finished_at","<",$).executeTakeFirst();return Number(U?.numDeletedRows??0)}async function o1(z){try{let{createClient:Z}=await import("redis"),{queue:$}=await import("@stacksjs/config"),U=$?.connections?.redis?.redis,V=U?.url||`redis://${U?.host||"localhost"}:${U?.port||6379}`,K=Z({url:V});await K.connect();let L=`${e}${z.id}`;await K.hSet(L,{id:z.id,name:z.name,total_jobs:String(z.total_jobs),pending_jobs:String(z.pending_jobs),failed_jobs:String(z.failed_jobs),failed_job_ids:z.failed_job_ids,options:z.options,cancelled_at:z.cancelled_at||"",created_at:z.created_at,finished_at:z.finished_at||""}),await K.sAdd(E0,z.id),await K.quit()}catch{await Z1(z)}}async function e1(z){try{let{createClient:Z}=await import("redis"),{queue:$}=await import("@stacksjs/config"),U=$?.connections?.redis?.redis,V=U?.url||`redis://${U?.host||"localhost"}:${U?.port||6379}`,K=Z({url:V});await K.connect();let L=`${e}${z}`,G=await K.hGetAll(L);if(await K.quit(),!G||!G.id)return null;return{id:G.id,name:G.name,total_jobs:Number(G.total_jobs),pending_jobs:Number(G.pending_jobs),failed_jobs:Number(G.failed_jobs),failed_job_ids:G.failed_job_ids,options:G.options,cancelled_at:G.cancelled_at||null,created_at:G.created_at,finished_at:G.finished_at||null}}catch{return $1(z)}}async function L1(){try{let{createClient:z}=await import("redis"),{queue:Z}=await import("@stacksjs/config"),$=Z?.connections?.redis?.redis,U=$?.url||`redis://${$?.host||"localhost"}:${$?.port||6379}`,V=z({url:U});await V.connect();let K=await V.sMembers(E0),L=[];for(let G of K){let Y=`${e}${G}`,X=await V.hGetAll(Y);if(X?.id)L.push({id:X.id,name:X.name,total_jobs:Number(X.total_jobs),pending_jobs:Number(X.pending_jobs),failed_jobs:Number(X.failed_jobs),failed_job_ids:X.failed_job_ids,options:X.options,cancelled_at:X.cancelled_at||null,created_at:X.created_at,finished_at:X.finished_at||null})}return await V.quit(),L}catch{return U1()}}async function Y1(z,Z){try{let{createClient:$}=await import("redis"),{queue:U}=await import("@stacksjs/config"),V=U?.connections?.redis?.redis,K=V?.url||`redis://${V?.host||"localhost"}:${V?.port||6379}`,L=$({url:K});await L.connect();let G=`${e}${z}`,Y={};for(let[X,M]of Object.entries(Z))Y[X]=M===null?"":String(M);await L.hSet(G,Y),await L.quit()}catch{await Q0(z,Z)}}async function G1(z){try{let{createClient:Z}=await import("redis"),{queue:$}=await import("@stacksjs/config"),U=$?.connections?.redis?.redis,V=U?.url||`redis://${U?.host||"localhost"}:${U?.port||6379}`,K=Z({url:V});await K.connect(),await K.del(`${e}${z}`),await K.sRem(E0,z),await K.quit()}catch{await V1(z)}}async function z9(z){let Z=new Date().toISOString().slice(0,19).replace("T"," ");await Y1(z,{cancelled_at:Z,finished_at:Z})}async function Z9(z){try{let Z=await L1(),$=Date.now()-z*60*60*1000,U=0;for(let V of Z)if(V.finished_at){if(new Date(V.finished_at).getTime()<$)await G1(V.id),U++}return U}catch{return K1(z)}}async function N0(z){try{let{db:L,sql:G}=await import("@stacksjs/database"),Y=await L.updateTable("job_batches").set({pending_jobs:G`GREATEST(pending_jobs - 1, 0)`}).where("id","=",z).where("pending_jobs",">",0).execute()}catch(L){E.debug(`[Batch] Atomic decrement unavailable, falling back to read-modify-write: ${L.message}`)}let Z=await b(z);if(!Z)return;let $=Math.max(0,Z.pending_jobs-1),U={pending_jobs:$};if($===0)U.finished_at=new Date().toISOString().slice(0,19).replace("T"," ");await S0(z,U);let V=o(z),K=new q(z);if(V)for(let L of V.progressCallbacks)try{await L(K)}catch(G){E.error(`[Batch] Error in progress callback for batch ${z}:`,G)}if($===0){try{let{emitQueueEvent:L}=await Promise.resolve().then(() => (R(),C));await L("batch:completed",{jobId:z})}catch{}if(V){let L=await b(z);if(!L){O0(z);return}let G=JSON.parse(L.options||"{}");if(!((L.failed_jobs||0)>0)||G.allowFailures)for(let X of V.thenCallbacks)try{await X(K)}catch(M){E.error(`[Batch] Error in then callback for batch ${z}:`,M)}for(let X of V.finallyCallbacks)try{await X(K)}catch(M){E.error(`[Batch] Error in finally callback for batch ${z}:`,M)}O0(z)}E.info(`[Batch] Batch ${z} finished`)}}async function B0(z,Z,$){let U=await b(z);if(!U)return;let V=[];try{V=JSON.parse(U.failed_job_ids||"[]")}catch{V=[]}V.push(Z);let K=Math.max(0,U.pending_jobs-1),L=U.failed_jobs+1,G={pending_jobs:K,failed_jobs:L,failed_job_ids:JSON.stringify(V)},Y=JSON.parse(U.options||"{}");if(!Y.allowFailures)G.cancelled_at=new Date().toISOString().slice(0,19).replace("T"," "),G.finished_at=new Date().toISOString().slice(0,19).replace("T"," ");else if(K===0)G.finished_at=new Date().toISOString().slice(0,19).replace("T"," ");await S0(z,G);let X=o(z),M=new q(z);if(X)for(let x of X.catchCallbacks)try{await x(M,$)}catch(O){E.error(`[Batch] Error in catch callback for batch ${z}:`,O)}try{let{emitQueueEvent:x}=await Promise.resolve().then(() => (R(),C));await x("batch:failed",{jobId:z,error:$})}catch{}if((!Y.allowFailures||K===0)&&X){for(let x of X.finallyCallbacks)try{await x(M)}catch(O){E.error(`[Batch] Error in finally callback for batch ${z}:`,O)}if(Y.allowFailures&&K===0)for(let x of X.thenCallbacks)try{await x(M)}catch(O){E.error(`[Batch] Error in then callback for batch ${z}:`,O)}O0(z),E.info(`[Batch] Batch ${z} finished with ${L} failure(s)`)}}async function X1(z){return(await b(z))?.cancelled_at!==null}var D0,e="stacks:batch:",E0="stacks:batches";var w=K0(()=>{D0=new Map});var Z0={};n(Z0,{runJob:()=>z0,jobBatch:()=>_1,job:()=>O1,Jobs:()=>W1});import{appPath as $9}from"@stacksjs/path";import{env as U9}from"@stacksjs/env";function V9(){return U9.QUEUE_DRIVER||"sync"}class y{name;payload;options={};constructor(z,Z){this.name=z;this.payload=Z}onQueue(z){return this.options.queue=z,this}delay(z){return this.options.delay=z,this}tries(z){return this.options.tries=z,this}timeout(z){return this.options.timeout=z,this}backoff(z){return this.options.backoff=z,this}withContext(z){return this.options.context=z,this}async dispatch(){let{isFaked:z,getFakeQueue:Z}=await Promise.resolve().then(() => A0);if(z()){Z()?.dispatch(this.name,this.payload,this.options);return}let $=V9();if($==="database")await this.dispatchToDatabase();else if($==="redis")await this.dispatchToRedis();else if($==="sync")await z0(this.name,{payload:this.payload,context:this.options.context});else await z0(this.name,{payload:this.payload,context:this.options.context})}async dispatchIf(z){if(z)await this.dispatch()}async dispatchUnless(z){if(!z)await this.dispatch()}async dispatchToDatabase(){let z=Math.floor(Date.now()/1000),Z=this.options.delay?z+this.options.delay:z,$={jobName:this.name,payload:this.payload,options:this.options},{db:U}=await import("@stacksjs/database");await U.insertInto("jobs").values({queue:this.options.queue||"default",payload:JSON.stringify($),attempts:0,reserved_at:null,available_at:Z,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(){let{RedisQueue:z}=await Promise.resolve().then(() => (t(),r)),{queue:Z}=await import("@stacksjs/config"),$=Z?.connections?.redis;if(!$)throw Error("Redis queue connection is not configured. Check config/queue.ts");await new z(this.options.queue||"default",$).add({jobName:this.name,payload:this.payload},{delay:this.options.delay,maxTries:this.options.tries,timeout:this.options.timeout,backoff:this.options.backoff})}async dispatchNow(){await z0(this.name,{payload:this.payload,context:this.options.context})}}function O1(z,Z){return new y(z,Z)}function _1(z){let{PendingBatch:Z}=(w(),C1(k));return new Z(z)}async function z0(z,Z={}){let{withTraceId:$}=await import("@stacksjs/router"),U=Z.traceId??`job:${z}:${Math.random().toString(36).slice(2,10)}`;await $(U,async()=>{let L=(await import($9(`Jobs/${z}.ts`))).default;if(!L)throw Error(`Job ${z} does not export a default`);if(typeof L.handle==="function")await L.handle(Z.payload);else if(typeof L.action==="string"){let{runAction:G}=await import("@stacksjs/actions");await G(L.action)}else if(typeof L.action==="function")await L.action();else if(typeof L==="function")await L(Z.payload,Z.context);else throw Error(`Job ${z} does not have a valid handler`)})}var W1;var l=K0(()=>{W1={make(z,Z){return new y(z,Z)},async dispatch(z,Z){await new y(z,Z).dispatch()},async dispatchIf(z,Z,$){if(z)await new y(Z,$).dispatch()},async dispatchUnless(z,Z,$){if(!z)await new y(Z,$).dispatch()},async dispatchNow(z,Z){await new y(z,Z).dispatchNow()},dispatchAfter(z,Z,$){return new y(Z,$).delay(z)}}});import{env as l1}from"@stacksjs/env";function r0(){return l1.QUEUE_DRIVER||"sync"}class t0{name;description;action;handle;queue;rate;tries;timeout;backoff;backoffConfig;enabled;constructor(z){this.name=z.name,this.description=z.description,this.handle=z.handle,this.queue=z.queue,this.rate=z.rate,this.action=z.action,this.tries=z.tries,this.timeout=z.timeout,this.backoff=z.backoff,this.backoffConfig=z.backoffConfig,this.enabled=z.enabled}async dispatch(z){let{isFaked:Z,getFakeQueue:$}=await Promise.resolve().then(() => A0);if(Z()){$()?.dispatch(this.name||"UnknownJob",z,{queue:this.queue,tries:this.tries,timeout:this.timeout});return}let U=r0();if(U==="sync")return this.dispatchNow(z);if(U==="redis")return this.dispatchToRedis(z);if(U==="database")return this.dispatchToDatabase(z);return this.dispatchNow(z)}async dispatchIf(z,Z){if(z)return this.dispatch(Z)}async dispatchUnless(z,Z){if(!z)return this.dispatch(Z)}async dispatchAfter(z,Z){let $=r0();if($==="redis")return this.dispatchToRedis(Z,{delay:z});if($==="database")return this.dispatchToDatabase(Z,{delay:z});return await new Promise((U)=>setTimeout(U,z*1000)),await this.dispatchNow(Z)}async dispatchNow(z){if(typeof this.handle==="function")await this.handle(z);else if(typeof this.action==="string"){let{runAction:Z}=await import("@stacksjs/actions");await Z(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(z,Z){let $=Math.floor(Date.now()/1000),U=Z?.delay?$+Z.delay:$,V={jobName:this.name,payload:z,options:{queue:this.queue,tries:this.tries,timeout:this.timeout,backoff:this.backoff}},{db:K}=await import("@stacksjs/database");await K.insertInto("jobs").values({queue:this.queue||"default",payload:JSON.stringify(V),attempts:0,reserved_at:null,available_at:U,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(z,Z){let{RedisQueue:$}=await Promise.resolve().then(() => (t(),r)),{queue:U}=await import("@stacksjs/config"),V=U?.connections?.redis;if(!V)throw Error("Redis queue connection is not configured. Check config/queue.ts");await new $(this.queue||"default",V).add({jobName:this.name,payload:z},{delay:Z?.delay,maxTries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0})}}l();var R0=(z)=>`__job_progress__:${z}`,J0=(z)=>`__job_cancel__:${z}`;async function K9(z,Z,$){let{cache:U}=await import("@stacksjs/cache"),V=Number.isFinite(Z)?Math.max(0,Math.min(100,Z*(Z<=1?100:1))):0;await U.set(R0(z),{percent:V,message:$,updatedAt:Date.now()},3600)}async function L9(z){let{cache:Z}=await import("@stacksjs/cache");return await Z.get(R0(z))??null}async function Y9(z){let{cache:Z}=await import("@stacksjs/cache");await Z.set(J0(z),1,3600)}async function G9(z){let{cache:Z}=await import("@stacksjs/cache");return Boolean(await Z.get(J0(z)))}async function X9(z){let{cache:Z}=await import("@stacksjs/cache");await Promise.all([Z.del(R0(z)),Z.del(J0(z))])}import{log as i}from"@stacksjs/logging";import*as M1 from"@stacksjs/path";class H1{jobs=new Map;initialized=!1;register(z){this.jobs.set(z.name,z),i.debug(`Registered job: ${z.name} (${z.type})`)}get(z){return this.jobs.get(z)}all(){return Array.from(this.jobs.values())}byQueue(z){return this.all().filter((Z)=>Z.config.queue===z)}scheduled(){return this.all().filter((z)=>z.config.rate||z.config.schedule)}has(z){return this.jobs.has(z)}clear(){this.jobs.clear(),this.initialized=!1}setInitialized(z){this.initialized=z}isInitialized(){return this.initialized}}var m=new H1;async function w0(z){let Z=z||M1.userJobsPath(),$=[];try{let U=new Bun.Glob("**/*.{ts,js}"),V={cwd:Z,onlyFiles:!0,absolute:!0};for await(let K of U.scan(V)){if(K.includes(".test.")||K.includes(".spec.")||K.endsWith("index.ts")||K.endsWith("index.js"))continue;try{let L=await O9(K);if(L)$.push(L),m.register(L)}catch(L){i.warn(`Failed to load job from ${K}: ${L.message}`)}}return m.setInitialized(!0),i.info(`Discovered ${$.length} jobs from ${Z}`),$}catch(U){return i.error(`Failed to discover jobs: ${U.message}`),[]}}async function O9(z){try{let Z=await import(z),$=z.split("/").pop()?.replace(/\.(ts|js)$/,"")||"UnknownJob";if(Z.default&&typeof Z.default==="function"){let U=Z.default;if(typeof U.handle==="function"||typeof U.prototype?.handle==="function"){let V=U.config||{};return{name:V.name||$,path:z,config:{name:V.name||$,description:V.description,queue:V.queue||"default",tries:V.retries||3,timeout:V.timeout,withoutOverlapping:V.withoutOverlapping,schedule:V.schedule,retryAfter:V.retryAfter},type:"class",module:U}}}if(Z.default&&typeof Z.default==="object"){let U=Z.default;if(typeof U.handle==="function"||typeof U.action==="string")return{name:U.name||$,path:z,config:{name:U.name||$,description:U.description,queue:U.queue||"default",tries:U.tries||3,backoff:U.backoff,rate:U.rate,timeout:U.timeout||U.timeOut,backoffConfig:U.backoffConfig},type:"function",module:U}}return null}catch(Z){return i.debug(`Could not load job from ${z}: ${Z.message}`),null}}function W9(z){return m.get(z)}function _9(){return m.all()}function C0(){return m.scheduled()}async function M9(z,Z){let $=m.get(z);if(!$)throw Error(`Job "${z}" not found. Did you run discoverJobs()?`);try{if($.type==="class"){if(typeof $.module.handle==="function")return await $.module.handle(Z);return await new $.module().handle(Z)}else{if(typeof $.module.handle==="function")return await $.module.handle(Z);throw Error(`Job "${z}" does not have a handle method`)}}catch(U){throw i.error(`Failed to execute job "${z}": ${U.message}`),U}}function H9(z){let Z=z.config;return{name:Z.name,queue:Z.queue,tries:Z.tries,backoff:Z.backoff,timeout:Z.timeout,backoffConfig:Z.backoffConfig,rate:Z.rate}}import{log as D}from"@stacksjs/logging";R();async function q0(z,Z){let $=JSON.stringify({jobName:z,payload:Z.payload||{},options:{queue:Z.queue,tries:Z.maxTries,timeout:Z.timeout,backoff:Z.backoff}}),{db:U}=await import("@stacksjs/database");await U.insertInto("jobs").values({queue:Z.queue||"default",payload:$,attempts:0,available_at:A9(Z.delay||0),created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}function A9(z){let Z=Date.now();return Math.floor(Z/1000+z)}var x1={checkInterval:60000,preventOverlapping:!0},A={isRunning:!1,isShuttingDown:!1,checkInterval:null,jobs:new Map,config:{...x1}};function x9(z,Z){let $=new Date,U=$.getMinutes(),V=$.getHours(),K=$.getDate(),L=$.getMonth()+1,G=$.getDay();if(Z){let P=Z.getMinutes(),I=Z.getHours(),p=Z.getDate();if(P===U&&I===V&&p===K)return!1}let Y=z.trim().split(/\s+/);if(Y.length===6)Y=Y.slice(1);if(Y.length<5)return D.warn(`Invalid cron expression: ${z}`),!1;let[X,M,F,x,O]=Y;return $0(X,U,0,59)&&$0(M,V,0,23)&&$0(F,K,1,31)&&$0(x,L,1,12)&&$0(O,G,0,6)}function $0(z,Z,$,U){if(z==="*")return!0;if(z.includes(","))return z.split(",").map((L)=>Number.parseInt(L.trim(),10)).includes(Z);if(z.includes("-")){let[K,L]=z.split("-").map((G)=>Number.parseInt(G.trim(),10));return Z>=K&&Z<=L}if(z.includes("/")){let[K,L]=z.split("/"),G=Number.parseInt(L,10);if(K==="*")return Z%G===0;if(K.includes("-")){let[Y,X]=K.split("-").map((M)=>Number.parseInt(M.trim(),10));return Z>=Y&&Z<=X&&(Z-Y)%G===0}}let V=Number.parseInt(z,10);return!Number.isNaN(V)&&Z===V}function F1(z){let $={"@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 * * * *"}[z.toLowerCase()];if($)return $;let U=z.match(/^Every\.(\w+)$/i);if(U&&U[1]!==void 0){let K=U[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 * *"}[K]||null}let V=z.split(/\s+/).length;if(V>=5&&V<=6)return z;return null}function T1(z){let Z=new Date;if(z.trim().split(/\s+/).length<5)return null;let U=new Date(Z.getTime()+60000);return U.setSeconds(0),U.setMilliseconds(0),U}async function F9(z={}){if(A.isRunning){D.warn("Scheduler is already running");return}A.config={...x1,...z},A.isRunning=!0,A.isShuttingDown=!1,await w0();let Z=C0();for(let U of Z){let V=U.config.rate||U.config.schedule;if(V){let K=F1(V);if(K)A.jobs.set(U.name,{job:U,lastRun:null,nextRun:T1(K),isRunning:!1}),D.info(`Registered scheduled job: ${U.name} (${K})`);else D.warn(`Invalid schedule for job ${U.name}: ${V}`)}}if(A.jobs.size===0){D.info("No scheduled jobs found");return}D.info(`Scheduler started with ${A.jobs.size} job(s)`),process.on("SIGINT",()=>v0()),process.on("SIGTERM",()=>v0());let $=!1;A.checkInterval=setInterval(()=>{if(!A.isShuttingDown&&!$)$=!0,A1().catch((U)=>D.error("Scheduler check failed:",U)).finally(()=>{$=!1})},A.config.checkInterval),A.checkInterval?.unref?.(),await A1()}async function A1(){for(let[z,Z]of A.jobs){let $=Z.job.config.rate||Z.job.config.schedule;if(!$)continue;let U=F1($);if(!U)continue;if(x9(U,Z.lastRun)){if(A.config.preventOverlapping&&Z.isRunning){D.debug(`Skipping ${z}: previous execution still running`);continue}if(Z.job.config.withoutOverlapping&&Z.isRunning){D.debug(`Skipping ${z}: withoutOverlapping is enabled`);continue}try{Z.isRunning=!0,Z.lastRun=new Date,Z.nextRun=T1(U),D.info(`Dispatching scheduled job: ${z}`),await f("job:added",{jobId:`scheduled-${z}-${Date.now()}`,queueName:Z.job.config.queue||"default",jobName:z}),await q0(z,{queue:Z.job.config.queue||"default",payload:{},maxTries:Z.job.config.tries||3,timeout:Z.job.config.timeout||60}),Z.isRunning=!1,D.info(`Scheduled job ${z} dispatched to queue`)}catch(V){Z.isRunning=!1,D.error(`Failed to dispatch scheduled job ${z}:`,V)}}}}async function v0(){if(!A.isRunning)return;if(D.info("Stopping scheduler..."),A.isShuttingDown=!0,A.checkInterval)clearInterval(A.checkInterval),A.checkInterval=null;A.isRunning=!1,A.jobs.clear(),D.info("Scheduler stopped")}function T9(){return{isRunning:A.isRunning,jobCount:A.jobs.size,jobs:Array.from(A.jobs.entries()).map(([z,Z])=>({name:z,schedule:Z.job.config.rate||Z.job.config.schedule,lastRun:Z.lastRun,nextRun:Z.nextRun,isRunning:Z.isRunning}))}}function P9(){return A.isRunning}function D9(){return new Map(A.jobs)}async function S9(z){let Z=A.jobs.get(z);if(!Z)throw Error(`Scheduled job "${z}" not found`);D.info(`Manually triggering scheduled job: ${z}`),await q0(z,{queue:Z.job.config.queue||"default",payload:{},maxTries:Z.job.config.tries||3,timeout:Z.job.config.timeout||60})}R();R();import{log as Q9}from"@stacksjs/logging";var E9={maxPendingWarning:1000,maxPendingCritical:5000,maxFailedWarning:10,maxFailedCritical:100,maxJobAgeWarning:3600,maxJobAgeCritical:86400,maxErrorRateWarning:0.1,maxErrorRateCritical:0.5};async function k0(z={}){let Z={...E9,...z},$=[],U=new Date,V=Math.floor(U.getTime()/1000);try{let{db:K}=await import("@stacksjs/database"),L=await K.selectFrom("jobs").selectAll().execute(),G=await K.selectFrom("failed_jobs").selectAll().execute(),Y=new Map;for(let _ of L){let T=_.queue||"default";if(!Y.has(T))Y.set(T,{pending:0,processing:0,delayed:0});let Q=Y.get(T);if(_.reserved_at)Q.processing++;else if(_.available_at&&_.available_at>V)Q.delayed++;else if(Q.pending++,_.created_at){let B=typeof _.created_at==="number"?_.created_at:Math.floor(new Date(_.created_at).getTime()/1000),j0=V-B;if(!Q.oldestAge||j0>Q.oldestAge)Q.oldestAge=j0}}let X=new Map;for(let _ of G){let T=_.queue||"default";X.set(T,(X.get(T)||0)+1)}let M=[],F=new Set([...Y.keys(),...X.keys()]),x=Z.queues?[...F].filter((_)=>Z.queues.includes(_)):[...F],O=0,P=0,I=0,p=0;for(let _ of x){let T=Y.get(_)||{pending:0,processing:0,delayed:0},Q=X.get(_)||0;O+=T.pending,P+=T.processing,I+=T.delayed,p+=Q;let B="healthy";if(T.pending>=Z.maxPendingCritical)B="unhealthy",$.push({level:"critical",message:`Queue "${_}" has ${T.pending} pending jobs (threshold: ${Z.maxPendingCritical})`,queue:_,timestamp:U.toISOString()});else if(T.pending>=Z.maxPendingWarning)B="degraded",$.push({level:"warning",message:`Queue "${_}" has ${T.pending} pending jobs (threshold: ${Z.maxPendingWarning})`,queue:_,timestamp:U.toISOString()});if(Q>=Z.maxFailedCritical)B="unhealthy",$.push({level:"critical",message:`Queue "${_}" has ${Q} failed jobs (threshold: ${Z.maxFailedCritical})`,queue:_,timestamp:U.toISOString()});else if(Q>=Z.maxFailedWarning){if(B==="healthy")B="degraded";$.push({level:"warning",message:`Queue "${_}" has ${Q} failed jobs (threshold: ${Z.maxFailedWarning})`,queue:_,timestamp:U.toISOString()})}if(T.oldestAge){if(T.oldestAge>=Z.maxJobAgeCritical)B="unhealthy",$.push({level:"critical",message:`Queue "${_}" has a job waiting for ${Math.floor(T.oldestAge/3600)} hours`,queue:_,timestamp:U.toISOString()});else if(T.oldestAge>=Z.maxJobAgeWarning){if(B==="healthy")B="degraded";$.push({level:"warning",message:`Queue "${_}" has a job waiting for ${Math.floor(T.oldestAge/60)} minutes`,queue:_,timestamp:U.toISOString()})}}M.push({name:_,status:B,pending:T.pending,processing:T.processing,delayed:T.delayed,failed:Q,oldestJobAge:T.oldestAge})}let h0=X0().getMetrics(),Q1=h0.throughputPerMinute,E1=h0.averageDuration,p0=O+P+I+p,d=p0>0?p/p0:0,j="healthy";if(M.some((_)=>_.status==="unhealthy"))j="unhealthy";else if(M.some((_)=>_.status==="degraded"))j="degraded";if(d>=Z.maxErrorRateCritical)j="unhealthy",$.push({level:"critical",message:`Overall error rate is ${(d*100).toFixed(1)}% (threshold: ${Z.maxErrorRateCritical*100}%)`,timestamp:U.toISOString()});else if(d>=Z.maxErrorRateWarning){if(j==="healthy")j="degraded";$.push({level:"warning",message:`Overall error rate is ${(d*100).toFixed(1)}% (threshold: ${Z.maxErrorRateWarning*100}%)`,timestamp:U.toISOString()})}let N1=a().getAll().map((_)=>({id:_.id,status:_.status,queue:_.queue,processedCount:_.processedCount,failedCount:_.failedCount,lastActivityAt:_.lastActivityAt}));return{status:j,timestamp:U.toISOString(),queues:M,workers:N1,metrics:{totalPending:O,totalProcessing:P,totalDelayed:I,totalFailed:p,throughputPerMinute:Q1,averageProcessingTime:E1,errorRate:d},alerts:$}}catch(K){Q9.error("Failed to perform queue health check:",K);let G=a().getAll().map((Y)=>({id:Y.id,status:Y.status,queue:Y.queue,processedCount:Y.processedCount,failedCount:Y.failedCount,lastActivityAt:Y.lastActivityAt}));return{status:"unhealthy",timestamp:U.toISOString(),queues:[],workers:G,metrics:{totalPending:0,totalProcessing:0,totalDelayed:0,totalFailed:0,throughputPerMinute:0,averageProcessingTime:0,errorRate:0},alerts:[{level:"critical",message:`Health check failed: ${K.message}`,timestamp:U.toISOString()}]}}}function N9(z={}){return async(Z)=>{let $=await k0(z),U=$.status==="healthy"?200:$.status==="degraded"?207:503;return new Response(JSON.stringify($,null,2),{status:U,headers:{"Content-Type":"application/json","Cache-Control":"no-store"}})}}async function B9(z={}){return(await k0(z)).status==="healthy"}import{log as N}from"@stacksjs/logging";var R9={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function U0(z){return String(z).replace(/[&<>"']/g,(Z)=>R9[Z]??Z)}class y0{config;notificationCount=0;lastResetTime=Date.now();pendingBatch=[];batchTimeout=null;activeFlush=null;constructor(z){this.config=z}async notify(z){if(this.config.filter&&!this.config.filter(z))return;if(this.config.rateLimit){let Z=Date.now();if(Z-this.lastResetTime>3600000)this.notificationCount=0,this.lastResetTime=Z;if(this.notificationCount>=this.config.rateLimit){N.debug("Rate limit reached for failed job notifications");return}}if(this.config.batch){if(this.pendingBatch.push(z),!this.batchTimeout)this.batchTimeout=setTimeout(()=>{this.batchTimeout=null,this.activeFlush=this.flushBatch().catch((Z)=>N.error("Failed to flush notification batch:",Z)).finally(()=>{this.activeFlush=null})},this.config.batchInterval||60000),this.batchTimeout.unref?.();return}await this.sendNotifications([z])}async flushBatch(){if(this.pendingBatch.length===0)return;let z=[...this.pendingBatch];if(this.pendingBatch=[],this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;await this.sendNotifications(z)}async shutdown(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.activeFlush)await this.activeFlush.catch(()=>{});await this.flushBatch().catch((z)=>N.error("Failed to flush notification batch on shutdown:",z))}async sendNotifications(z){let Z=[];for(let $ of this.config.channels)switch($){case"email":if(this.config.email)Z.push(this.sendEmail(z));break;case"slack":if(this.config.slack)Z.push(this.sendSlack(z));break;case"discord":if(this.config.discord)Z.push(this.sendDiscord(z));break;case"webhook":if(this.config.webhook)Z.push(this.sendWebhook(z));break;case"log":this.logFailures(z);break}try{await Promise.all(Z)}catch($){N.error("Failed to send job failure notifications:",$)}finally{this.notificationCount+=z.length}}async sendEmail(z){let Z=this.config.email,$=Z.subject||`[Queue] ${z.length} Job(s) Failed`,U=this.formatEmailBody(z);try{let{mail:V}=await import("@stacksjs/email");await V.send({to:Array.isArray(Z.to)?Z.to:[Z.to],from:Z.from,subject:$,html:U})}catch(V){N.error("Failed to send email notification:",V)}}formatEmailBody(z){let Z=z.map(($)=>`
2
+ var{defineProperty:kZ,getOwnPropertyNames:M8,getOwnPropertyDescriptor:B8}=Object,T8=Object.prototype.hasOwnProperty;function E8(Z){return this[Z]}var R8=(Z)=>{var $=(_5??=new WeakMap).get(Z),z;if($)return $;if($=kZ({},"__esModule",{value:!0}),Z&&typeof Z==="object"||typeof Z==="function"){for(var V of M8(Z))if(!T8.call($,V))kZ($,V,{get:E8.bind(Z,V),enumerable:!(z=B8(Z,V))||z.enumerable})}return _5.set(Z,$),$},_5;var N8=(Z)=>Z;function S8(Z,$){this[Z]=N8.bind(null,$)}var h=(Z,$)=>{for(var z in $)kZ(Z,z,{get:$[z],enumerable:!0,configurable:!0,set:S8.bind($,z)})};var p=(Z,$)=>()=>(Z&&($=Z(Z=0)),$);var O=import.meta.require;function g(Z,$,z){return{jobName:Z,payload:$,options:z,envelopeVersion:1,dispatchedAt:new Date().toISOString()}}function O5(Z,$){if(IZ.has(Z))return;IZ.add(Z),console.warn($)}function C8(){IZ.clear()}function QZ(Z){let $;if(typeof Z==="string")try{$=JSON.parse(Z)}catch{return{ok:!1,reason:"malformed",detail:"not valid JSON"}}else if(Z&&typeof Z==="object")$=Z;else return{ok:!1,reason:"malformed",detail:`expected string or object, got ${typeof Z}`};if($.envelopeVersion===1){if(typeof $.jobName!=="string")return{ok:!1,reason:"missing-job-name"};return{ok:!0,envelope:{jobName:$.jobName,payload:$.payload,options:$.options??void 0,envelopeVersion:1,dispatchedAt:typeof $.dispatchedAt==="string"?$.dispatchedAt:new Date(0).toISOString()},source:"v1"}}if(typeof $.envelopeVersion==="number"&&$.envelopeVersion>1)return{ok:!1,reason:"unknown-version",detail:`envelopeVersion=${$.envelopeVersion}, this worker speaks v1`};if(typeof $.jobName==="string")return O5("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:$.jobName,payload:$.payload,options:$.options??void 0,envelopeVersion:1,dispatchedAt:typeof $.dispatchedAt==="string"?$.dispatchedAt:new Date(0).toISOString()},source:"v0-implicit"};if(typeof $.job==="string")return O5("laravel-legacy","[queue/envelope] Processing Laravel-legacy job envelope (`{ job, data }` shape). "+"Will continue to process but the queue table contains migration-era rows \u2014 "+"consider flushing once they drain."),{ok:!0,envelope:{jobName:$.job.replace(/^App\\+Jobs\\+/,"").replace(/^.*\\/,""),payload:$.data,options:void 0,envelopeVersion:1,dispatchedAt:new Date(0).toISOString()},source:"laravel-legacy"};return{ok:!1,reason:"missing-job-name"}}var J8=1,IZ;var ZZ=p(()=>{IZ=new Set});var fZ={};h(fZ,{runJob:()=>A5,restore:()=>wZ,isFaked:()=>x5,getFakeQueue:()=>F5,fake:()=>yZ,expectJobToFail:()=>D5,createQueueTester:()=>P5,QueueTester:()=>MZ});class H5{dispatchedJobs=[];pushedJobs=[];processedJobs=[];failedJobs=[];dispatch(Z,$,z={}){this.dispatchedJobs.push({name:Z,data:$,options:z,dispatchedAt:new Date,queue:z.queue||"default"})}push(Z,$,z={}){this.pushedJobs.push({name:Z,data:$,options:z,dispatchedAt:new Date,queue:z.queue||"default"})}dispatched(Z){if(Z)return this.dispatchedJobs.filter(($)=>$.name===Z);return[...this.dispatchedJobs]}pushed(Z){if(Z)return this.pushedJobs.filter(($)=>$.name===Z);return[...this.pushedJobs]}assertDispatched(Z,$){let z=this.dispatched(Z);if(z.length===0)throw Error(`Expected job "${Z}" to be dispatched, but it was not.`);if($){if(z.filter($).length===0)throw Error(`Expected job "${Z}" to be dispatched matching the callback, but no matching jobs were found.`)}}assertNotDispatched(Z){let $=this.dispatched(Z);if($.length>0)throw Error(`Expected job "${Z}" to not be dispatched, but it was dispatched ${$.length} time(s).`)}assertDispatchedTimes(Z,$){let z=this.dispatched(Z);if(z.length!==$)throw Error(`Expected job "${Z}" to be dispatched ${$} time(s), but it was dispatched ${z.length} time(s).`)}assertNothingDispatched(){if(this.dispatchedJobs.length>0){let Z=[...new Set(this.dispatchedJobs.map(($)=>$.name))].join(", ");throw Error(`Expected no jobs to be dispatched, but found: ${Z}`)}}assertPushed(Z,$){let z=this.pushed(Z);if(z.length===0)throw Error(`Expected job "${Z}" to be pushed, but it was not.`);if($){if(z.filter($).length===0)throw Error(`Expected job "${Z}" to be pushed matching the callback, but no matching jobs were found.`)}}assertPushedWithDelay(Z,$){if(this.pushed(Z).filter((Y)=>Y.options.delay===$).length===0)throw Error(`Expected job "${Z}" to be pushed with delay ${$}ms, but no matching jobs were found.`)}assertPushedOn(Z,$){if(this.pushed($).filter((V)=>V.queue===Z).length===0)throw Error(`Expected job "${$}" to be pushed on queue "${Z}", but it was not.`)}async processJob(Z,$){let z=this.dispatchedJobs.find((V)=>V.name===Z);if(!z)throw Error(`No dispatched job found with name "${Z}"`);try{await $(z.data),this.processedJobs.push(z)}catch(V){throw this.failedJobs.push({job:z,error:V}),V}}processed(Z){if(Z)return this.processedJobs.filter(($)=>$.name===Z);return[...this.processedJobs]}failed(Z){if(Z)return this.failedJobs.filter(($)=>$.job.name===Z);return[...this.failedJobs]}reset(){this.dispatchedJobs=[],this.pushedJobs=[],this.processedJobs=[],this.failedJobs=[]}}function yZ(){return $Z=new H5,$Z}function F5(){return $Z}function x5(){return $Z!==null}function wZ(){$Z=null}class MZ{queue;constructor(){this.queue=yZ()}dispatch(Z,$,z={}){return this.queue.dispatch(Z,$,z),this}push(Z,$,z={}){return this.queue.push(Z,$,z),this}assertDispatched(Z,$){return this.queue.assertDispatched(Z,$),this}assertNotDispatched(Z){return this.queue.assertNotDispatched(Z),this}assertDispatchedTimes(Z,$){return this.queue.assertDispatchedTimes(Z,$),this}assertNothingDispatched(){return this.queue.assertNothingDispatched(),this}dispatched(Z){return this.queue.dispatched(Z)}reset(){return this.queue.reset(),this}cleanup(){wZ()}}function P5(){return new MZ}async function A5(Z,$){return await Z.handle($)}async function D5(Z,$,z){try{throw await Z.handle($),Error("Expected job to fail, but it succeeded")}catch(V){if(V.message==="Expected job to fail, but it succeeded")throw V;if(z){let Y=V.message;if(typeof z==="string"){if(!Y.includes(z))throw Error(`Expected error to contain "${z}", got "${Y}"`)}else if(!z.test(Y))throw Error(`Expected error to match ${z}, got "${Y}"`)}return V}}var $Z=null;var VZ={};h(VZ,{setQueueManager:()=>j8,getQueueManager:()=>f8,dispatchSync:()=>w8,dispatchAfter:()=>y8,dispatch:()=>I8,createRedisDispatcher:()=>h8,chain:()=>k8,batch:()=>v8,StacksQueueManager:()=>Q5,RedisQueue:()=>s,RedisJob:()=>M5,QueueManager:()=>m8});import{Queue as q8,batch as v8,chain as k8,dispatch as I8,dispatchAfter as y8,dispatchSync as w8,getQueueManager as f8,QueueManager as m8,setQueueManager as j8}from"bun-queue";import{log as mZ}from"@stacksjs/logging";class s{queue;config;isProcessing=!1;constructor(Z,$){this.config=$,this.queue=new q8(Z,{driver:"redis",prefix:$.prefix,redis:$.redis?{url:$.redis.url||this.buildRedisUrl($.redis)}:void 0,defaultJobOptions:$.defaultJobOptions?{delay:$.defaultJobOptions.delay,attempts:$.defaultJobOptions.attempts,backoff:$.defaultJobOptions.backoff,removeOnComplete:$.defaultJobOptions.removeOnComplete,removeOnFail:$.defaultJobOptions.removeOnFail,priority:$.defaultJobOptions.priority,lifo:$.defaultJobOptions.lifo,timeout:$.defaultJobOptions.timeout,jobId:$.defaultJobOptions.jobId,dependsOn:$.defaultJobOptions.dependsOn,keepJobs:$.defaultJobOptions.keepJobs,deadLetter:$.defaultJobOptions.deadLetter}:void 0,limiter:$.limiter,metrics:$.metrics,stalledJobCheckInterval:$.stalledJobCheckInterval,maxStalledJobRetries:$.maxStalledJobRetries,distributedLock:$.distributedLock,defaultDeadLetterOptions:$.defaultDeadLetterOptions,horizontalScaling:$.horizontalScaling,logLevel:$.logLevel}),mZ.debug(`Redis queue "${Z}" initialized`)}buildRedisUrl(Z){let $=Z.host||"localhost",z=Z.port||6379,V=Z.password?`:${encodeURIComponent(Z.password)}@`:"",Y=Number.isFinite(Z.db)&&Z.db>=0?Z.db:0;return`redis://${V}${$}:${z}/${Y}`}async add(Z,$){let z={delay:$?.delay?$.delay*1000:void 0,attempts:$?.maxTries,priority:$?.priority,timeout:$?.timeout?$.timeout*1000:void 0,backoff:Array.isArray($?.backoff)?{type:"fixed",delay:($.backoff[0]||1)*1000}:$?.backoff};return this.queue.add(Z,z)}process(Z,$){if(this.isProcessing){mZ.warn("Queue is already processing");return}this.isProcessing=!0,this.queue.process(Z,$),mZ.info(`Started processing queue with concurrency ${Z}`)}async getJob(Z){return this.queue.getJob(Z)}async getJobs(Z){return this.queue.getJobs(Z)}async getJobCounts(){return this.queue.getJobCounts()}async removeJob(Z){return this.queue.removeJob(Z)}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(Z){return this.queue.scheduleCron({cron:Z.cron,data:Z.data,tz:Z.tz,name:Z.name})}async unscheduleCron(Z){return this.queue.unscheduleCron(Z)}async getDeadLetterJobs(){return this.queue.getDeadLetterJobs()}async republishDeadLetterJob(Z){return this.queue.republishDeadLetterJob(Z)}async clearDeadLetterQueue(){return this.queue.clearDeadLetterQueue()}async bulkRemove(Z){return this.queue.bulkRemove(Z)}async getClusterInfo(){return this.queue.getClusterInfo()}isLeader(){return this.queue.isLeader()}getQueue(){return this.queue}on(Z,$){this.queue.events.on(Z,$)}}class Q5{config;queues=new Map;defaultConnection="default";constructor(Z){this.config=Z;for(let[$,z]of Object.entries(Z))if(z.driver==="redis")this.queues.set($,new s($,z))}queue(Z){let $=Z||this.defaultConnection,z=this.queues.get($);if(!z){let V=this.config[$];if(!V)throw Error(`Queue "${$}" not configured`);z=new s($,V),this.queues.set($,z)}return z}setDefaultConnection(Z){this.defaultConnection=Z}async closeAll(){let Z=Array.from(this.queues.values()).map(($)=>$.close());await Promise.all(Z),this.queues.clear()}}function h8(Z,$){let z=new s(Z,$);return async(V,Y)=>{return z.add(V,Y)}}class M5{data;options={};queue;constructor(Z,$,z){this.data=z;this.queue=new s(Z,$)}async dispatch(){await this.queue.add(this.data,this.options)}async dispatchNow(){await this.queue.add(this.data,{...this.options,immediate:!0})}delay(Z){return this.options.delay=Z,this}afterResponse(){return this.options.afterResponse=!0,this}chain(Z){return this.options.chainedJobs=Z,this}onQueue(Z){return this.options.queue=Z,this}priority(Z){return this.options.priority=Z,this}tries(Z){return this.options.maxTries=Z,this}timeout(Z){return this.options.timeout=Z,this}backoff(Z){return this.options.backoff=Z,this}}var YZ=()=>{};function R5(){if(E5)return;E5=!0,console.warn("[queue/idempotency] job_idempotency table missing \u2014 idempotency keys are accepted but NOT enforced. "+"Run migrations to enable dedup.")}function N5(Z){let $=Z?.message??"";return $.includes("no such table")||$.includes("doesn't exist")}async function jZ(Z){try{let{db:$}=await import("@stacksjs/database"),z=await $.selectFrom("job_idempotency").where("idempotency_key","=",Z).select(["idempotency_key"]).executeTakeFirst();return Boolean(z)}catch($){if(N5($))return R5(),!1;throw $}}async function hZ(Z,$,z){try{let{db:V}=await import("@stacksjs/database");await V.insertInto("job_idempotency").values({idempotency_key:Z,job_name:$,queue:z??"default",dispatched_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch(V){if(N5(V)){R5();return}let Y=V?.message??"";if(Y.includes("UNIQUE constraint")||Y.includes("Duplicate entry"))return;throw V}}var E5=!1;function GZ(Z){let $=Z?.numUpdatedRows;if($===null||$===void 0)return 0;if(typeof $==="object")return Number($.changes??0);return Number($)}async function pZ(Z,$){let z=JSON.stringify({jobName:Z,payload:$.payload||{},options:{queue:$.queue,tries:$.maxTries,timeout:$.timeout,backoff:$.backoff}}),{db:V}=await import("@stacksjs/database");await V.insertInto("jobs").values({queue:$.queue||"default",payload:z,attempts:0,available_at:g8($.delay||0),created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}function g8(Z){let $=Date.now();return Math.floor($/1000+Z)}var y={};h(y,{withEvents:()=>J5,onQueueEvent:()=>S5,getWorkerTracker:()=>KZ,getQueueEvents:()=>t,getGlobalMetrics:()=>EZ,emitQueueEvent:()=>b,QueueMetrics:()=>TZ,QueueEvents:()=>BZ,OnQueueEvent:()=>C5});import{log as k}from"@stacksjs/logging";class BZ{handlers=new Map;wildcardHandlers=new Set;on(Z,$){if(!this.handlers.has(Z))this.handlers.set(Z,new Set);return this.handlers.get(Z).add($),()=>{this.handlers.get(Z)?.delete($)}}onAny(Z){return this.wildcardHandlers.add(Z),()=>{this.wildcardHandlers.delete(Z)}}once(Z,$){let z=async(V)=>{this.handlers.get(Z)?.delete(z),await $(V)};return this.on(Z,z)}async emit(Z,$){let z={...$,timestamp:Date.now()};this.logEvent(Z,z);let V=this.handlers.get(Z);if(V)for(let Y of V)try{await Y(z)}catch(G){k.error(`Error in queue event handler for ${Z}:`,G)}for(let Y of this.wildcardHandlers)try{await Y(Z,z)}catch(G){k.error("Error in wildcard queue event handler:",G)}}logEvent(Z,$){let z=$.jobId?`[${$.jobId}]`:"",V=$.queueName?`on ${$.queueName}`:"";switch(Z){case"job:added":k.debug(`Job added ${z} ${V}`);break;case"job:processing":k.debug(`Job processing ${z} ${V}`);break;case"job:completed":k.info(`Job completed ${z} ${V} in ${$.duration}ms`);break;case"job:failed":k.error(`Job failed ${z} ${V}:`,$.error);break;case"job:retrying":k.warn(`Job retrying ${z} ${V} (attempt ${$.attemptsMade})`);break;case"job:stalled":k.warn(`Job stalled ${z} ${V}`);break;case"queue:error":k.error(`Queue error ${V}:`,$.error);break}}off(Z){this.handlers.delete(Z)}removeAllListeners(){this.handlers.clear(),this.wildcardHandlers.clear()}}function t(){if(!gZ)gZ=new BZ;return gZ}function S5(Z,$){let z=t();if(Z==="*")return z.onAny($);return z.on(Z,$)}function b(Z,$){return t().emit(Z,$)}function J5(Z,$){return async(...z)=>{let V=z[0]?.id||"unknown",Y=Date.now();await b("job:processing",{jobId:V,queueName:Z,data:z[0]?.data});try{let G=await $(...z);return await b("job:completed",{jobId:V,queueName:Z,result:G,duration:Date.now()-Y}),G}catch(G){throw await b("job:failed",{jobId:V,queueName:Z,error:G,duration:Date.now()-Y}),G}}}function C5(Z){return function($,z,V){let Y=V.value;return t().on(Z,Y),V}}class TZ{jobCounts={added:0,completed:0,failed:0,processing:0};completions=[];errors=[];unsubscribe=[];constructor(){this.setupListeners()}setupListeners(){let Z=t();this.unsubscribe.push(Z.on("job:added",()=>{this.jobCounts.added++}),Z.on("job:processing",()=>{this.jobCounts.processing++}),Z.on("job:completed",($)=>{this.jobCounts.completed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1);let z=$.duration||0;if(this.completions.push({timestamp:Date.now(),duration:z}),this.completions.length>1000)this.completions.shift()}),Z.on("job:failed",($)=>{if(this.jobCounts.failed++,this.jobCounts.processing=Math.max(0,this.jobCounts.processing-1),$.error){if(this.errors.push({error:$.error,timestamp:Date.now()}),this.errors.length>100)this.errors.shift()}}))}getThroughputPerMinute(){let Z=Date.now()-60000;return this.completions.filter((z)=>z.timestamp>=Z).length}getAverageProcessingTime(){let Z=Date.now()-60000,$=this.completions.filter((z)=>z.timestamp>=Z);if($.length===0)return 0;return $.reduce((z,V)=>z+V.duration,0)/$.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((Z)=>Z()),this.unsubscribe=[]}}function EZ(){if(!bZ)bZ=new TZ;return bZ}class q5{workers=new Map;register(Z,$){this.workers.set(Z,{id:Z,status:"idle",queue:$,processedCount:0,failedCount:0,lastActivityAt:new Date().toISOString(),startedAt:new Date().toISOString()})}markActive(Z){let $=this.workers.get(Z);if($)$.status="active",$.lastActivityAt=new Date().toISOString()}markIdle(Z){let $=this.workers.get(Z);if($)$.status="idle",$.lastActivityAt=new Date().toISOString()}recordCompletion(Z){let $=this.workers.get(Z);if($)$.processedCount++,$.lastActivityAt=new Date().toISOString()}recordFailure(Z){let $=this.workers.get(Z);if($)$.failedCount++,$.lastActivityAt=new Date().toISOString()}unregister(Z){let $=this.workers.get(Z);if($)$.status="stopped"}getAll(){return Array.from(this.workers.values())}clear(){this.workers.clear()}}function KZ(){return b8}var gZ=null,bZ=null,b8;var q=p(()=>{b8=new q5});var m={};h(m,{recordBatchJobFailure:()=>tZ,recordBatchJobCompletion:()=>sZ,isBatchCancelled:()=>p5,getBatchCallbacks:()=>XZ,PendingBatch:()=>NZ,DispatchedBatch:()=>w,Batch:()=>cZ});import{log as M}from"@stacksjs/logging";import{env as u8}from"@stacksjs/env";function f(){return u8.QUEUE_DRIVER||"sync"}class NZ{jobs;options={thenCallbacks:[],catchCallbacks:[],finallyCallbacks:[],progressCallbacks:[]};constructor(Z){this.jobs=Z.map(($)=>("job"in $)?$:{job:$})}name(Z){return this.options.name=Z,this}onQueue(Z){return this.options.queue=Z,this}allowFailures(){return this.options.allowFailures=!0,this}then(Z){return this.options.thenCallbacks.push(Z),this}catch(Z){return this.options.catchCallbacks.push(Z),this}finally(Z){return this.options.finallyCallbacks.push(Z),this}progress(Z){return this.options.progressCallbacks.push(Z),this}thenHandler(Z){return this.options.thenHandler=Z,this}catchHandler(Z){return this.options.catchHandler=Z,this}finallyHandler(Z){return this.options.finallyHandler=Z,this}async dispatch(){let Z=crypto.randomUUID(),$=this.jobs.length;if($===0)throw Error("Cannot dispatch an empty batch");let z=f();await c8({id:Z,name:this.options.name||"",total_jobs:$,pending_jobs:$,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}),l8(Z,this.options);try{let{emitQueueEvent:V}=await Promise.resolve().then(() => (q(),y));await V("batch:added",{jobId:Z,data:{name:this.options.name,totalJobs:$}})}catch{}for(let V=0;V<this.jobs.length;V++){let Y=this.jobs[V];if(!Y)continue;let{job:G,payload:K}=Y,U={...K,_batchId:Z,_batchIndex:V};if(this.options.queue&&!G.queue)G.queue=this.options.queue;if(z==="sync")try{await G.dispatchNow(U),await sZ(Z)}catch(X){await tZ(Z,`${Z}:${V}`,X)}else await G.dispatch(U)}return M.info(`[Batch] Dispatched batch "${this.options.name||Z}" with ${$} jobs`),new w(Z)}getJobs(){return[...this.jobs]}getOptions(){return this.options}}class w{id;constructor(Z){this.id=Z}async fresh(){return LZ(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 Z=await this.fresh();if(!Z)return 0;return Z.total_jobs-Z.pending_jobs}async progress(){let Z=await this.fresh();if(!Z||Z.total_jobs===0)return 0;let $=Z.total_jobs-Z.pending_jobs;return Math.round($/Z.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 Z=await this.fresh();if(!Z)return[];try{return JSON.parse(Z.failed_job_ids||"[]")}catch{return[]}}async cancel(){if(f()==="redis")await a8(this.id);else await s8(this.id);M.info(`[Batch] Cancelled batch ${this.id}`);let $=XZ(this.id);if($)for(let z of $.finallyCallbacks)try{await z(this)}catch(V){M.error(`[Batch] Error in finally callback for batch ${this.id}:`,V)}}async add(Z){let $=await this.fresh();if(!$)throw Error(`Batch ${this.id} not found`);if($.cancelled_at)throw Error(`Batch ${this.id} has been cancelled`);if($.finished_at)throw Error(`Batch ${this.id} has already finished`);let z=Z.map((K)=>("job"in K)?K:{job:K}),V=$.total_jobs+z.length,Y=$.pending_jobs+z.length;await v5(this.id,{total_jobs:V,pending_jobs:Y});let G=JSON.parse($.options||"{}");for(let K=0;K<z.length;K++){let U=z[K];if(!U)continue;let{job:X,payload:L}=U,W={...L,_batchId:this.id,_batchIndex:$.total_jobs+K};if(G.queue&&!X.queue)X.queue=G.queue;await X.dispatch(W)}M.info(`[Batch] Added ${z.length} jobs to batch ${this.id}`)}async delete(){await d8(this.id),RZ(this.id)}}class cZ{static create(Z){return new NZ(Z)}static async find(Z){if(!await LZ(Z))return null;return new w(Z)}static async all(){return(await i8()).map(($)=>new w($.id))}static async prune(Z=24){return n8(Z)}}function l8(Z,$){iZ.set(Z,$)}function XZ(Z){return iZ.get(Z)}function RZ(Z){iZ.delete(Z)}async function c8(Z){if(f()==="redis")await t8(Z);else await k5(Z)}async function LZ(Z){if(f()==="redis")return r8(Z);return I5(Z)}async function i8(){if(f()==="redis")return m5();return y5()}async function v5(Z,$){if(f()==="redis")await j5(Z,$);else await dZ(Z,$)}async function d8(Z){if(f()==="redis")await h5(Z);else await w5(Z)}async function n8(Z){if(f()==="redis")return o8(Z);return f5(Z)}async function k5(Z){let{db:$}=await import("@stacksjs/database");await $.insertInto("job_batches").values({id:Z.id,name:Z.name,total_jobs:Z.total_jobs,pending_jobs:Z.pending_jobs,failed_jobs:Z.failed_jobs,failed_job_ids:Z.failed_job_ids,options:Z.options,cancelled_at:Z.cancelled_at,created_at:Z.created_at,finished_at:Z.finished_at}).execute()}async function I5(Z){let{db:$}=await import("@stacksjs/database");return await $.selectFrom("job_batches").where("id","=",Z).selectAll().executeTakeFirst()||null}async function y5(){let{db:Z}=await import("@stacksjs/database");return await Z.selectFrom("job_batches").selectAll().orderBy("created_at","desc").execute()}async function dZ(Z,$){let{db:z}=await import("@stacksjs/database");await z.updateTable("job_batches").set($).where("id","=",Z).execute()}async function w5(Z){let{db:$}=await import("@stacksjs/database");await $.deleteFrom("job_batches").where("id","=",Z).execute()}async function s8(Z){await dZ(Z,{cancelled_at:new Date().toISOString().slice(0,19).replace("T"," "),finished_at:new Date().toISOString().slice(0,19).replace("T"," ")})}async function f5(Z){let{db:$}=await import("@stacksjs/database"),z=new Date(Date.now()-Z*60*60*1000).toISOString().slice(0,19).replace("T"," "),V=await $.deleteFrom("job_batches").where("finished_at","is not",null).where("finished_at","<",z).executeTakeFirst();return Number(V?.numDeletedRows??0)}async function t8(Z){try{let{createClient:$}=await import("redis"),{queue:z}=await import("@stacksjs/config"),V=z?.connections?.redis?.redis,Y=V?.url||`redis://${V?.host||"localhost"}:${V?.port||6379}`,G=$({url:Y});await G.connect();let K=`${UZ}${Z.id}`;await G.hSet(K,{id:Z.id,name:Z.name,total_jobs:String(Z.total_jobs),pending_jobs:String(Z.pending_jobs),failed_jobs:String(Z.failed_jobs),failed_job_ids:Z.failed_job_ids,options:Z.options,cancelled_at:Z.cancelled_at||"",created_at:Z.created_at,finished_at:Z.finished_at||""}),await G.sAdd(nZ,Z.id),await G.quit()}catch{await k5(Z)}}async function r8(Z){try{let{createClient:$}=await import("redis"),{queue:z}=await import("@stacksjs/config"),V=z?.connections?.redis?.redis,Y=V?.url||`redis://${V?.host||"localhost"}:${V?.port||6379}`,G=$({url:Y});await G.connect();let K=`${UZ}${Z}`,U=await G.hGetAll(K);if(await G.quit(),!U||!U.id)return null;return{id:U.id,name:U.name,total_jobs:Number(U.total_jobs),pending_jobs:Number(U.pending_jobs),failed_jobs:Number(U.failed_jobs),failed_job_ids:U.failed_job_ids,options:U.options,cancelled_at:U.cancelled_at||null,created_at:U.created_at,finished_at:U.finished_at||null}}catch{return I5(Z)}}async function m5(){try{let{createClient:Z}=await import("redis"),{queue:$}=await import("@stacksjs/config"),z=$?.connections?.redis?.redis,V=z?.url||`redis://${z?.host||"localhost"}:${z?.port||6379}`,Y=Z({url:V});await Y.connect();let G=await Y.sMembers(nZ),K=[];for(let U of G){let X=`${UZ}${U}`,L=await Y.hGetAll(X);if(L?.id)K.push({id:L.id,name:L.name,total_jobs:Number(L.total_jobs),pending_jobs:Number(L.pending_jobs),failed_jobs:Number(L.failed_jobs),failed_job_ids:L.failed_job_ids,options:L.options,cancelled_at:L.cancelled_at||null,created_at:L.created_at,finished_at:L.finished_at||null})}return await Y.quit(),K}catch{return y5()}}async function j5(Z,$){try{let{createClient:z}=await import("redis"),{queue:V}=await import("@stacksjs/config"),Y=V?.connections?.redis?.redis,G=Y?.url||`redis://${Y?.host||"localhost"}:${Y?.port||6379}`,K=z({url:G});await K.connect();let U=`${UZ}${Z}`,X={};for(let[L,W]of Object.entries($))X[L]=W===null?"":String(W);await K.hSet(U,X),await K.quit()}catch{await dZ(Z,$)}}async function h5(Z){try{let{createClient:$}=await import("redis"),{queue:z}=await import("@stacksjs/config"),V=z?.connections?.redis?.redis,Y=V?.url||`redis://${V?.host||"localhost"}:${V?.port||6379}`,G=$({url:Y});await G.connect(),await G.del(`${UZ}${Z}`),await G.sRem(nZ,Z),await G.quit()}catch{await w5(Z)}}async function a8(Z){let $=new Date().toISOString().slice(0,19).replace("T"," ");await j5(Z,{cancelled_at:$,finished_at:$})}async function o8(Z){try{let $=await m5(),z=Date.now()-Z*60*60*1000,V=0;for(let Y of $)if(Y.finished_at){if(new Date(Y.finished_at).getTime()<z)await h5(Y.id),V++}return V}catch{return f5(Z)}}function uZ(Z){if(!Z)return null;try{let $=JSON.parse(Z);if($&&($.kind==="job"||$.kind==="module"))return $;return M.warn(`[Batch] handler JSON has unknown kind '${$?.kind}' \u2014 skipping`),null}catch($){return M.warn(`[Batch] failed to parse persistent handler: ${$.message}`),null}}async function lZ(Z,$){try{if(Z.kind==="job"){let{Jobs:Y}=await Promise.resolve().then(() => (WZ(),SZ));await Y.dispatch(Z.name,{...Z.payload??{},_batchId:$});return}let z=await import(Z.module).catch((Y)=>{return M.warn(`[Batch] persistent handler module not found: ${Z.module} (${Y.message})`),null});if(!z)return;let V=z[Z.export];if(typeof V!=="function"){M.warn(`[Batch] persistent handler export '${Z.export}' is not a function on ${Z.module}`);return}await V(Z.payload,$)}catch(z){M.error(`[Batch] persistent handler threw for batch ${$}:`,z)}}async function sZ(Z){let{db:$,sql:z}=await import("@stacksjs/database");await $.updateTable("job_batches").set({pending_jobs:z`GREATEST(pending_jobs - 1, 0)`}).where("id","=",Z).where("pending_jobs",">",0).execute();let V=new Date().toISOString().slice(0,19).replace("T"," "),Y=await $.updateTable("job_batches").set({finished_at:V}).where("id","=",Z).where("pending_jobs","=",0).where("finished_at","is",null).executeTakeFirst(),G=GZ(Y)>0,K=XZ(Z),U=new w(Z);if(K)for(let X of K.progressCallbacks)try{await X(U)}catch(L){M.error(`[Batch] Error in progress callback for batch ${Z}:`,L)}if(G){try{let{emitQueueEvent:_}=await Promise.resolve().then(() => (q(),y));await _("batch:completed",{jobId:Z})}catch{}let X=await LZ(Z);if(!X){RZ(Z),M.info(`[Batch] Batch ${Z} finished (record vanished)`);return}let L=JSON.parse(X.options||"{}"),P=!((X.failed_jobs||0)>0)||L.allowFailures;if(K){if(P)for(let _ of K.thenCallbacks)try{await _(U)}catch(H){M.error(`[Batch] Error in then callback for batch ${Z}:`,H)}for(let _ of K.finallyCallbacks)try{await _(U)}catch(H){M.error(`[Batch] Error in finally callback for batch ${Z}:`,H)}RZ(Z)}if(P){let _=uZ(X.then_handler);if(_)await lZ(_,Z)}else{let _=uZ(X.catch_handler);if(_)await lZ(_,Z)}let A=uZ(X.finally_handler);if(A)await lZ(A,Z);M.info(`[Batch] Batch ${Z} finished`)}}async function tZ(Z,$,z){let V=await LZ(Z);if(!V)return;let Y=[];try{Y=JSON.parse(V.failed_job_ids||"[]")}catch{Y=[]}Y.push($);let G=Math.max(0,V.pending_jobs-1),K=V.failed_jobs+1,U={pending_jobs:G,failed_jobs:K,failed_job_ids:JSON.stringify(Y)},X=JSON.parse(V.options||"{}");if(!X.allowFailures)U.cancelled_at=new Date().toISOString().slice(0,19).replace("T"," "),U.finished_at=new Date().toISOString().slice(0,19).replace("T"," ");else if(G===0)U.finished_at=new Date().toISOString().slice(0,19).replace("T"," ");await v5(Z,U);let L=XZ(Z),W=new w(Z);if(L)for(let A of L.catchCallbacks)try{await A(W,z)}catch(_){M.error(`[Batch] Error in catch callback for batch ${Z}:`,_)}try{let{emitQueueEvent:A}=await Promise.resolve().then(() => (q(),y));await A("batch:failed",{jobId:Z,error:z})}catch{}if((!X.allowFailures||G===0)&&L){for(let A of L.finallyCallbacks)try{await A(W)}catch(_){M.error(`[Batch] Error in finally callback for batch ${Z}:`,_)}if(X.allowFailures&&G===0)for(let A of L.thenCallbacks)try{await A(W)}catch(_){M.error(`[Batch] Error in then callback for batch ${Z}:`,_)}RZ(Z),M.info(`[Batch] Batch ${Z} finished with ${K} failure(s)`)}}async function p5(Z){return(await LZ(Z))?.cancelled_at!==null}var iZ,UZ="stacks:batch:",nZ="stacks:batches";var I=p(()=>{iZ=new Map});var SZ={};h(SZ,{runJob:()=>JZ,jobBatch:()=>l5,job:()=>b5,Jobs:()=>u5});import{appPath as e8}from"@stacksjs/path";import{env as Z9}from"@stacksjs/env";import{enqueueAfterCommit as $9,isInTransaction as z9}from"@stacksjs/database";function V9(){return Z9.QUEUE_DRIVER||"sync"}function Y9(){if(g5)return;g5=!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 v{name;payload;options={};txMode="auto";constructor(Z,$){this.name=Z;this.payload=$}onQueue(Z){return this.options.queue=Z,this}delay(Z){return this.options.delay=Z,this}tries(Z){return this.options.tries=Z,this}timeout(Z){return this.options.timeout=Z,this}backoff(Z){return this.options.backoff=Z,this}withContext(Z){return this.options.context=Z,this}withIdempotencyKey(Z){return this.options.idempotencyKey=Z,this}afterCommit(){return this.txMode="after",this}withoutCommit(){return this.txMode="immediate",this}async dispatch(){let{isFaked:Z,getFakeQueue:$}=await Promise.resolve().then(() => fZ);if(Z()){$()?.dispatch(this.name,this.payload,this.options);return}if(this.txMode!=="immediate"){if(z9()){let V=this.runDispatchPipeline.bind(this);if($9(async()=>{await V()}))return}else if(this.txMode==="after")Y9()}await this.runDispatchPipeline()}async runDispatchPipeline(){if(this.options.idempotencyKey){if(await jZ(this.options.idempotencyKey))return}let Z=V9();if(Z==="database")await this.dispatchToDatabase();else if(Z==="redis")await this.dispatchToRedis();else if(Z==="sync")await JZ(this.name,{payload:this.payload,context:this.options.context});else if(Z==="sqs"||Z==="memory"||Z==="beanstalkd")throw Error(`[queue] Driver "${Z}" is not implemented yet. Set QUEUE_DRIVER to one of: redis, database, sync.`);else throw Error(`[queue] Unknown QUEUE_DRIVER "${Z}". Allowed values: redis, database, sync.`);if(this.options.idempotencyKey)await hZ(this.options.idempotencyKey,this.name,this.options.queue)}async dispatchIf(Z){if(Z)await this.dispatch()}async dispatchUnless(Z){if(!Z)await this.dispatch()}async dispatchToDatabase(){let Z=Math.floor(Date.now()/1000),$=this.options.delay?Z+this.options.delay:Z,z=g(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff}),{db:V}=await import("@stacksjs/database");await V.insertInto("jobs").values({queue:this.options.queue||"default",payload:JSON.stringify(z),attempts:0,reserved_at:null,available_at:$,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(){let{RedisQueue:Z}=await Promise.resolve().then(() => (YZ(),VZ)),{queue:$}=await import("@stacksjs/config"),z=$?.connections?.redis;if(!z)throw Error("Redis queue connection is not configured. Check config/queue.ts");let V=new Z(this.options.queue||"default",z),Y=g(this.name,this.payload,{queue:this.options.queue,timeout:this.options.timeout,tries:this.options.tries,backoff:this.options.backoff});await V.add(Y,{delay:this.options.delay,maxTries:this.options.tries,timeout:this.options.timeout,backoff:this.options.backoff})}async dispatchNow(){await JZ(this.name,{payload:this.payload,context:this.options.context})}}function b5(Z,$){return new v(Z,$)}function l5(Z){let{PendingBatch:$}=(I(),R8(m));return new $(Z)}async function JZ(Z,$={}){let{withTraceId:z}=await import("@stacksjs/router"),V=$.traceId??`job:${Z}:${Math.random().toString(36).slice(2,10)}`;await z(V,async()=>{let K=(await import(e8(`Jobs/${Z}.ts`))).default;if(!K)throw Error(`Job ${Z} does not export a default`);if(typeof K.handle==="function")await K.handle($.payload);else if(typeof K.action==="string"){let{runAction:U}=await import("@stacksjs/actions");await U(K.action)}else if(typeof K.action==="function")await K.action();else if(typeof K==="function")await K($.payload,$.context);else throw Error(`Job ${Z} does not have a valid handler`)})}var g5=!1,u5;var WZ=p(()=>{ZZ();u5={make(Z,$){return new v(Z,$)},async dispatch(Z,$){await new v(Z,$).dispatch()},async dispatchIf(Z,$,z){if(Z)await new v($,z).dispatch()},async dispatchUnless(Z,$,z){if(!Z)await new v($,z).dispatch()},async dispatchNow(Z,$){await new v(Z,$).dispatchNow()},dispatchAfter(Z,$,z){return new v($,z).delay(Z)},async dispatchOnce(Z,$,z){await new v($,z).withIdempotencyKey(Z).dispatch()},async dispatchAfterCommit(Z,$){await new v(Z,$).afterCommit().dispatch()}}});var a5={};h(a5,{unquarantineJob:()=>t5,recordFailureForPoison:()=>d5,quarantineJob:()=>s5,listQuarantined:()=>r5,isQuarantined:()=>n5,hashPayload:()=>HZ});import{createHash as U9}from"crypto";import{db as S}from"@stacksjs/database";function _Z(){if(i5)return;i5=!0,console.warn("[queue/poison] job_quarantine table missing \u2014 poison detection disabled. "+"Run migrations to enable.")}function OZ(Z){let $=Z?.message??"";return $.includes("no such table")||$.includes("doesn't exist")}function HZ(Z){let $;try{$=typeof Z==="string"?Z:JSON.stringify(Z??null)}catch{$=String(Z)}return U9("sha256").update($).digest("hex").slice(0,32)}async function d5(Z,$,z={}){let V=z.maxFailures??5,Y=z.windowMinutes??60,G=HZ($),K=new Date,U=K.toISOString().slice(0,19).replace("T"," ");try{let X=await S.selectFrom("job_quarantine").where("job_name","=",Z).where("payload_hash","=",G).selectAll().executeTakeFirst();if(!X)return await S.insertInto("job_quarantine").values({job_name:Z,payload_hash:G,failure_count:1,window_start:U,quarantined_at:null}).execute(),!1;if(X.quarantined_at)return!0;let L=Date.parse(X.window_start.replace(" ","T")+"Z"),W=K.getTime()-L,P=Y*60*1000;if(Number.isFinite(L)&&W>P)return await S.updateTable("job_quarantine").set({failure_count:1,window_start:U}).where("id","=",X.id).execute(),!1;let A=X.failure_count+1;if(A>=V)return await S.updateTable("job_quarantine").set({failure_count:A,quarantined_at:U}).where("id","=",X.id).execute(),!0;return await S.updateTable("job_quarantine").set({failure_count:A}).where("id","=",X.id).execute(),!1}catch(X){if(OZ(X))return _Z(),!1;throw X}}async function n5(Z,$){let z=HZ($);try{let V=await S.selectFrom("job_quarantine").where("job_name","=",Z).where("payload_hash","=",z).where("quarantined_at","is not",null).select(["id"]).executeTakeFirst();return Boolean(V)}catch(V){if(OZ(V))return _Z(),!1;throw V}}async function s5(Z,$){let z=$===void 0?"*":HZ($),V=new Date().toISOString().slice(0,19).replace("T"," ");try{let Y=await S.selectFrom("job_quarantine").where("job_name","=",Z).where("payload_hash","=",z).select(["id"]).executeTakeFirst();if(Y)await S.updateTable("job_quarantine").set({quarantined_at:V}).where("id","=",Y.id).execute();else await S.insertInto("job_quarantine").values({job_name:Z,payload_hash:z,failure_count:0,window_start:V,quarantined_at:V}).execute()}catch(Y){if(OZ(Y)){_Z();return}throw Y}}async function t5(Z){try{await S.deleteFrom("job_quarantine").where("job_name","=",Z).execute()}catch($){if(OZ($)){_Z();return}throw $}}async function r5(){try{return await S.selectFrom("job_quarantine").where("quarantined_at","is not",null).selectAll().execute()??[]}catch(Z){if(OZ(Z))return _Z(),[];throw Z}}var i5=!1;var rZ=()=>{};var vZ={};h(vZ,{resumeQueue:()=>V8,recordCircuitSuccess:()=>Z8,recordCircuitFailure:()=>$8,pauseQueue:()=>z8,listCircuitState:()=>Y8,isCircuitOpen:()=>e5});import{db as N}from"@stacksjs/database";function u(){if(o5)return;o5=!0,console.warn("[queue/circuit-breaker] queue_circuit_state table missing \u2014 circuit breaker disabled. "+"Run migrations to enable.")}function l(Z){let $=Z?.message??"";return $.includes("no such table")||$.includes("doesn't exist")}async function oZ(Z,$){try{let z=await N.selectFrom("queue_circuit_state").where("queue_name","=",Z).selectAll().executeTakeFirst();if(z)return z;return await N.insertInto("queue_circuit_state").values({queue_name:Z,success_count:0,failure_count:0,window_start:$,paused_at:null,resume_at:null}).execute(),{queue_name:Z,success_count:0,failure_count:0,window_start:$,paused_at:null,resume_at:null}}catch(z){if(l(z))return u(),null;let V=z?.message??"";if(V.includes("UNIQUE constraint")||V.includes("Duplicate entry"))return await N.selectFrom("queue_circuit_state").where("queue_name","=",Z).selectAll().executeTakeFirst();throw z}}async function e5(Z){try{let $=await N.selectFrom("queue_circuit_state").where("queue_name","=",Z).select(["paused_at","resume_at"]).executeTakeFirst();if(!$||!$.paused_at)return!1;if($.resume_at){let z=Date.parse($.resume_at.replace(" ","T")+"Z");if(Number.isFinite(z)&&Date.now()>=z)return await N.updateTable("queue_circuit_state").set({paused_at:null,resume_at:null,success_count:0,failure_count:0}).where("queue_name","=",Z).execute(),!1}return!0}catch($){if(l($))return u(),!1;throw $}}async function Z8(Z,$={}){let z=$.windowSeconds??300,V=new Date().toISOString().slice(0,19).replace("T"," "),Y=await oZ(Z,V);if(!Y)return;try{if(aZ(Y.window_start,z)){await N.updateTable("queue_circuit_state").set({success_count:1,failure_count:0,window_start:V}).where("queue_name","=",Z).execute();return}await N.updateTable("queue_circuit_state").set({success_count:Y.success_count+1}).where("queue_name","=",Z).execute()}catch(G){if(l(G)){u();return}throw G}}async function $8(Z,$={}){let z=$.failureRateThreshold??0.5,V=$.windowSeconds??300,Y=$.pauseSeconds??300,G=$.minObservations??10,K=new Date,U=K.toISOString().slice(0,19).replace("T"," "),X=await oZ(Z,U);if(!X)return!1;if(X.paused_at)return!1;try{let{success_count:L,failure_count:W}=X;if(aZ(X.window_start,V))L=0,W=0;W+=1;let P=L+W,A=P===0?0:W/P;if(P>=G&&A>=z){let H=new Date(K.getTime()+Y*1000).toISOString().slice(0,19).replace("T"," ");return await N.updateTable("queue_circuit_state").set({success_count:L,failure_count:W,window_start:U,paused_at:U,resume_at:H}).where("queue_name","=",Z).execute(),!0}return await N.updateTable("queue_circuit_state").set({success_count:L,failure_count:W,window_start:aZ(X.window_start,V)?U:X.window_start}).where("queue_name","=",Z).execute(),!1}catch(L){if(l(L))return u(),!1;throw L}}function aZ(Z,$){if(!Z)return!0;let z=Date.parse(Z.replace(" ","T")+"Z");if(!Number.isFinite(z))return!0;return Date.now()-z>$*1000}async function z8(Z,$=300){let z=new Date,V=z.toISOString().slice(0,19).replace("T"," "),Y=new Date(z.getTime()+$*1000).toISOString().slice(0,19).replace("T"," ");await oZ(Z,V);try{await N.updateTable("queue_circuit_state").set({paused_at:V,resume_at:Y}).where("queue_name","=",Z).execute()}catch(G){if(l(G)){u();return}throw G}}async function V8(Z){try{await N.updateTable("queue_circuit_state").set({paused_at:null,resume_at:null,success_count:0,failure_count:0}).where("queue_name","=",Z).execute()}catch($){if(l($)){u();return}throw $}}async function Y8(){try{return await N.selectFrom("queue_circuit_state").selectAll().execute()??[]}catch(Z){if(l(Z))return u(),[];throw Z}}var o5=!1;var FZ=()=>{};ZZ();import{env as p8}from"@stacksjs/env";function B5(){return p8.QUEUE_DRIVER||"sync"}class T5{name;description;action;handle;queue;rate;tries;timeout;backoff;backoffConfig;enabled;constructor(Z){this.name=Z.name,this.description=Z.description,this.handle=Z.handle,this.queue=Z.queue,this.rate=Z.rate,this.action=Z.action,this.tries=Z.tries,this.timeout=Z.timeout,this.backoff=Z.backoff,this.backoffConfig=Z.backoffConfig,this.enabled=Z.enabled}async dispatch(Z){let{isFaked:$,getFakeQueue:z}=await Promise.resolve().then(() => fZ);if($()){z()?.dispatch(this.name||"UnknownJob",Z,{queue:this.queue,tries:this.tries,timeout:this.timeout});return}let V=B5();if(V==="sync")return this.dispatchNow(Z);if(V==="redis")return this.dispatchToRedis(Z);if(V==="database")return this.dispatchToDatabase(Z);if(V==="sqs"||V==="memory"||V==="beanstalkd")throw Error(`[queue] Driver "${V}" is not implemented yet. Set QUEUE_DRIVER to one of: redis, database, sync.`);throw Error(`[queue] Unknown QUEUE_DRIVER "${V}". Allowed values: redis, database, sync.`)}async dispatchIf(Z,$){if(Z)return this.dispatch($)}async dispatchUnless(Z,$){if(!Z)return this.dispatch($)}async dispatchAfter(Z,$){let z=B5();if(z==="redis")return this.dispatchToRedis($,{delay:Z});if(z==="database")return this.dispatchToDatabase($,{delay:Z});return await new Promise((V)=>setTimeout(V,Z*1000)),await this.dispatchNow($)}async dispatchNow(Z){if(typeof this.handle==="function")await this.handle(Z);else if(typeof this.action==="string"){let{runAction:$}=await import("@stacksjs/actions");await $(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(Z,$){let z=Math.floor(Date.now()/1000),V=$?.delay?z+$.delay:z,Y=g(this.name,Z,{queue:this.queue,tries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0}),{db:G}=await import("@stacksjs/database");await G.insertInto("jobs").values({queue:this.queue||"default",payload:JSON.stringify(Y),attempts:0,reserved_at:null,available_at:V,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}async dispatchToRedis(Z,$){let{RedisQueue:z}=await Promise.resolve().then(() => (YZ(),VZ)),{queue:V}=await import("@stacksjs/config"),Y=V?.connections?.redis;if(!Y)throw Error("Redis queue connection is not configured. Check config/queue.ts");let G=new z(this.queue||"default",Y),K=g(this.name,Z,{queue:this.queue,tries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0});await G.add(K,{delay:$?.delay,maxTries:typeof this.tries==="number"?this.tries:void 0,timeout:this.timeout,backoff:Array.isArray(this.backoff)?this.backoff:void 0})}}WZ();import{db as r}from"@stacksjs/database";var c5=!1;function CZ(){if(c5)return;c5=!0,console.warn("[queue/dlq] dead_letter_jobs table missing \u2014 DLQ disabled. "+"Run migrations to enable poison-message isolation.")}function qZ(Z){let $=Z?.message??"";return $.includes("no such table")||$.includes("doesn't exist")}async function G9(Z,$,z=1){let V=new Date().toISOString().slice(0,19).replace("T"," ");try{return await r.insertInto("dead_letter_jobs").values({uuid:Z.uuid??crypto.randomUUID(),connection:Z.connection??"database",queue:Z.queue??"default",payload:Z.payload??"{}",exception:Z.exception??"unknown",reason:$,total_failures:z,first_failed_at:Z.failed_at??V,last_failed_at:V,dead_lettered_at:V}).execute(),!0}catch(Y){if(qZ(Y))return CZ(),!1;throw Y}}async function K9(Z={}){try{let $=r.selectFrom("dead_letter_jobs").selectAll();if(Z.queue)$=$.where("queue","=",Z.queue);if(Z.reason)$=$.where("reason","=",Z.reason);if(Z.sinceCutoffMs){let V=new Date(Z.sinceCutoffMs).toISOString().slice(0,19).replace("T"," ");$=$.where("dead_lettered_at",">=",V)}if(Z.limit&&Z.limit>0)$=$.limit(Z.limit);return await $.execute()??[]}catch($){if(qZ($))return CZ(),[];throw $}}async function X9(Z){try{let $=await r.selectFrom("dead_letter_jobs").where("id","=",Z).selectAll().executeTakeFirst();if(!$)return!1;let z=Math.floor(Date.now()/1000);return await r.insertInto("jobs").values({queue:$.queue,payload:$.payload,attempts:0,reserved_at:null,available_at:z,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),await r.deleteFrom("dead_letter_jobs").where("id","=",Z).execute(),!0}catch($){if(qZ($))return CZ(),!1;throw $}}async function L9(Z=30){try{let $=new Date(Date.now()-Z*24*60*60*1000).toISOString().slice(0,19).replace("T"," "),z=await r.deleteFrom("dead_letter_jobs").where("dead_lettered_at","<",$).execute();return Number(z?.numDeletedRows??z?.[0]?.numDeletedRows??z?.affectedRows??0)}catch($){if(qZ($))return CZ(),0;throw $}}rZ();FZ();ZZ();var eZ=(Z)=>`__job_progress__:${Z}`,Z5=(Z)=>`__job_cancel__:${Z}`;async function W9(Z,$,z){let{cache:V}=await import("@stacksjs/cache"),Y=Number.isFinite($)?Math.max(0,Math.min(100,$*($<=1?100:1))):0;await V.set(eZ(Z),{percent:Y,message:z,updatedAt:Date.now()},3600)}async function _9(Z){let{cache:$}=await import("@stacksjs/cache");return await $.get(eZ(Z))??null}async function O9(Z){let{cache:$}=await import("@stacksjs/cache");await $.set(Z5(Z),1,3600)}async function H9(Z){let{cache:$}=await import("@stacksjs/cache");return Boolean(await $.get(Z5(Z)))}async function F9(Z){let{cache:$}=await import("@stacksjs/cache");await Promise.all([$.del(eZ(Z)),$.del(Z5(Z))])}import{log as a}from"@stacksjs/logging";import*as G8 from"@stacksjs/path";class K8{jobs=new Map;initialized=!1;register(Z){this.jobs.set(Z.name,Z),a.debug(`Registered job: ${Z.name} (${Z.type})`)}get(Z){return this.jobs.get(Z)}all(){return Array.from(this.jobs.values())}byQueue(Z){return this.all().filter(($)=>$.config.queue===Z)}scheduled(){return this.all().filter((Z)=>Z.config.rate||Z.config.schedule)}has(Z){return this.jobs.has(Z)}clear(){this.jobs.clear(),this.initialized=!1}setInitialized(Z){this.initialized=Z}isInitialized(){return this.initialized}}var c=new K8;async function $5(Z){let $=Z||G8.userJobsPath(),z=[];try{let V=new Bun.Glob("**/*.{ts,js}"),Y={cwd:$,onlyFiles:!0,absolute:!0};for await(let G of V.scan(Y)){if(G.includes(".test.")||G.includes(".spec.")||G.endsWith("index.ts")||G.endsWith("index.js"))continue;try{let K=await x9(G);if(K)z.push(K),c.register(K)}catch(K){a.warn(`Failed to load job from ${G}: ${K.message}`)}}return c.setInitialized(!0),a.info(`Discovered ${z.length} jobs from ${$}`),z}catch(V){return a.error(`Failed to discover jobs: ${V.message}`),[]}}async function x9(Z){try{let $=await import(Z),z=Z.split("/").pop()?.replace(/\.(ts|js)$/,"")||"UnknownJob";if($.default&&typeof $.default==="function"){let V=$.default;if(typeof V.handle==="function"||typeof V.prototype?.handle==="function"){let Y=V.config||{};return{name:Y.name||z,path:Z,config:{name:Y.name||z,description:Y.description,queue:Y.queue||"default",tries:Y.retries||3,timeout:Y.timeout,withoutOverlapping:Y.withoutOverlapping,schedule:Y.schedule,retryAfter:Y.retryAfter},type:"class",module:V}}}if($.default&&typeof $.default==="object"){let V=$.default;if(typeof V.handle==="function"||typeof V.action==="string")return{name:V.name||z,path:Z,config:{name:V.name||z,description:V.description,queue:V.queue||"default",tries:V.tries||3,backoff:V.backoff,rate:V.rate,timeout:V.timeout||V.timeOut,backoffConfig:V.backoffConfig},type:"function",module:V}}return null}catch($){return a.debug(`Could not load job from ${Z}: ${$.message}`),null}}function P9(Z){return c.get(Z)}function A9(){return c.all()}function z5(){return c.scheduled()}async function D9(Z,$){let z=c.get(Z);if(!z)throw Error(`Job "${Z}" not found. Did you run discoverJobs()?`);try{if(z.type==="class"){if(typeof z.module.handle==="function")return await z.module.handle($);return await new z.module().handle($)}else{if(typeof z.module.handle==="function")return await z.module.handle($);throw Error(`Job "${Z}" does not have a handle method`)}}catch(V){throw a.error(`Failed to execute job "${Z}": ${V.message}`),V}}function Q9(Z){let $=Z.config;return{name:$.name,queue:$.queue,tries:$.tries,backoff:$.backoff,timeout:$.timeout,backoffConfig:$.backoffConfig,rate:$.rate}}import{log as B}from"@stacksjs/logging";q();var U8={checkInterval:60000,preventOverlapping:!0},D={isRunning:!1,isShuttingDown:!1,checkInterval:null,jobs:new Map,config:{...U8}},X8=new Set;function M9(Z,$){if(X8.has(Z))return;X8.add(Z),B.warn(`[scheduler] Cron expression "${Z}" specifies seconds="${$}" but the scheduler `+"ticks at minute granularity \u2014 the seconds field is being ignored. Use a 5-field expression "+"to avoid this warning, or wait for sub-minute scheduling support.")}function B9(Z,$){let z=new Date,V=z.getMinutes(),Y=z.getHours(),G=z.getDate(),K=z.getMonth()+1,U=z.getDay();if($){let H=$.getMinutes(),E=$.getHours(),d=$.getDate();if(H===V&&E===Y&&d===G)return!1}let X=Z.trim().split(/\s+/);if(X.length===6){let H=X[0];if(H&&H!=="0"&&H!=="*")M9(Z,H);X=X.slice(1)}if(X.length<5)return B.warn(`Invalid cron expression: ${Z}`),!1;let[L,W,P,A,_]=X;return xZ(L,V,0,59)&&xZ(W,Y,0,23)&&xZ(P,G,1,31)&&xZ(A,K,1,12)&&xZ(_,U,0,6)}function xZ(Z,$,z,V){if(Z==="*")return!0;if(Z.includes(","))return Z.split(",").map((K)=>Number.parseInt(K.trim(),10)).includes($);if(Z.includes("-")){let[G,K]=Z.split("-").map((U)=>Number.parseInt(U.trim(),10));return $>=G&&$<=K}if(Z.includes("/")){let[G,K]=Z.split("/"),U=Number.parseInt(K,10);if(G==="*")return $%U===0;if(G.includes("-")){let[X,L]=G.split("-").map((W)=>Number.parseInt(W.trim(),10));return $>=X&&$<=L&&($-X)%U===0}}let Y=Number.parseInt(Z,10);return!Number.isNaN(Y)&&$===Y}function W8(Z){let z={"@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 * * * *"}[Z.toLowerCase()];if(z)return z;let V=Z.match(/^Every\.(\w+)$/i);if(V&&V[1]!==void 0){let G=V[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 * *"}[G]||null}let Y=Z.split(/\s+/).length;if(Y>=5&&Y<=6)return Z;return null}function _8(Z){let $=new Date;if(Z.trim().split(/\s+/).length<5)return null;let V=new Date($.getTime()+60000);return V.setSeconds(0),V.setMilliseconds(0),V}async function T9(Z={}){if(D.isRunning){B.warn("Scheduler is already running");return}D.config={...U8,...Z},D.isRunning=!0,D.isShuttingDown=!1,await $5();let $=z5();for(let V of $){let Y=V.config.rate||V.config.schedule;if(Y){let G=W8(Y);if(G)D.jobs.set(V.name,{job:V,lastRun:null,nextRun:_8(G),isRunning:!1}),B.info(`Registered scheduled job: ${V.name} (${G})`);else B.warn(`Invalid schedule for job ${V.name}: ${Y}`)}}if(D.jobs.size===0){B.info("No scheduled jobs found");return}B.info(`Scheduler started with ${D.jobs.size} job(s)`),process.on("SIGINT",()=>V5()),process.on("SIGTERM",()=>V5());let z=!1;D.checkInterval=setInterval(()=>{if(!D.isShuttingDown&&!z)z=!0,L8().catch((V)=>B.error("Scheduler check failed:",V)).finally(()=>{z=!1})},D.config.checkInterval),D.checkInterval?.unref?.(),await L8()}async function L8(){for(let[Z,$]of D.jobs){let z=$.job.config.rate||$.job.config.schedule;if(!z)continue;let V=W8(z);if(!V)continue;if(B9(V,$.lastRun)){if(D.config.preventOverlapping&&$.isRunning){B.debug(`Skipping ${Z}: previous execution still running`);continue}if($.job.config.withoutOverlapping&&$.isRunning){B.debug(`Skipping ${Z}: withoutOverlapping is enabled`);continue}try{$.isRunning=!0,$.lastRun=new Date,$.nextRun=_8(V),B.info(`Dispatching scheduled job: ${Z}`),await b("job:added",{jobId:`scheduled-${Z}-${Date.now()}`,queueName:$.job.config.queue||"default",jobName:Z}),await pZ(Z,{queue:$.job.config.queue||"default",payload:{},maxTries:$.job.config.tries||3,timeout:$.job.config.timeout||60}),$.isRunning=!1,B.info(`Scheduled job ${Z} dispatched to queue`)}catch(Y){$.isRunning=!1,B.error(`Failed to dispatch scheduled job ${Z}:`,Y)}}}}async function V5(){if(!D.isRunning)return;if(B.info("Stopping scheduler..."),D.isShuttingDown=!0,D.checkInterval)clearInterval(D.checkInterval),D.checkInterval=null;D.isRunning=!1,D.jobs.clear(),B.info("Scheduler stopped")}function E9(){return{isRunning:D.isRunning,jobCount:D.jobs.size,jobs:Array.from(D.jobs.entries()).map(([Z,$])=>({name:Z,schedule:$.job.config.rate||$.job.config.schedule,lastRun:$.lastRun,nextRun:$.nextRun,isRunning:$.isRunning}))}}function R9(){return D.isRunning}function N9(){return new Map(D.jobs)}async function S9(Z){let $=D.jobs.get(Z);if(!$)throw Error(`Scheduled job "${Z}" not found`);B.info(`Manually triggering scheduled job: ${Z}`),await pZ(Z,{queue:$.job.config.queue||"default",payload:{},maxTries:$.job.config.tries||3,timeout:$.job.config.timeout||60})}q();q();import{log as J9}from"@stacksjs/logging";var C9={maxPendingWarning:1000,maxPendingCritical:5000,maxFailedWarning:10,maxFailedCritical:100,maxJobAgeWarning:3600,maxJobAgeCritical:86400,maxErrorRateWarning:0.1,maxErrorRateCritical:0.5};async function Y5(Z={}){let $={...C9,...Z},z=[],V=new Date,Y=Math.floor(V.getTime()/1000);try{let{db:G}=await import("@stacksjs/database"),K=await G.selectFrom("jobs").selectAll().execute(),U=await G.selectFrom("failed_jobs").selectAll().execute(),X=new Map;for(let F of K){let Q=F.queue||"default";if(!X.has(Q))X.set(Q,{pending:0,processing:0,delayed:0});let R=X.get(Q);if(F.reserved_at)R.processing++;else if(F.available_at&&F.available_at>Y)R.delayed++;else if(R.pending++,F.created_at){let C=typeof F.created_at==="number"?F.created_at:Math.floor(new Date(F.created_at).getTime()/1000),W5=Y-C;if(!R.oldestAge||W5>R.oldestAge)R.oldestAge=W5}}let L=new Map;for(let F of U){let Q=F.queue||"default";L.set(Q,(L.get(Q)||0)+1)}let W=[],P=new Set([...X.keys(),...L.keys()]),A=$.queues?[...P].filter((F)=>$.queues.includes(F)):[...P],_=0,H=0,E=0,d=0;for(let F of A){let Q=X.get(F)||{pending:0,processing:0,delayed:0},R=L.get(F)||0;_+=Q.pending,H+=Q.processing,E+=Q.delayed,d+=R;let C="healthy";if(Q.pending>=$.maxPendingCritical)C="unhealthy",z.push({level:"critical",message:`Queue "${F}" has ${Q.pending} pending jobs (threshold: ${$.maxPendingCritical})`,queue:F,timestamp:V.toISOString()});else if(Q.pending>=$.maxPendingWarning)C="degraded",z.push({level:"warning",message:`Queue "${F}" has ${Q.pending} pending jobs (threshold: ${$.maxPendingWarning})`,queue:F,timestamp:V.toISOString()});if(R>=$.maxFailedCritical)C="unhealthy",z.push({level:"critical",message:`Queue "${F}" has ${R} failed jobs (threshold: ${$.maxFailedCritical})`,queue:F,timestamp:V.toISOString()});else if(R>=$.maxFailedWarning){if(C==="healthy")C="degraded";z.push({level:"warning",message:`Queue "${F}" has ${R} failed jobs (threshold: ${$.maxFailedWarning})`,queue:F,timestamp:V.toISOString()})}if(Q.oldestAge){if(Q.oldestAge>=$.maxJobAgeCritical)C="unhealthy",z.push({level:"critical",message:`Queue "${F}" has a job waiting for ${Math.floor(Q.oldestAge/3600)} hours`,queue:F,timestamp:V.toISOString()});else if(Q.oldestAge>=$.maxJobAgeWarning){if(C==="healthy")C="degraded";z.push({level:"warning",message:`Queue "${F}" has a job waiting for ${Math.floor(Q.oldestAge/60)} minutes`,queue:F,timestamp:V.toISOString()})}}W.push({name:F,status:C,pending:Q.pending,processing:Q.processing,delayed:Q.delayed,failed:R,oldestJobAge:Q.oldestAge})}let L5=EZ().getMetrics(),A8=L5.throughputPerMinute,D8=L5.averageDuration,U5=_+H+E+d,e=U5>0?d/U5:0,n="healthy";if(W.some((F)=>F.status==="unhealthy"))n="unhealthy";else if(W.some((F)=>F.status==="degraded"))n="degraded";if(e>=$.maxErrorRateCritical)n="unhealthy",z.push({level:"critical",message:`Overall error rate is ${(e*100).toFixed(1)}% (threshold: ${$.maxErrorRateCritical*100}%)`,timestamp:V.toISOString()});else if(e>=$.maxErrorRateWarning){if(n==="healthy")n="degraded";z.push({level:"warning",message:`Overall error rate is ${(e*100).toFixed(1)}% (threshold: ${$.maxErrorRateWarning*100}%)`,timestamp:V.toISOString()})}let Q8=KZ().getAll().map((F)=>({id:F.id,status:F.status,queue:F.queue,processedCount:F.processedCount,failedCount:F.failedCount,lastActivityAt:F.lastActivityAt}));return{status:n,timestamp:V.toISOString(),queues:W,workers:Q8,metrics:{totalPending:_,totalProcessing:H,totalDelayed:E,totalFailed:d,throughputPerMinute:A8,averageProcessingTime:D8,errorRate:e},alerts:z}}catch(G){J9.error("Failed to perform queue health check:",G);let U=KZ().getAll().map((X)=>({id:X.id,status:X.status,queue:X.queue,processedCount:X.processedCount,failedCount:X.failedCount,lastActivityAt:X.lastActivityAt}));return{status:"unhealthy",timestamp:V.toISOString(),queues:[],workers:U,metrics:{totalPending:0,totalProcessing:0,totalDelayed:0,totalFailed:0,throughputPerMinute:0,averageProcessingTime:0,errorRate:0},alerts:[{level:"critical",message:`Health check failed: ${G.message}`,timestamp:V.toISOString()}]}}}function q9(Z={}){return async($)=>{let z=await Y5(Z),V=z.status==="healthy"?200:z.status==="degraded"?207:503;return new Response(JSON.stringify(z,null,2),{status:V,headers:{"Content-Type":"application/json","Cache-Control":"no-store"}})}}async function v9(Z={}){return(await Y5(Z)).status==="healthy"}import{log as J}from"@stacksjs/logging";var k9={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function PZ(Z){return String(Z).replace(/[&<>"']/g,($)=>k9[$]??$)}class G5{config;notificationCount=0;lastResetTime=Date.now();pendingBatch=[];batchTimeout=null;activeFlush=null;constructor(Z){this.config=Z}async notify(Z){if(this.config.filter&&!this.config.filter(Z))return;if(this.config.rateLimit){let $=Date.now();if($-this.lastResetTime>3600000)this.notificationCount=0,this.lastResetTime=$;if(this.notificationCount>=this.config.rateLimit){J.debug("Rate limit reached for failed job notifications");return}}if(this.config.batch){if(this.pendingBatch.push(Z),!this.batchTimeout)this.batchTimeout=setTimeout(()=>{this.batchTimeout=null,this.activeFlush=this.flushBatch().catch(($)=>J.error("Failed to flush notification batch:",$)).finally(()=>{this.activeFlush=null})},this.config.batchInterval||60000),this.batchTimeout.unref?.();return}await this.sendNotifications([Z])}async flushBatch(){if(this.pendingBatch.length===0)return;let Z=[...this.pendingBatch];if(this.pendingBatch=[],this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;await this.sendNotifications(Z)}async shutdown(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.activeFlush)await this.activeFlush.catch(()=>{});await this.flushBatch().catch((Z)=>J.error("Failed to flush notification batch on shutdown:",Z))}async sendNotifications(Z){let $=[];for(let z of this.config.channels)switch(z){case"email":if(this.config.email)$.push(this.sendEmail(Z));break;case"slack":if(this.config.slack)$.push(this.sendSlack(Z));break;case"discord":if(this.config.discord)$.push(this.sendDiscord(Z));break;case"webhook":if(this.config.webhook)$.push(this.sendWebhook(Z));break;case"log":this.logFailures(Z);break}try{await Promise.all($)}catch(z){J.error("Failed to send job failure notifications:",z)}finally{this.notificationCount+=Z.length}}async sendEmail(Z){let $=this.config.email,z=$.subject||`[Queue] ${Z.length} Job(s) Failed`,V=this.formatEmailBody(Z);try{let{mail:Y}=await import("@stacksjs/email");await Y.send({to:Array.isArray($.to)?$.to:[$.to],from:$.from,subject:z,html:V})}catch(Y){J.error("Failed to send email notification:",Y)}}formatEmailBody(Z){let $=Z.map((z)=>`
3
3
  <tr>
4
- <td style="padding: 8px; border: 1px solid #ddd;">${U0(String($.id))}</td>
5
- <td style="padding: 8px; border: 1px solid #ddd;">${U0($.name)}</td>
6
- <td style="padding: 8px; border: 1px solid #ddd;">${U0($.queue)}</td>
7
- <td style="padding: 8px; border: 1px solid #ddd;">${$.attempts}/${$.maxAttempts}</td>
8
- <td style="padding: 8px; border: 1px solid #ddd;">${U0($.failedAt.toISOString())}</td>
9
- <td style="padding: 8px; border: 1px solid #ddd;"><pre style="max-width: 300px; overflow: auto;">${U0($.exception)}</pre></td>
4
+ <td style="padding: 8px; border: 1px solid #ddd;">${PZ(String(z.id))}</td>
5
+ <td style="padding: 8px; border: 1px solid #ddd;">${PZ(z.name)}</td>
6
+ <td style="padding: 8px; border: 1px solid #ddd;">${PZ(z.queue)}</td>
7
+ <td style="padding: 8px; border: 1px solid #ddd;">${z.attempts}/${z.maxAttempts}</td>
8
+ <td style="padding: 8px; border: 1px solid #ddd;">${PZ(z.failedAt.toISOString())}</td>
9
+ <td style="padding: 8px; border: 1px solid #ddd;"><pre style="max-width: 300px; overflow: auto;">${PZ(z.exception)}</pre></td>
10
10
  </tr>
11
11
  `).join("");return`
12
12
  <h2>Failed Jobs Report</h2>
13
- <p>${z.length} job(s) have failed.</p>
13
+ <p>${Z.length} job(s) have failed.</p>
14
14
  <table style="border-collapse: collapse; width: 100%;">
15
15
  <thead>
16
16
  <tr style="background: #f5f5f5;">
@@ -23,9 +23,9 @@ var{defineProperty:_0,getOwnPropertyNames:B1,getOwnPropertyDescriptor:R1}=Object
23
23
  </tr>
24
24
  </thead>
25
25
  <tbody>
26
- ${Z}
26
+ ${$}
27
27
  </tbody>
28
28
  </table>
29
- `}async sendSlack(z){let Z=this.config.slack,$=[{type:"header",text:{type:"plain_text",text:`\uD83D\uDEA8 ${z.length} Job(s) Failed`,emoji:!0}},...z.slice(0,10).map((U)=>({type:"section",text:{type:"mrkdwn",text:`*${U.name}*
30
- Queue: ${U.queue} | Attempts: ${U.attempts}/${U.maxAttempts}
31
- \`\`\`${U.exception.slice(0,200)}\`\`\``}}))];if(z.length>10)$.push({type:"section",text:{type:"mrkdwn",text:`_...and ${z.length-10} more failed jobs_`}});try{let U=await fetch(Z.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel:Z.channel,username:Z.username||"Queue Monitor",icon_emoji:Z.iconEmoji||":warning:",blocks:$})});if(!U.ok)N.error(`Slack notification failed with status ${U.status}`)}catch(U){N.error("Failed to send Slack notification:",U)}}async sendDiscord(z){let Z=this.config.discord,$=z.slice(0,10).map((U)=>({title:`\u274C ${U.name}`,color:15158332,fields:[{name:"Queue",value:U.queue,inline:!0},{name:"Attempts",value:`${U.attempts}/${U.maxAttempts}`,inline:!0},{name:"Failed At",value:U.failedAt.toISOString(),inline:!0},{name:"Exception",value:`\`\`\`${U.exception.slice(0,500)}\`\`\``}],timestamp:U.failedAt.toISOString()}));try{let U=await fetch(Z.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:Z.username||"Queue Monitor",avatar_url:Z.avatarUrl,content:`\uD83D\uDEA8 **${z.length} Job(s) Failed**`,embeds:$})});if(!U.ok)N.error(`Discord notification failed with status ${U.status}`)}catch(U){N.error("Failed to send Discord notification:",U)}}async sendWebhook(z){let Z=this.config.webhook,$={event:"jobs.failed",timestamp:new Date().toISOString(),count:z.length,jobs:z.map((V)=>({id:V.id,name:V.name,queue:V.queue,attempts:V.attempts,maxAttempts:V.maxAttempts,exception:V.exception,failedAt:V.failedAt.toISOString()}))},U={"Content-Type":"application/json",...Z.headers};if(Z.secret){let V=JSON.stringify($),K=await this.generateSignature(V,Z.secret);U["X-Signature"]=K}try{let V=await fetch(Z.url,{method:"POST",headers:U,body:JSON.stringify($)});if(!V.ok)N.error(`Webhook notification failed with status ${V.status}`)}catch(V){N.error("Failed to send webhook notification:",V)}}async generateSignature(z,Z){let $=new TextEncoder,U=await crypto.subtle.importKey("raw",$.encode(Z),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),V=await crypto.subtle.sign("HMAC",U,$.encode(z));return Array.from(new Uint8Array(V)).map((K)=>K.toString(16).padStart(2,"0")).join("")}logFailures(z){for(let Z of z)N.error(`[Queue] Job "${Z.name}" failed on queue "${Z.queue}" after ${Z.attempts} attempts: ${Z.exception}`)}async cleanup(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.pendingBatch.length>0)await this.flushBatch()}}var V0=null;function J9(z){return V0=new y0(z),V0}function w9(){return V0}async function C9(z){if(V0)await V0.notify(z)}w();import{err as q9,ok as P1}from"@stacksjs/error-handling";import{log as H}from"@stacksjs/logging";import m0 from"process";import{env as v9}from"@stacksjs/env";m0.on("unhandledRejection",(z,Z)=>{H.error(`Unhandled Rejection: ${z}`)});m0.on("uncaughtException",(z)=>{H.error(`Uncaught Exception: ${z.message}`)});var h=0,c=!1,S="";function k9(z){if(!z||typeof z!=="object")return!1;let Z=z.status??z.statusCode;if(typeof Z==="number"&&Z>=400&&Z<500)return!0;let $=z.name;if(typeof $==="string"&&($==="ValidationError"||$==="ModelNotFoundError"))return!0;return!1}function y9(){return v9.QUEUE_DRIVER||"sync"}async function I9(z,Z={}){try{H.info("Starting queue processor..."),c=!0,S=`worker-${m0.pid}-${Date.now()}`;let $=Z.concurrency||1,U=y9(),{getWorkerTracker:V,getGlobalMetrics:K}=await Promise.resolve().then(() => (R(),C));if(K(),V().register(S,z||"default"),U==="redis")return H.info("Using Redis queue driver (bun-queue)"),await u9(z||"default",$),P1(void 0);let L;if(z)L=[z];else if(L=await D1(),L.length===0)L=["default"];return H.info(`Processing queues: ${L.join(", ")}`),await f9(L,$),P1(void 0)}catch($){return c=!1,q9($)}}async function D1(){try{let{db:z}=await import("@stacksjs/database"),U=(await z.rawQuery("SELECT DISTINCT queue FROM jobs")).map((V)=>V.queue).filter(Boolean);return U.length>0?U:["default"]}catch{return["default"]}}async function f9(z,Z){H.info("Listening for jobs...");let $=z,U=Date.now(),V=1e4;while(c)try{let K=Date.now();if(K-U>V){try{let L=await D1();if(L.length>0)$=L}catch{}U=K}for(let L of $){let G=[];try{G=await m9(L,Z)}catch{continue}for(let Y of G)try{H.info(`Processing job ${Y.id} from queue "${L}"`),await h9(Y)}catch{H.error(`Unexpected error processing job ${Y.id}`)}}await f0(1000)}catch{await f0(3000)}}async function m9(z,Z){let $=Math.floor(Date.now()/1000),{db:U}=await import("@stacksjs/database"),V=[];for(let K=0;K<Z;K++){let L=await U.selectFrom("jobs").where("queue","=",z).whereNull("reserved_at").where("available_at","<=",$).orderBy("id","asc").limit(1).selectAll().executeTakeFirst();if(!L)break;let G=await U.updateTable("jobs").set({reserved_at:$,attempts:(L.attempts||0)+1}).where("id","=",L.id).where("reserved_at","is",null).executeTakeFirst();if(Number(G?.numUpdatedRows??0)>0)V.push(L)}return V}async function h9(z){let Z=z.id,$=z.queue||"default";h++;let U=Date.now(),{emitQueueEvent:V,getWorkerTracker:K}=await Promise.resolve().then(() => (R(),C)),L=K();L.markActive(S);let G;try{G=JSON.parse(z.payload||"{}").jobName}catch{}await V("job:processing",{jobId:String(Z),queueName:$,jobName:G});try{let M=JSON.parse(z.payload||"{}").payload?._batchId;if(M){let{isBatchCancelled:F}=await Promise.resolve().then(() => (w(),k));if(await F(M)){H.info(`[Queue] Skipping job ${Z} - batch ${M} has been cancelled`),await I0(Z),h--,L.markIdle(S);return}}}catch{}let Y=null;try{let X=JSON.parse(z.payload||"{}");await g9(X)}catch(X){Y=X instanceof Error?X:Error(String(X))}if(!Y)try{await I0(Z),H.info(`[Queue] Job ${Z} completed`),L.recordCompletion(S),await V("job:completed",{jobId:String(Z),queueName:$,duration:Date.now()-U});try{let M=JSON.parse(z.payload||"{}").payload?._batchId;if(M){let{recordBatchJobCompletion:F}=await Promise.resolve().then(() => (w(),k));await F(M)}}catch{}}catch{H.info(`[Queue] Failed to delete completed job ${Z}`)}else{let X=Y.message;H.info(`[Queue] Job ${Z} failed: ${X}`),L.recordFailure(S),await V("job:failed",{jobId:String(Z),queueName:$,error:Y,duration:Date.now()-U,attemptsMade:(z.attempts||0)+1});let M=1,F={};try{F=JSON.parse(z.payload||"{}"),M=F.options?.tries||1}catch{}let x=(z.attempts||0)+1;if(x>=M){H.info(`[Queue] Job ${Z} exceeded max attempts (${x}/${M}), moving to failed_jobs`);try{await j9(z,Y)}catch{H.info(`[Queue] Failed to move job ${Z} to failed_jobs`)}try{await I0(Z)}catch{H.info(`[Queue] Failed to delete failed job ${Z}`)}try{let O=F.payload?._batchId;if(O){let{recordBatchJobFailure:P}=await Promise.resolve().then(() => (w(),k));await P(O,String(Z),Y)}}catch{}}else{let O=F.options?.backoff,P=30;if(Array.isArray(O)&&O.length>0){let I=Math.min(x-1,O.length-1);P=O[I]}else if(typeof O==="number"&&O>0)P=O;H.info(`[Queue] Job ${Z} will be retried in ${P}s (attempt ${x}/${M})`);try{await p9(Z,P),H.info(`[Queue] Job ${Z} released for retry`)}catch{H.info(`[Queue] Failed to release job ${Z} for retry`)}}}h--,L.markIdle(S)}async function I0(z){let{db:Z}=await import("@stacksjs/database");await Z.deleteFrom("jobs").where("id","=",z).execute()}async function p9(z,Z=30){let $=Math.floor(Date.now()/1000)+Z;H.debug(`Releasing job ${z} for retry at ${$}`);try{let{db:U}=await import("@stacksjs/database");await U.updateTable("jobs").set({reserved_at:null,available_at:$}).where("id","=",z).execute(),H.debug(`Job ${z} released successfully`)}catch{H.error(`Failed to release job ${z}`)}}async function j9(z,Z){try{let $=new Date().toISOString().slice(0,19).replace("T"," "),U=crypto.randomUUID(),V=Z.stack||Z.message,{db:K}=await import("@stacksjs/database");await K.insertInto("failed_jobs").values({uuid:U,connection:"database",queue:z.queue,payload:z.payload,exception:V,failed_at:$}).execute()}catch($){H.error("Failed to log failed job:",$)}}async function g9(z){if(z.job&&z.job.startsWith("App\\Jobs\\")){let Z=z.job.replace("App\\Jobs\\",""),{runJob:$}=await Promise.resolve().then(() => (l(),Z0));await $(Z,{payload:z.data})}else if(z.jobName){let{runJob:Z}=await Promise.resolve().then(() => (l(),Z0));await Z(z.jobName,{payload:z.payload})}}async function u9(z,Z){let{RedisQueue:$}=await Promise.resolve().then(() => (t(),r)),{queue:U}=await import("@stacksjs/config"),V=U?.connections?.redis;if(!V)throw Error("Redis queue connection is not configured. Check config/queue.ts");let K=new $(z,V),{emitQueueEvent:L,getWorkerTracker:G}=await Promise.resolve().then(() => (R(),C)),Y=G();K.process(Z,async(X)=>{h++,Y.markActive(S);let M=Date.now(),F=X.data,x=F?.payload?._batchId;if(x)try{let{isBatchCancelled:O}=await Promise.resolve().then(() => (w(),k));if(await O(x)){H.info(`[Queue] Skipping Redis job ${X.id} - batch ${x} has been cancelled`),h--,Y.markIdle(S);return}}catch{}await L("job:processing",{jobId:String(X.id),queueName:z});try{if(F?.jobName){let{runJob:O}=await Promise.resolve().then(() => (l(),Z0));await O(F.jobName,{payload:F.payload})}else if(F?.job&&F.job.startsWith?.("App\\Jobs\\")){let O=F.job.replace("App\\Jobs\\",""),{runJob:P}=await Promise.resolve().then(() => (l(),Z0));await P(O,{payload:F.data})}if(Y.recordCompletion(S),await L("job:completed",{jobId:String(X.id),queueName:z,duration:Date.now()-M}),x)try{let{recordBatchJobCompletion:O}=await Promise.resolve().then(() => (w(),k));await O(x)}catch{}H.info(`[Queue] Redis job ${X.id} completed`)}catch(O){if(Y.recordFailure(S),await L("job:failed",{jobId:String(X.id),queueName:z,error:O instanceof Error?O:Error(String(O)),duration:Date.now()-M}),x)try{let{recordBatchJobFailure:P}=await Promise.resolve().then(() => (w(),k));await P(x,String(X.id),O instanceof Error?O:Error(String(O)))}catch{}if(H.error(`[Queue] Redis job ${X.id} failed: ${O}`),k9(O)){H.info(`[Queue] Redis job ${X.id} hit a non-retryable error \u2014 skipping retry`);return}throw O}finally{h--,Y.markIdle(S)}}),H.info(`Listening for Redis jobs on queue "${z}" with concurrency ${Z}...`);while(c)await f0(1000);await K.close()}async function b9(){if(c=!1,S){let{getWorkerTracker:z}=await Promise.resolve().then(() => (R(),C));z().unregister(S)}H.info("Queue processor stopped")}async function l9(){let{db:z}=await import("@stacksjs/database"),Z=await z.selectFrom("failed_jobs").selectAll().execute();for(let $ of Z)await S1(Number($.id))}async function S1(z){let Z=Math.floor(Date.now()/1000),$=new Date().toISOString().slice(0,19).replace("T"," "),{db:U}=await import("@stacksjs/database"),K=(await U.selectFrom("failed_jobs").where("id","=",z).selectAll().execute())[0];if(!K)throw Error(`Failed job ${z} not found`);await U.insertInto("jobs").values({queue:K.queue,payload:K.payload,attempts:0,reserved_at:null,available_at:Z,created_at:$}).execute(),await U.deleteFrom("failed_jobs").where("id","=",z).execute(),H.info(`Failed job ${z} has been re-queued`)}function i9(){return h}function c9(){return c}function f0(z){return new Promise((Z)=>setTimeout(Z,z))}async function Ez(){let{RedisQueue:z}=await Promise.resolve().then(() => (t(),r));return z}export{o0 as withEvents,S9 as triggerJob,H9 as toJobOptions,v0 as stopScheduler,b9 as stopProcessor,F9 as startScheduler,I9 as startProcessor,K9 as setJobProgress,c0 as runTestJob,z0 as runJob,S1 as retryFailedJob,H0 as restore,B0 as recordBatchJobFailure,N0 as recordBatchJobCompletion,a0 as onQueueEvent,C9 as notifyJobFailed,m as jobRegistry,_1 as jobBatch,O1 as job,c9 as isWorkerRunning,P9 as isSchedulerRunning,B9 as isQueueHealthy,G9 as isJobCancelled,l0 as isFaked,X1 as isBatchCancelled,a as getWorkerTracker,T9 as getSchedulerStatus,C0 as getScheduledJobs,D9 as getRegisteredJobs,Ez as getRedisQueue,u as getQueueEvents,L9 as getJobProgress,W9 as getJob,X0 as getGlobalMetrics,b0 as getFakeQueue,w9 as getFailedJobNotifier,o as getBatchCallbacks,_9 as getAllJobs,i9 as getActiveJobCount,M0 as fake,d0 as expectJobToFail,M9 as executeJob,l9 as executeFailedJobs,f as emitQueueEvent,w0 as discoverJobs,i0 as createQueueTester,N9 as createHealthCheckHandler,J9 as configureFailedJobNotifications,X9 as clearJobState,k0 as checkQueueHealth,Y9 as cancelJob,L0 as QueueTester,G0 as QueueMetrics,Y0 as QueueEvents,W0 as PendingBatch,e0 as OnQueueEvent,W1 as Jobs,t0 as Job,y0 as FailedJobNotifier,q as DispatchedBatch,P0 as Batch};
29
+ `}async sendSlack(Z){let $=this.config.slack,z=[{type:"header",text:{type:"plain_text",text:`\uD83D\uDEA8 ${Z.length} Job(s) Failed`,emoji:!0}},...Z.slice(0,10).map((V)=>({type:"section",text:{type:"mrkdwn",text:`*${V.name}*
30
+ Queue: ${V.queue} | Attempts: ${V.attempts}/${V.maxAttempts}
31
+ \`\`\`${V.exception.slice(0,200)}\`\`\``}}))];if(Z.length>10)z.push({type:"section",text:{type:"mrkdwn",text:`_...and ${Z.length-10} more failed jobs_`}});try{let V=await fetch($.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel:$.channel,username:$.username||"Queue Monitor",icon_emoji:$.iconEmoji||":warning:",blocks:z})});if(!V.ok)J.error(`Slack notification failed with status ${V.status}`)}catch(V){J.error("Failed to send Slack notification:",V)}}async sendDiscord(Z){let $=this.config.discord,z=Z.slice(0,10).map((V)=>({title:`\u274C ${V.name}`,color:15158332,fields:[{name:"Queue",value:V.queue,inline:!0},{name:"Attempts",value:`${V.attempts}/${V.maxAttempts}`,inline:!0},{name:"Failed At",value:V.failedAt.toISOString(),inline:!0},{name:"Exception",value:`\`\`\`${V.exception.slice(0,500)}\`\`\``}],timestamp:V.failedAt.toISOString()}));try{let V=await fetch($.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$.username||"Queue Monitor",avatar_url:$.avatarUrl,content:`\uD83D\uDEA8 **${Z.length} Job(s) Failed**`,embeds:z})});if(!V.ok)J.error(`Discord notification failed with status ${V.status}`)}catch(V){J.error("Failed to send Discord notification:",V)}}async sendWebhook(Z){let $=this.config.webhook,z={event:"jobs.failed",timestamp:new Date().toISOString(),count:Z.length,jobs:Z.map((Y)=>({id:Y.id,name:Y.name,queue:Y.queue,attempts:Y.attempts,maxAttempts:Y.maxAttempts,exception:Y.exception,failedAt:Y.failedAt.toISOString()}))},V={"Content-Type":"application/json",...$.headers};if($.secret){let Y=JSON.stringify(z),G=await this.generateSignature(Y,$.secret);V["X-Signature"]=G}try{let Y=await fetch($.url,{method:"POST",headers:V,body:JSON.stringify(z)});if(!Y.ok)J.error(`Webhook notification failed with status ${Y.status}`)}catch(Y){J.error("Failed to send webhook notification:",Y)}}async generateSignature(Z,$){let z=new TextEncoder,V=await crypto.subtle.importKey("raw",z.encode($),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),Y=await crypto.subtle.sign("HMAC",V,z.encode(Z));return Array.from(new Uint8Array(Y)).map((G)=>G.toString(16).padStart(2,"0")).join("")}logFailures(Z){for(let $ of Z)J.error(`[Queue] Job "${$.name}" failed on queue "${$.queue}" after ${$.attempts} attempts: ${$.exception}`)}async cleanup(){if(this.batchTimeout)clearTimeout(this.batchTimeout),this.batchTimeout=null;if(this.pendingBatch.length>0)await this.flushBatch()}}var AZ=null;function I9(Z){return AZ=new G5(Z),AZ}function y9(){return AZ}async function w9(Z){if(AZ)await AZ.notify(Z)}I();ZZ();import{err as f9,ok as O8}from"@stacksjs/error-handling";import{log as x}from"@stacksjs/logging";import DZ from"process";import{env as m9}from"@stacksjs/env";DZ.on("unhandledRejection",(Z,$)=>{x.error(`Unhandled Rejection: ${Z}`)});DZ.on("uncaughtException",(Z)=>{x.error(`Uncaught Exception: ${Z.message}`)});var j=0,o=!1,T="",i=new Set;function j9(Z){return i.add(Z),Z.finally(()=>i.delete(Z)),Z}function h9(){let Z=DZ.env.STACKS_QUEUE_RESERVATION_TTL_SEC,$=Z===void 0?Number.NaN:Number.parseInt(Z,10);return Number.isFinite($)&&$>0?$:3600}function p9(){let Z=DZ.env.STACKS_QUEUE_SWEEP_INTERVAL_SEC,$=Z===void 0?Number.NaN:Number.parseInt(Z,10);return(Number.isFinite($)&&$>0?$:60)*1000}async function H8(){let Z=h9(),$=Math.floor(Date.now()/1000)-Z,z=Math.floor(Date.now()/1000);try{let{db:V}=await import("@stacksjs/database"),Y=await V.updateTable("jobs").set({reserved_at:null,available_at:z}).where("reserved_at","<=",$).executeTakeFirst(),G=GZ(Y);if(G>0)x.warn(`[queue] Requeued ${G} job(s) whose reservation exceeded the ${Z}s TTL \u2014 likely victims of a worker crash. Set `+"STACKS_QUEUE_RESERVATION_TTL_SEC to tune.");return G}catch(V){return x.error("[queue] Reservation sweep failed",{reason:V instanceof Error?V.message:String(V)}),0}}function g9(Z){if(!Z||typeof Z!=="object")return!1;let $=Z.status??Z.statusCode;if(typeof $==="number"&&$>=400&&$<500)return!0;let z=Z.name;if(typeof z==="string"&&(z==="ValidationError"||z==="ModelNotFoundError"))return!0;return!1}function b9(){return m9.QUEUE_DRIVER||"sync"}async function u9(Z,$={}){try{x.info("Starting queue processor..."),o=!0,T=`worker-${DZ.pid}-${Date.now()}`;let z=$.concurrency||1,V=b9(),{getWorkerTracker:Y,getGlobalMetrics:G}=await Promise.resolve().then(() => (q(),y));if(G(),Y().register(T,Z||"default"),V==="redis")return x.info("Using Redis queue driver (bun-queue)"),await r9(Z||"default",z),O8(void 0);let K;if(Z)K=[Z];else if(K=await x8(),K.length===0)K=["default"];return x.info(`Processing queues: ${K.join(", ")}`),await l9(K,z),O8(void 0)}catch(z){return o=!1,f9(z)}}async function x8(){try{let{db:Z}=await import("@stacksjs/database"),z=(await Z.selectFrom("jobs").select("queue").distinct().execute()).map((V)=>V.queue).filter((V)=>Boolean(V));return z.length>0?z:["default"]}catch{return["default"]}}async function l9(Z,$){x.info("Listening for jobs...");let z=Z,V=Date.now(),Y=1e4,G=Date.now(),K=p9();await H8();while(o)try{let U=Date.now();if(U-V>Y){try{let X=await x8();if(X.length>0)z=X}catch{}V=U}if(U-G>K)await H8(),G=U;for(let X of z){try{let{isCircuitOpen:W}=await Promise.resolve().then(() => (FZ(),vZ));if(await W(X))continue}catch{}let L=[];try{L=await c9(X,$)}catch{continue}for(let W of L)try{x.info(`Processing job ${W.id} from queue "${X}"`),await j9(i9(W))}catch{x.error(`Unexpected error processing job ${W.id}`)}}await X5(1000)}catch{await X5(3000)}}async function c9(Z,$){let z=Math.floor(Date.now()/1000),{db:V}=await import("@stacksjs/database"),Y=[];for(let G=0;G<$;G++){let K=await V.selectFrom("jobs").where("queue","=",Z).whereNull("reserved_at").where("available_at","<=",z).orderBy("id","asc").limit(1).selectAll().executeTakeFirst();if(!K)break;let U=await V.updateTable("jobs").set({reserved_at:z,attempts:(K.attempts||0)+1}).where("id","=",K.id).where("reserved_at","is",null).executeTakeFirst();if(GZ(U)>0)Y.push(K)}return Y}async function i9(Z){let $=Z.id,z=Z.queue||"default";j++;let V=Date.now(),{emitQueueEvent:Y,getWorkerTracker:G}=await Promise.resolve().then(() => (q(),y)),K=G();K.markActive(T);let U;try{U=JSON.parse(Z.payload||"{}").jobName}catch{}await Y("job:processing",{jobId:String($),queueName:z,jobName:U});try{let W=JSON.parse(Z.payload||"{}").payload?._batchId;if(W){let{isBatchCancelled:P}=await Promise.resolve().then(() => (I(),m));if(await P(W)){x.info(`[Queue] Skipping job ${$} - batch ${W} has been cancelled`),await K5($),j--,K.markIdle(T);return}}}catch{}let X=null;try{let L=JSON.parse(Z.payload||"{}"),W=s9(L);if(W===void 0)await F8(L);else await t9(F8(L),W*1000,`Job ${$} exceeded ${W}s timeout`)}catch(L){X=L instanceof Error?L:Error(String(L))}if(!X)try{await K5($),x.info(`[Queue] Job ${$} completed`),K.recordCompletion(T);try{let{recordCircuitSuccess:L}=await Promise.resolve().then(() => (FZ(),vZ));await L(Z.queue??"default")}catch{}await Y("job:completed",{jobId:String($),queueName:z,duration:Date.now()-V});try{let W=JSON.parse(Z.payload||"{}").payload?._batchId;if(W){let{recordBatchJobCompletion:P}=await Promise.resolve().then(() => (I(),m));await P(W)}}catch{}}catch{x.info(`[Queue] Failed to delete completed job ${$}`)}else{let L=X.message;x.info(`[Queue] Job ${$} failed: ${L}`),K.recordFailure(T),await Y("job:failed",{jobId:String($),queueName:z,error:X,duration:Date.now()-V,attemptsMade:(Z.attempts||0)+1});let W=1,P={};try{P=JSON.parse(Z.payload||"{}"),W=P.options?.tries||1}catch{}let A=(Z.attempts||0)+1;if(A>=W){x.info(`[Queue] Job ${$} exceeded max attempts (${A}/${W}), moving to failed_jobs`);try{await n9(Z,X)}catch{x.info(`[Queue] Failed to move job ${$} to failed_jobs`)}try{await K5($)}catch{x.info(`[Queue] Failed to delete failed job ${$}`)}try{let{recordFailureForPoison:_}=await Promise.resolve().then(() => (rZ(),a5)),{recordCircuitFailure:H}=await Promise.resolve().then(() => (FZ(),vZ)),E=P?.jobName??"unknown";await _(E,P?.payload),await H(Z.queue??"default")}catch{}try{let _=P.payload?._batchId;if(_){let{recordBatchJobFailure:H}=await Promise.resolve().then(() => (I(),m));await H(_,String($),X)}}catch{}}else{let _=P.options?.backoff,H=30;if(Array.isArray(_)&&_.length>0){let E=Math.min(A-1,_.length-1);H=_[E]}else if(typeof _==="number"&&_>0)H=_;x.info(`[Queue] Job ${$} will be retried in ${H}s (attempt ${A}/${W})`);try{await d9($,H),x.info(`[Queue] Job ${$} released for retry`)}catch{x.info(`[Queue] Failed to release job ${$} for retry`)}}}j--,K.markIdle(T)}async function K5(Z){let{db:$}=await import("@stacksjs/database");await $.deleteFrom("jobs").where("id","=",Z).execute()}async function d9(Z,$=30){let z=Math.floor(Date.now()/1000)+$;x.debug(`Releasing job ${Z} for retry at ${z}`);try{let{db:V}=await import("@stacksjs/database");await V.updateTable("jobs").set({reserved_at:null,available_at:z}).where("id","=",Z).execute(),x.debug(`Job ${Z} released successfully`)}catch{x.error(`Failed to release job ${Z}`)}}async function n9(Z,$){try{let z=new Date().toISOString().slice(0,19).replace("T"," "),V=crypto.randomUUID(),Y=$.stack||$.message,{db:G}=await import("@stacksjs/database");await G.insertInto("failed_jobs").values({uuid:V,connection:"database",queue:Z.queue,payload:Z.payload,exception:Y,failed_at:z}).execute()}catch(z){x.error("Failed to log failed job:",z)}}function s9(Z){if(!Z||typeof Z!=="object")return;let z=Z.options?.timeout;if(typeof z!=="number"||!Number.isFinite(z)||z<=0)return;return z}async function t9(Z,$,z){let V,Y=new Promise((G,K)=>{V=setTimeout(()=>K(Error(z)),$)});try{return await Promise.race([Z,Y])}finally{if(V!==void 0)clearTimeout(V)}}async function F8(Z){let $=QZ(Z);if(!$.ok)throw Error(`[queue] Cannot deserialize job envelope: ${$.reason}`+($.detail?` (${$.detail})`:""));let{runJob:z}=await Promise.resolve().then(() => (WZ(),SZ));await z($.envelope.jobName,{payload:$.envelope.payload})}async function r9(Z,$){let{RedisQueue:z}=await Promise.resolve().then(() => (YZ(),VZ)),{queue:V}=await import("@stacksjs/config"),Y=V?.connections?.redis;if(!Y)throw Error("Redis queue connection is not configured. Check config/queue.ts");let G=new z(Z,Y),{emitQueueEvent:K,getWorkerTracker:U}=await Promise.resolve().then(() => (q(),y)),X=U();G.process($,async(L)=>{j++,X.markActive(T);let W=Date.now(),P=QZ(L.data);if(!P.ok){j--,X.markIdle(T),x.error(`[Queue] Skipping Redis job ${L.id} - unparseable envelope: ${P.reason}${P.detail?` (${P.detail})`:""}`);return}let A=P.envelope,_=A.payload?._batchId;if(_)try{let{isBatchCancelled:H}=await Promise.resolve().then(() => (I(),m));if(await H(_)){x.info(`[Queue] Skipping Redis job ${L.id} - batch ${_} has been cancelled`),j--,X.markIdle(T);return}}catch{}await K("job:processing",{jobId:String(L.id),queueName:Z});try{let{runJob:H}=await Promise.resolve().then(() => (WZ(),SZ));if(await H(A.jobName,{payload:A.payload}),X.recordCompletion(T),await K("job:completed",{jobId:String(L.id),queueName:Z,duration:Date.now()-W}),_)try{let{recordBatchJobCompletion:E}=await Promise.resolve().then(() => (I(),m));await E(_)}catch{}x.info(`[Queue] Redis job ${L.id} completed`)}catch(H){if(X.recordFailure(T),await K("job:failed",{jobId:String(L.id),queueName:Z,error:H instanceof Error?H:Error(String(H)),duration:Date.now()-W}),_)try{let{recordBatchJobFailure:E}=await Promise.resolve().then(() => (I(),m));await E(_,String(L.id),H instanceof Error?H:Error(String(H)))}catch{}if(x.error(`[Queue] Redis job ${L.id} failed: ${H}`),g9(H)){x.info(`[Queue] Redis job ${L.id} hit a non-retryable error \u2014 skipping retry`);return}throw H}finally{j--,X.markIdle(T)}}),x.info(`Listening for Redis jobs on queue "${Z}" with concurrency ${$}...`);while(o)await X5(1000);await G.close()}async function a9(Z={}){let $=Z.graceMs??1e4;if(o=!1,i.size>0&&$>0){x.info(`[queue] Draining ${i.size} in-flight job(s) (grace ${$}ms)`);let z=Promise.allSettled([...i]),V=new Promise((G)=>setTimeout(()=>G("timeout"),$));if(await Promise.race([z.then(()=>"drained"),V])==="timeout"&&i.size>0)x.warn(`[queue] Drain timed out with ${i.size} job(s) still active. Their reservations will be reclaimed by the next worker's sweep (Q-2).`)}if(T){let{getWorkerTracker:z}=await Promise.resolve().then(() => (q(),y));z().unregister(T)}x.info("Queue processor stopped")}async function o9(){let{db:Z}=await import("@stacksjs/database"),$=await Z.selectFrom("failed_jobs").selectAll().execute();for(let z of $)await P8(Number(z.id))}async function P8(Z){let $=Math.floor(Date.now()/1000),z=new Date().toISOString().slice(0,19).replace("T"," "),{db:V}=await import("@stacksjs/database"),G=(await V.selectFrom("failed_jobs").where("id","=",Z).selectAll().execute())[0];if(!G)throw Error(`Failed job ${Z} not found`);await V.insertInto("jobs").values({queue:G.queue,payload:G.payload,attempts:0,reserved_at:null,available_at:$,created_at:z}).execute(),await V.deleteFrom("failed_jobs").where("id","=",Z).execute(),x.info(`Failed job ${Z} has been re-queued`)}function e9(){return j}function Z$(){return o}function X5(Z){return new Promise(($)=>setTimeout($,Z))}async function d$(){let{RedisQueue:Z}=await Promise.resolve().then(() => (YZ(),VZ));return Z}export{J5 as withEvents,t5 as unquarantineJob,S9 as triggerJob,Q9 as toJobOptions,V5 as stopScheduler,a9 as stopProcessor,T9 as startScheduler,u9 as startProcessor,W9 as setJobProgress,A5 as runTestJob,JZ as runJob,P8 as retryFailedJob,X9 as retryDeadLetterJob,V8 as resumeQueue,wZ as restore,d5 as recordFailureForPoison,hZ as recordDispatchedKey,Z8 as recordCircuitSuccess,$8 as recordCircuitFailure,tZ as recordBatchJobFailure,sZ as recordBatchJobCompletion,s5 as quarantineJob,L9 as purgeDeadLetterJobs,z8 as pauseQueue,QZ as parseEnvelope,S5 as onQueueEvent,w9 as notifyJobFailed,G9 as moveToDeadLetter,r5 as listQuarantined,K9 as listDeadLetterJobs,Y8 as listCircuitState,c as jobRegistry,l5 as jobBatch,b5 as job,Z$ as isWorkerRunning,R9 as isSchedulerRunning,v9 as isQueueHealthy,n5 as isQuarantined,H9 as isJobCancelled,x5 as isFaked,e5 as isCircuitOpen,p5 as isBatchCancelled,HZ as hashPayload,jZ as hasDispatchedKey,KZ as getWorkerTracker,E9 as getSchedulerStatus,z5 as getScheduledJobs,N9 as getRegisteredJobs,d$ as getRedisQueue,t as getQueueEvents,_9 as getJobProgress,P9 as getJob,EZ as getGlobalMetrics,F5 as getFakeQueue,y9 as getFailedJobNotifier,XZ as getBatchCallbacks,A9 as getAllJobs,e9 as getActiveJobCount,yZ as fake,D5 as expectJobToFail,D9 as executeJob,o9 as executeFailedJobs,b as emitQueueEvent,$5 as discoverJobs,P5 as createQueueTester,q9 as createHealthCheckHandler,g as createEnvelope,I9 as configureFailedJobNotifications,F9 as clearJobState,C8 as clearEnvelopeWarnings,Y5 as checkQueueHealth,O9 as cancelJob,MZ as QueueTester,TZ as QueueMetrics,BZ as QueueEvents,NZ as PendingBatch,C5 as OnQueueEvent,u5 as Jobs,T5 as Job,J8 as JOB_ENVELOPE_VERSION,G5 as FailedJobNotifier,w as DispatchedBatch,cZ as Batch};
@@ -38,9 +38,9 @@ export declare class Job {
38
38
  backoffConfig: JobOptions['backoffConfig'];
39
39
  enabled: JobOptions['enabled'];
40
40
  constructor(options: JobOptions & { queue?: string; timeout?: number });
41
- dispatch(payload?: any): Promise<void>;
42
- dispatchIf(condition: boolean, payload?: any): Promise<void>;
43
- dispatchUnless(condition: boolean, payload?: any): Promise<void>;
44
- dispatchAfter(delaySeconds: number, payload?: any): Promise<void>;
45
- dispatchNow(payload?: any): Promise<void>;
41
+ dispatch<T = unknown>(payload?: T): Promise<void>;
42
+ dispatchIf<T = unknown>(condition: boolean, payload?: T): Promise<void>;
43
+ dispatchUnless<T = unknown>(condition: boolean, payload?: T): Promise<void>;
44
+ dispatchAfter<T = unknown>(delaySeconds: number, payload?: T): Promise<void>;
45
+ dispatchNow<T = unknown>(payload?: T): Promise<void>;
46
46
  }
@@ -42,6 +42,9 @@ export declare interface BatchRecord {
42
42
  cancelled_at: string | null
43
43
  created_at: string
44
44
  finished_at: string | null
45
+ then_handler?: string | null
46
+ catch_handler?: string | null
47
+ finally_handler?: string | null
45
48
  }
46
49
  /**
47
50
  * Batch options for callbacks and behavior
@@ -54,11 +57,40 @@ export declare interface BatchOptions {
54
57
  catchCallbacks: Array<(batch: PendingBatch | DispatchedBatch, error: Error) => Promise<void> | void>
55
58
  finallyCallbacks: Array<(batch: PendingBatch | DispatchedBatch) => Promise<void> | void>
56
59
  progressCallbacks: Array<(batch: PendingBatch | DispatchedBatch) => Promise<void> | void>
60
+ thenHandler?: PersistentBatchHandler
61
+ catchHandler?: PersistentBatchHandler
62
+ finallyHandler?: PersistentBatchHandler
57
63
  }
58
64
  /**
59
65
  * Batch status
60
66
  */
61
67
  export type BatchStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
68
+ /**
69
+ * Persistent batch handler shape (stacksjs/stacks#1883).
70
+ *
71
+ * Survives worker restart by being JSON-serialized into the
72
+ * `job_batches.then_handler` / `catch_handler` / `finally_handler`
73
+ * columns. The winning worker (the one that flips
74
+ * `pending_jobs = 0 → finished_at`) reads + dispatches these on
75
+ * completion, so a batch that finishes after the dispatching
76
+ * process died still fires its terminal callbacks.
77
+ *
78
+ * Two shapes:
79
+ * - `job` — dispatch a queued job by name; the worker fires
80
+ * the job through `Jobs.dispatch(name, payload)`
81
+ * - `module` — load a module and invoke a named export; payload
82
+ * is passed through. Useful for in-process side-
83
+ * effects that don't warrant their own job (e.g.
84
+ * cleanup helpers, simple notifications).
85
+ *
86
+ * Inline function callbacks (the existing `.then(fn)` API) stay
87
+ * supported as a process-local convenience — they continue to live
88
+ * in the in-memory registry alongside any persistent handler. The
89
+ * winning worker fires both: any in-memory callbacks AND the
90
+ * persistent handler if one was registered.
91
+ */
92
+ export type PersistentBatchHandler = | { kind: 'job', name: string, payload?: unknown }
93
+ | { kind: 'module', module: string, export: string, payload?: unknown }
62
94
  /**
63
95
  * PendingBatch - a batch that has been configured but not yet dispatched
64
96
  */
@@ -71,6 +103,9 @@ export declare class PendingBatch {
71
103
  catch(callback: (batch: PendingBatch | DispatchedBatch, error: Error) => Promise<void> | void): this;
72
104
  finally(callback: (batch: PendingBatch | DispatchedBatch) => Promise<void> | void): this;
73
105
  progress(callback: (batch: PendingBatch | DispatchedBatch) => Promise<void> | void): this;
106
+ thenHandler(handler: PersistentBatchHandler): this;
107
+ catchHandler(handler: PersistentBatchHandler): this;
108
+ finallyHandler(handler: PersistentBatchHandler): this;
74
109
  dispatch(): Promise<DispatchedBatch>;
75
110
  getJobs(): BatchableJob[];
76
111
  getOptions(): BatchOptions;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Is the queue currently paused? Returns `true` when `paused_at`
3
+ * is set and `resume_at` hasn't passed yet. Also auto-clears the
4
+ * paused state when the cooldown elapses so the next call sees
5
+ * the queue as available again.
6
+ */
7
+ export declare function isCircuitOpen(queue: string): Promise<boolean>;
8
+ /**
9
+ * Record a successful job. Maintained alongside failure_count so
10
+ * the failure-rate calculation is meaningful. Window resets after
11
+ * `windowSeconds` so an old failure burst doesn't permanently
12
+ * skew the rate.
13
+ */
14
+ export declare function recordCircuitSuccess(queue: string, config?: CircuitBreakerConfig): Promise<void>;
15
+ /**
16
+ * Record a failed job. After incrementing, evaluates whether the
17
+ * failure rate has crossed the threshold and trips the breaker if
18
+ * so. Returns `true` when this call triggered the trip.
19
+ */
20
+ export declare function recordCircuitFailure(queue: string, config?: CircuitBreakerConfig): Promise<boolean>;
21
+ /**
22
+ * Manually pause a queue for the given cooldown (default matches
23
+ * the configured `pauseSeconds`). Useful when an operator knows
24
+ * downstream is broken and wants to skip jobs immediately rather
25
+ * than wait for the auto-trip.
26
+ */
27
+ export declare function pauseQueue(queue: string, pauseSeconds?: number): Promise<void>;
28
+ /**
29
+ * Lift the pause on a queue (manual resume). Workers resume
30
+ * processing on the next poll cycle.
31
+ */
32
+ export declare function resumeQueue(queue: string): Promise<void>;
33
+ /**
34
+ * Snapshot the current circuit state for every tracked queue.
35
+ * Used by `queue:status` to show paused queues alongside
36
+ * pending/failed counts.
37
+ */
38
+ export declare function listCircuitState(): Promise<CircuitRow[]>;
39
+ export declare interface CircuitBreakerConfig {
40
+ failureRateThreshold?: number
41
+ windowSeconds?: number
42
+ pauseSeconds?: number
43
+ minObservations?: number
44
+ }
45
+ declare interface CircuitRow {
46
+ queue_name: string
47
+ success_count: number
48
+ failure_count: number
49
+ window_start: string
50
+ paused_at: string | null
51
+ resume_at: string | null
52
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Move a job from `failed_jobs` into `dead_letter_jobs`. Called
3
+ * by the retry path when a job re-fails, and by the poison/
4
+ * circuit-breaker paths when they intercept a job pre-dispatch.
5
+ *
6
+ * Returns `true` on success, `false` when the DLQ table is missing
7
+ * (caller falls back to today's behavior — leave the row in
8
+ * `failed_jobs`).
9
+ */
10
+ export declare function moveToDeadLetter(failedJob: {
11
+ uuid?: string
12
+ connection?: string
13
+ queue?: string
14
+ payload?: string
15
+ exception?: string
16
+ failed_at?: string | null
17
+ }, reason: DeadLetterReason, totalFailures?: number): Promise<boolean>;
18
+ export declare function listDeadLetterJobs(filter?: ListDeadLetterFilter): Promise<DeadLetterRecord[]>;
19
+ /**
20
+ * Pull a DLQ row back into the active `jobs` queue. Resets
21
+ * `attempts` to 0 and `reserved_at` to null so the worker picks
22
+ * it up on the next poll cycle. The original DLQ row is deleted
23
+ * after the re-enqueue.
24
+ *
25
+ * Returns `true` when the job was found + re-enqueued, `false`
26
+ * when the id didn't exist OR the DLQ table is missing.
27
+ */
28
+ export declare function retryDeadLetterJob(id: number): Promise<boolean>;
29
+ /**
30
+ * Delete DLQ rows older than `olderThanDays`. Useful for retention
31
+ * cleanup — apps with high failure volume can prune rows after the
32
+ * configured window. Default 30 days.
33
+ *
34
+ * Returns the number of rows deleted; 0 when the table is missing.
35
+ */
36
+ export declare function purgeDeadLetterJobs(olderThanDays?: number): Promise<number>;
37
+ export declare interface DeadLetterRecord {
38
+ id: number
39
+ uuid: string
40
+ connection: string
41
+ queue: string
42
+ payload: string
43
+ exception: string
44
+ reason: DeadLetterReason
45
+ total_failures: number
46
+ first_failed_at: string | null
47
+ last_failed_at: string | null
48
+ dead_lettered_at: string
49
+ }
50
+ /**
51
+ * List dead-letter rows. Filters compose: `queue` narrows by
52
+ * queue name, `since` filters by `dead_lettered_at` cutoff. Limit
53
+ * defaults to 100 to keep CLI output manageable.
54
+ */
55
+ export declare interface ListDeadLetterFilter {
56
+ queue?: string
57
+ reason?: DeadLetterReason
58
+ sinceCutoffMs?: number
59
+ limit?: number
60
+ }
61
+ export type DeadLetterReason = 'repeat-failure' | 'poison-detected' | 'circuit-broken' | 'manual';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Construct a fresh envelope for an outgoing dispatch. Both drivers
3
+ * call this — single source of truth for the on-the-wire shape.
4
+ */
5
+ export declare function createEnvelope(jobName: string, payload: unknown, options?: JobEnvelopeOptions): JobEnvelope;
6
+ /**
7
+ * Test-only: clear the warn-once state. Production callers never
8
+ * need this.
9
+ */
10
+ export declare function clearEnvelopeWarnings(): void;
11
+ /**
12
+ * Parse an opaque value into a `JobEnvelope`. Accepts strings (which
13
+ * are JSON-parsed) or objects (which are inspected directly) —
14
+ * the database driver passes a JSON string; the redis driver passes
15
+ * the already-deserialized object that bun-queue gave back.
16
+ *
17
+ * Returns a discriminated result so caller code doesn't have to
18
+ * pattern-match on the version field. Forward-compatible: an
19
+ * unknown-but-newer version returns `{ ok: false, reason:
20
+ * 'unknown-version' }` so the worker can leave the row for a
21
+ * newer-build worker.
22
+ */
23
+ export declare function parseEnvelope(raw: unknown): ParsedEnvelope;
24
+ /**
25
+ * Unified job envelope (stacksjs/stacks#1884, Q-6 from #1872).
26
+ *
27
+ * Background: the database and redis queue drivers serialized jobs
28
+ * differently:
29
+ *
30
+ * - database: `{ jobName, payload, options }` JSON-stringified into
31
+ * the `jobs.payload` column
32
+ * - redis: `{ jobName, payload }` passed as bun-queue's `data` arg
33
+ * with options split into bun-queue's second arg
34
+ *
35
+ * Switching `QUEUE_DRIVER` mid-flight orphaned in-flight jobs because
36
+ * the new worker couldn't deserialize the other shape. Fix: one
37
+ * `JobEnvelope` shape, used by every driver's write AND read, with a
38
+ * `envelopeVersion` field so a future shape change can be rolled out
39
+ * without breaking in-flight jobs queued under the previous version.
40
+ *
41
+ * Backward-compat: `parseEnvelope` handles three shapes:
42
+ *
43
+ * 1. **v1 (this version)** — full envelope with `envelopeVersion: 1`
44
+ * 2. **v0 implicit** — pre-fix shape (`{ jobName, payload, options? }`
45
+ * with no `envelopeVersion`). One-shot warn-and-process so the
46
+ * operator knows the migration window is closing.
47
+ * 3. **Laravel legacy** — `{ job: 'App\\Jobs\\Foo', data }` carried
48
+ * over from the database-Laravel-port era. Same warn-and-process.
49
+ *
50
+ * Forward-compat: a `envelopeVersion` newer than `JOB_ENVELOPE_VERSION`
51
+ * is logged and skipped (so a rolling deploy with a newer-shape envelope
52
+ * doesn't crash old workers; they leave the row for the new workers to
53
+ * pick up).
54
+ */
55
+ /**
56
+ * Current envelope version. Bump when you change the shape of
57
+ * `JobEnvelope` in a way that older workers couldn't deserialize.
58
+ *
59
+ * - v1: jobName + payload + optional options + envelopeVersion +
60
+ * dispatchedAt
61
+ */
62
+ export declare const JOB_ENVELOPE_VERSION: 1;
63
+ /**
64
+ * Per-job runtime options that travel with the envelope so a worker
65
+ * processing the job knows how to apply them (timeout, retries,
66
+ * backoff). The database driver previously embedded these in the
67
+ * envelope; the redis driver lost them across the wire because they
68
+ * went to bun-queue's separate options arg only.
69
+ */
70
+ export declare interface JobEnvelopeOptions {
71
+ queue?: string
72
+ timeout?: number
73
+ tries?: number
74
+ backoff?: number | number[]
75
+ }
76
+ export declare interface JobEnvelope {
77
+ jobName: string
78
+ payload: unknown
79
+ options?: JobEnvelopeOptions
80
+ envelopeVersion: number
81
+ dispatchedAt: string
82
+ }
83
+ /**
84
+ * Result of attempting to parse an arbitrary input into a
85
+ * `JobEnvelope`. Discriminated so callers don't have to inspect
86
+ * the version field directly.
87
+ */
88
+ export type ParsedEnvelope = | { ok: true, envelope: JobEnvelope, source: 'v1' | 'v0-implicit' | 'laravel-legacy' }
89
+ | { ok: false, reason: 'malformed' | 'unknown-version' | 'missing-job-name', detail?: string }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Check whether a job has already been dispatched under this key.
3
+ * Returns `true` when the dedup row exists (skip dispatch), `false`
4
+ * when it doesn't (proceed with dispatch). Degrades to "always
5
+ * false" with a startup warn when the table is missing.
6
+ */
7
+ export declare function hasDispatchedKey(key: string): Promise<boolean>;
8
+ /**
9
+ * Record that a job was successfully dispatched under this key.
10
+ * Called after the driver-side insert/push succeeds. No-op when
11
+ * the table is missing; collisions (concurrent dispatch of the
12
+ * same key) are swallowed because the existing row is already
13
+ * what the next lookup would return.
14
+ */
15
+ export declare function recordDispatchedKey(key: string, jobName: string, queue?: string): Promise<void>;
@@ -1,3 +1,7 @@
1
+ export type { DeadLetterReason, DeadLetterRecord, ListDeadLetterFilter } from './dead-letter';
2
+ export type { PoisonConfig } from './poison';
3
+ export type { CircuitBreakerConfig } from './circuit-breaker';
4
+ export type { JobEnvelope, JobEnvelopeOptions, ParsedEnvelope } from './envelope';
1
5
  // =============================================================================
2
6
  // Redis queue driver
3
7
  // =============================================================================
@@ -21,6 +25,48 @@ export { Job } from './action';
21
25
  // =============================================================================
22
26
  export { Jobs, job, jobBatch, runJob } from './job';
23
27
  // =============================================================================
28
+ // Job dispatch idempotency (stacksjs/stacks#1872 Q-8)
29
+ // =============================================================================
30
+ export { hasDispatchedKey, recordDispatchedKey } from './idempotency';
31
+ // =============================================================================
32
+ // Dead-letter queue + poison detection + circuit breaker
33
+ // (stacksjs/stacks#1885, Q-13 from #1872)
34
+ // =============================================================================
35
+ export {
36
+ listDeadLetterJobs,
37
+ moveToDeadLetter,
38
+ purgeDeadLetterJobs,
39
+ retryDeadLetterJob,
40
+ } from './dead-letter';
41
+ export {
42
+ hashPayload,
43
+ isQuarantined,
44
+ listQuarantined,
45
+ quarantineJob,
46
+ recordFailureForPoison,
47
+ unquarantineJob,
48
+ } from './poison';
49
+ export {
50
+ isCircuitOpen,
51
+ listCircuitState,
52
+ pauseQueue,
53
+ recordCircuitFailure,
54
+ recordCircuitSuccess,
55
+ resumeQueue,
56
+ } from './circuit-breaker';
57
+ // =============================================================================
58
+ // Unified job envelope (stacksjs/stacks#1884 — Q-6 from #1872)
59
+ // Single shape across database + redis drivers; back-compat parser
60
+ // handles the pre-#1884 implicit-v0 shape and the Laravel-import
61
+ // legacy `{ job, data }` form.
62
+ // =============================================================================
63
+ export {
64
+ clearEnvelopeWarnings,
65
+ createEnvelope,
66
+ JOB_ENVELOPE_VERSION,
67
+ parseEnvelope,
68
+ } from './envelope';
69
+ // =============================================================================
24
70
  // Per-job progress + cancellation (cache-backed; safe across processes)
25
71
  // =============================================================================
26
72
  export { setJobProgress, getJobProgress, cancelJob, isJobCancelled, clearJobState } from './job-progress';
package/dist/src/job.d.ts CHANGED
@@ -78,7 +78,8 @@ export declare function runJob(name: string, options?: { payload?: any; context?
78
78
  * dispatch: () => unknown,
79
79
  * dispatchIf: () => unknown,
80
80
  * dispatchUnless: () => unknown,
81
- * dispatchNow: () => unknown
81
+ * dispatchNow: () => unknown,
82
+ * dispatchOnce: () => unknown
82
83
  * }
83
84
  * ```
84
85
  */
@@ -94,7 +95,13 @@ export declare const Jobs: {
94
95
  /** Run the job synchronously, bypassing the queue. */
95
96
  dispatchNow: (name: string, payload?: any) => Promise<void>;
96
97
  /** Schedule the job to run after `seconds` of delay. */
97
- dispatchAfter: (seconds: number, name: string, payload?: any) => JobBuilder
98
+ dispatchAfter: (seconds: number, name: string, payload?: any) => JobBuilder;
99
+ /**
100
+ * Dispatch with an idempotency key in one call
101
+ * (stacksjs/stacks#1872 Q-8). Equivalent to
102
+ * `Jobs.make(name, payload).withIdempotencyKey(key).dispatch()`.
103
+ */
104
+ dispatchOnce: (key: string, name: string, payload?: any) => Promise<void>
98
105
  };
99
106
  /**
100
107
  * Options for job dispatch
@@ -106,7 +113,22 @@ export declare interface JobDispatchOptions {
106
113
  timeout?: number
107
114
  backoff?: number[]
108
115
  context?: any
116
+ idempotencyKey?: string
109
117
  }
118
+ /**
119
+ * Per-builder override of the transaction-context default.
120
+ * `'auto'` (the default) respects the current transaction scope:
121
+ * buffers when inside `db.transaction(...)`, dispatches immediately
122
+ * otherwise. `'after'` forces buffering even outside a transaction
123
+ * (warn-and-dispatch since there's nothing to buffer against).
124
+ * `'immediate'` forces immediate dispatch even inside a transaction
125
+ * — used for fire-and-forget side-effects that legitimately want
126
+ * to run before the surrounding transaction commits (analytics,
127
+ * lock probes, etc.).
128
+ *
129
+ * See stacksjs/stacks#1882.
130
+ */
131
+ declare type TransactionMode = 'auto' | 'after' | 'immediate';
110
132
  /**
111
133
  * Fluent job builder for file-based jobs
112
134
  */
@@ -118,6 +140,9 @@ declare class JobBuilder {
118
140
  timeout(seconds: number): this;
119
141
  backoff(delays: number[]): this;
120
142
  withContext(context: any): this;
143
+ withIdempotencyKey(key: string): this;
144
+ afterCommit(): this;
145
+ withoutCommit(): this;
121
146
  dispatch(): Promise<void>;
122
147
  dispatchIf(condition: boolean): Promise<void>;
123
148
  dispatchUnless(condition: boolean): Promise<void>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Compute the dedup key for a job+payload combination. Stable
3
+ * sha256 truncated to 32 hex chars — short enough to fit in
4
+ * indexed columns, wide enough to avoid collisions in practice.
5
+ */
6
+ export declare function hashPayload(payload: unknown): string;
7
+ /**
8
+ * Record a failure against `(jobName, payload-hash)`. If this push
9
+ * makes the count exceed the threshold within the window, the row
10
+ * is marked `quarantined_at` and the function returns `true`.
11
+ *
12
+ * Called from the worker's failure path AFTER the job lands in
13
+ * `failed_jobs`. Failures outside the window reset the count.
14
+ */
15
+ export declare function recordFailureForPoison(jobName: string, payload: unknown, config?: PoisonConfig): Promise<boolean>;
16
+ /**
17
+ * Is `(jobName, payload)` currently quarantined? Called from the
18
+ * dispatch path — true means "skip the queue, route directly to
19
+ * the DLQ".
20
+ */
21
+ export declare function isQuarantined(jobName: string, payload: unknown): Promise<boolean>;
22
+ /**
23
+ * Manually quarantine a job class. Useful when an operator knows
24
+ * a particular flow is broken (e.g. third-party API outage) and
25
+ * wants to stop the queue from churning on it before the failure
26
+ * threshold trips.
27
+ *
28
+ * If `payload` is omitted, every payload-hash for the job name is
29
+ * matched (uses wildcard `payload_hash = '*'`). For class-wide
30
+ * quarantine without per-payload granularity.
31
+ */
32
+ export declare function quarantineJob(jobName: string, payload?: unknown): Promise<void>;
33
+ /**
34
+ * Lift the quarantine for a job class. Deletes every
35
+ * `job_quarantine` row matching `jobName` (across all payload
36
+ * hashes) so future dispatches go to the queue normally.
37
+ */
38
+ export declare function unquarantineJob(jobName: string): Promise<void>;
39
+ /**
40
+ * Read the set of currently-quarantined job names + payload hashes
41
+ * for the `queue:quarantine` CLI list view.
42
+ */
43
+ export declare function listQuarantined(): Promise<Array<{
44
+ id: number
45
+ job_name: string
46
+ payload_hash: string
47
+ failure_count: number
48
+ quarantined_at: string | null
49
+ }>>;
50
+ export declare interface PoisonConfig {
51
+ maxFailures?: number
52
+ windowMinutes?: number
53
+ }
@@ -1,2 +1,13 @@
1
1
  import type { QueueOption } from '@stacksjs/types';
2
+ /**
3
+ * Affected-row count from an UPDATE result, across every shape the query
4
+ * builders in the wild return: Kysely's `numUpdatedRows` bigint,
5
+ * bun-query-builder's `{ changes, lastInsertRowid }` object, or a plain
6
+ * number. `Number({...})` is NaN, and `NaN > 0` is false — which made the
7
+ * queue worker's CAS claim treat every SUCCESSFUL reservation as lost:
8
+ * jobs were reserved then discarded, one per poll, forever, with nothing
9
+ * processed and nothing logged (found on stacksjs/status production,
10
+ * 2026-07-04, 1,600+ jobs stuck reserved).
11
+ */
12
+ export declare function updatedRowCount(result: unknown): number;
2
13
  export declare function storeJob(name: string, options: QueueOption): Promise<void>;
@@ -5,9 +5,23 @@ import type { Result } from '@stacksjs/error-handling';
5
5
  */
6
6
  export declare function startProcessor(queueName?: string, options?: { concurrency?: number }): Promise<Result<undefined, Error>>;
7
7
  /**
8
- * Stop the queue processor
8
+ * Stop the queue processor, draining in-flight jobs within a grace
9
+ * window before returning. The `signalled to stop` flag stops the
10
+ * polling loop from claiming new jobs; any handler that's currently
11
+ * mid-execution gets up to `graceMs` to finish.
12
+ *
13
+ * `graceMs` defaults to 10s (matches the buddy CLI's existing
14
+ * SIGTERM → SIGKILL window). Pass 0 to skip the wait entirely
15
+ * (test runs, CI).
16
+ *
17
+ * When the grace window expires with jobs still active, the function
18
+ * resolves anyway — the reservation-sweep (Q-2) will requeue them on
19
+ * the next worker startup. The alternative (hanging the process
20
+ * forever) is worse than the requeue cost.
21
+ *
22
+ * See stacksjs/stacks#1872 Q-10.
9
23
  */
10
- export declare function stopProcessor(): Promise<void>;
24
+ export declare function stopProcessor(options?: { graceMs?: number }): Promise<void>;
11
25
  /**
12
26
  * Retry all failed jobs
13
27
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/queue",
3
3
  "type": "module",
4
- "version": "0.70.45",
4
+ "version": "0.70.54",
5
5
  "description": "The Stacks Queue system powered by bun-queue.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [