@stacksjs/queue 0.70.229 → 0.70.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28,4 +28,4 @@ var{defineProperty:m1,getOwnPropertyNames:yL,getOwnPropertyDescriptor:wL}=Object
28
28
  </table>
29
29
  `}async sendSlack(L){let $=this.config.slack,V=[{type:"header",text:{type:"plain_text",text:`\uD83D\uDEA8 ${L.length} Job(s) Failed`,emoji:!0}},...L.slice(0,10).map((Y)=>({type:"section",text:{type:"mrkdwn",text:`*${Y.name}*
30
30
  Queue: ${Y.queue} | Attempts: ${Y.attempts}/${Y.maxAttempts}
31
- \`\`\`${Y.exception.slice(0,200)}\`\`\``}}))];if(L.length>10)V.push({type:"section",text:{type:"mrkdwn",text:`_...and ${L.length-10} more failed jobs_`}});try{let Y=await fetch($.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel:$.channel,username:$.username||"Queue Monitor",icon_emoji:$.iconEmoji||":warning:",blocks:V})});if(!Y.ok)C.error(`Slack notification failed with status ${Y.status}`)}catch(Y){C.error("Failed to send Slack notification:",Y)}}async sendDiscord(L){let $=this.config.discord,V=L.slice(0,10).map((Y)=>({title:`\u274C ${Y.name}`,color:15158332,fields:[{name:"Queue",value:Y.queue,inline:!0},{name:"Attempts",value:`${Y.attempts}/${Y.maxAttempts}`,inline:!0},{name:"Failed At",value:Y.failedAt.toISOString(),inline:!0},{name:"Exception",value:`\`\`\`${Y.exception.slice(0,500)}\`\`\``}],timestamp:Y.failedAt.toISOString()}));try{let Y=await fetch($.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$.username||"Queue Monitor",avatar_url:$.avatarUrl,content:`\uD83D\uDEA8 **${L.length} Job(s) Failed**`,embeds:V})});if(!Y.ok)C.error(`Discord notification failed with status ${Y.status}`)}catch(Y){C.error("Failed to send Discord notification:",Y)}}async sendWebhook(L){let $=this.config.webhook,V={event:"jobs.failed",timestamp:new Date().toISOString(),count:L.length,jobs:L.map((G)=>({id:G.id,name:G.name,queue:G.queue,attempts:G.attempts,maxAttempts:G.maxAttempts,exception:G.exception,failedAt:G.failedAt.toISOString()}))},Y={"Content-Type":"application/json",...$.headers};if($.secret){let G=JSON.stringify(V),U=await this.generateSignature(G,$.secret);Y["X-Signature"]=U}try{let G=await fetch($.url,{method:"POST",headers:Y,body:JSON.stringify(V)});if(!G.ok)C.error(`Webhook notification failed with status ${G.status}`)}catch(G){C.error("Failed to send webhook notification:",G)}}async generateSignature(L,$){let V=new TextEncoder,Y=await crypto.subtle.importKey("raw",V.encode($),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),G=await crypto.subtle.sign("HMAC",Y,V.encode(L));return Array.from(new Uint8Array(G)).map((U)=>U.toString(16).padStart(2,"0")).join("")}logFailures(L){for(let $ of L)C.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 P1=null;function s2(L){return P1=new W4(L),P1}function t2(){return P1}async function r2(L){if(P1)await P1.notify(L)}f();U1();import{err as a2,ok as SL}from"@stacksjs/error-handling";import{log as A}from"@stacksjs/logging";import M1 from"process";import{env as o2}from"@stacksjs/env";M1.on("unhandledRejection",(L,$)=>{A.error(`Unhandled Rejection: ${L}`)});M1.on("uncaughtException",(L)=>{A.error(`Uncaught Exception: ${L.message}`)});var b=0,Y1=!1,N="",n=new Set;function e2(L){return n.add(L),L.finally(()=>n.delete(L)),L}function L5(){let L=M1.env.STACKS_QUEUE_RESERVATION_TTL_SEC,$=L===void 0?Number.NaN:Number.parseInt(L,10);return Number.isFinite($)&&$>0?$:3600}function $5(){let L=M1.env.STACKS_QUEUE_SWEEP_INTERVAL_SEC,$=L===void 0?Number.NaN:Number.parseInt(L,10);return(Number.isFinite($)&&$>0?$:60)*1000}async function JL(){let L=L5(),$=Math.floor(Date.now()/1000)-L,V=Math.floor(Date.now()/1000);try{let{db:Y}=await import("@stacksjs/database"),G=await Y.updateTable("jobs").set({reserved_at:null,available_at:V}).where("reserved_at","<=",$).executeTakeFirst(),U=o(G);if(U>0)A.warn(`[queue] Requeued ${U} job(s) whose reservation exceeded the ${L}s TTL \u2014 likely victims of a worker crash. Set `+"STACKS_QUEUE_RESERVATION_TTL_SEC to tune.");return U}catch(Y){return A.error("[queue] Reservation sweep failed",{reason:Y instanceof Error?Y.message:String(Y)}),0}}function V5(L){if(!L||typeof L!=="object")return!1;let $=L.status??L.statusCode;if(typeof $==="number"&&$>=400&&$<500)return!0;let V=L.name;if(typeof V==="string"&&(V==="ValidationError"||V==="ModelNotFoundError"))return!0;return!1}function Y5(){return o2.QUEUE_DRIVER||"sync"}async function G5(L,$={}){try{A.info("Starting queue processor..."),Y1=!0,N=`worker-${M1.pid}-${Date.now()}`;let V=$.concurrency||1,Y=Y5(),{getWorkerTracker:G,getGlobalMetrics:U}=await Promise.resolve().then(() => (v(),h));if(U(),G().register(N,L||"default"),Y==="redis")return A.info("Using Redis queue driver (bun-queue)"),await O5(L||"default",V),SL(void 0);let X;if(L)X=[L];else if(X=await CL(),X.length===0)X=["default"];return A.info(`Processing queues: ${X.join(", ")}`),await U5(X,V),SL(void 0)}catch(V){return Y1=!1,a2(V)}}async function CL(){try{let{db:L}=await import("@stacksjs/database"),V=(await L.selectFrom("jobs").select("queue").distinct().execute()).map((Y)=>Y.queue).filter((Y)=>Boolean(Y));return V.length>0?V:["default"]}catch{return["default"]}}async function U5(L,$){A.info("Listening for jobs...");let V=L,Y=Date.now(),G=1e4,U=Date.now(),X=$5();await JL();while(Y1)try{let _=Date.now();if(_-Y>G){try{let K=await CL();if(K.length>0)V=K}catch{}Y=_}if(_-U>X)await JL(),U=_;for(let K of V){try{let{isCircuitOpen:O}=await Promise.resolve().then(() => (A1(),f1));if(await O(K))continue}catch{}let z=[];try{z=await X5(K,$)}catch{continue}await Promise.all(z.map(async(O)=>{try{A.info(`Processing job ${O.id} from queue "${K}"`),await e2(K5(O))}catch{A.error(`Unexpected error processing job ${O.id}`)}}))}await O4(1000)}catch{await O4(3000)}}async function X5(L,$){let V=Math.floor(Date.now()/1000),{db:Y}=await import("@stacksjs/database"),G=[];for(let U=0;U<$;U++){let X=await Y.selectFrom("jobs").where("queue","=",L).whereNull("reserved_at").where("available_at","<=",V).orderBy("id","asc").limit(1).selectAll().executeTakeFirst();if(!X)break;let _=await Y.updateTable("jobs").set({reserved_at:V,attempts:(X.attempts||0)+1}).where("id","=",X.id).where("reserved_at","is",null).executeTakeFirst();if(o(_)>0)G.push(X)}return G}async function K5(L){let $=L.id,V=L.queue||"default";b++;let Y=Date.now(),{emitQueueEvent:G,getWorkerTracker:U}=await Promise.resolve().then(() => (v(),h)),X=U();X.markActive(N);let _;try{_=JSON.parse(L.payload||"{}").jobName}catch{}await G("job:processing",{jobId:String($),queueName:V,jobName:_});try{let O=JSON.parse(L.payload||"{}").payload?._batchId;if(O){let{isBatchCancelled:Q}=await Promise.resolve().then(() => (f(),u));if(await Q(O)){A.info(`[Queue] Skipping job ${$} - batch ${O} has been cancelled`),await Z4($),b--,X.markIdle(N);return}}}catch{}let K=null;try{let z=JSON.parse(L.payload||"{}"),O=W5(z);if(O===void 0)await RL(z);else await Z5(RL(z),O*1000,`Job ${$} exceeded ${O}s timeout`)}catch(z){K=z instanceof Error?z:Error(String(z))}if(!K)try{await Z4($),A.info(`[Queue] Job ${$} completed`),X.recordCompletion(N);try{let{recordCircuitSuccess:z}=await Promise.resolve().then(() => (A1(),f1));await z(L.queue??"default")}catch{}await G("job:completed",{jobId:String($),queueName:V,duration:Date.now()-Y});try{let O=JSON.parse(L.payload||"{}").payload?._batchId;if(O){let{recordBatchJobCompletion:Q}=await Promise.resolve().then(() => (f(),u));await Q(O)}}catch{}}catch{A.info(`[Queue] Failed to delete completed job ${$}`)}else{let z=K.message;A.info(`[Queue] Job ${$} failed: ${z}`),X.recordFailure(N),await G("job:failed",{jobId:String($),queueName:V,error:K,duration:Date.now()-Y,attemptsMade:(L.attempts||0)+1});let O=1,Q={};try{Q=JSON.parse(L.payload||"{}"),O=Q.options?.tries||1}catch{}let P=(L.attempts||0)+1;if(P>=O){A.info(`[Queue] Job ${$} exceeded max attempts (${P}/${O})`);let Z=!1;if(Q?._retriedFromFailed===!0)try{let{moveToDeadLetter:W}=await Promise.resolve().then(() => (S1(),I4));if(Z=await W({queue:L.queue,payload:L.payload,exception:K.stack||K.message},"repeat-failure",2),Z)A.info(`[Queue] Job ${$} re-failed after retry \u2014 moved to dead_letter_jobs`)}catch{Z=!1}if(!Z){A.info(`[Queue] Moving job ${$} to failed_jobs`);try{Z=await _5(L,K)}catch{Z=!1}}if(Z)try{await Z4($)}catch{A.info(`[Queue] Failed to delete failed job ${$}`)}else A.error(`[Queue] Job ${$} exhausted its retries but could NOT be persisted to failed_jobs \u2014 leaving it in the queue to avoid data loss (the reservation sweep will retry it). Check that the failed_jobs table exists and is writable.`);try{let{recordFailureForPoison:W}=await Promise.resolve().then(() => (R1(),p4)),{recordCircuitFailure:x}=await Promise.resolve().then(() => (A1(),f1)),s=Q?.jobName??"unknown";await W(s,Q?.payload),await x(L.queue??"default")}catch{}try{let W=Q.payload?._batchId;if(W){let{recordBatchJobFailure:x}=await Promise.resolve().then(() => (f(),u));await x(W,String($),K)}}catch{}}else{let Z=Q.options?.backoff,W=30;if(Array.isArray(Z)&&Z.length>0){let x=Math.min(P-1,Z.length-1);W=Z[x]}else if(typeof Z==="number"&&Z>0)W=Z;if(W=Number(W),!Number.isFinite(W)||W<0)W=30;A.info(`[Queue] Job ${$} will be retried in ${W}s (attempt ${P}/${O})`);try{await z5($,W),A.info(`[Queue] Job ${$} released for retry`)}catch{A.info(`[Queue] Failed to release job ${$} for retry`)}}}b--,X.markIdle(N)}async function Z4(L){let{db:$}=await import("@stacksjs/database");await $.deleteFrom("jobs").where("id","=",L).execute()}async function z5(L,$=30){let V=Math.floor(Date.now()/1000)+$;A.debug(`Releasing job ${L} for retry at ${V}`);try{let{db:Y}=await import("@stacksjs/database");await Y.updateTable("jobs").set({reserved_at:null,available_at:V}).where("id","=",L).execute(),A.debug(`Job ${L} released successfully`)}catch{A.error(`Failed to release job ${L}`)}}async function _5(L,$){try{let V=new Date().toISOString().slice(0,19).replace("T"," "),Y=crypto.randomUUID(),G=$.stack||$.message,{db:U}=await import("@stacksjs/database");return await U.insertInto("failed_jobs").values({uuid:Y,connection:"database",queue:L.queue,payload:L.payload,exception:G,failed_at:V}).execute(),!0}catch(V){return A.error("Failed to log failed job:",V),!1}}function W5(L){if(!L||typeof L!=="object")return;let V=L.options?.timeout;if(typeof V!=="number"||!Number.isFinite(V)||V<=0)return;return V}async function Z5(L,$,V){let Y,G=new Promise((U,X)=>{Y=setTimeout(()=>X(Error(V)),$)});try{return await Promise.race([L,G])}finally{if(Y!==void 0)clearTimeout(Y)}}async function RL(L){let $=B1(L);if(!$.ok)throw Error(`[queue] Cannot deserialize job envelope: ${$.reason}`+($.detail?` (${$.detail})`:""));let{runJob:V}=await Promise.resolve().then(() => (Q1(),y1));await V($.envelope.jobName,{payload:$.envelope.payload})}async function O5(L,$){let{RedisQueue:V}=await Promise.resolve().then(() => (_1(),z1)),{queue:Y}=await import("@stacksjs/config"),G=Y?.connections?.redis;if(!G)throw Error("Redis queue connection is not configured. Check config/queue.ts");let U=new V(L,G),{emitQueueEvent:X,getWorkerTracker:_}=await Promise.resolve().then(() => (v(),h)),K=_();U.process($,async(z)=>{b++,K.markActive(N);let O=Date.now(),Q=B1(z.data);if(!Q.ok){b--,K.markIdle(N),A.error(`[Queue] Skipping Redis job ${z.id} - unparseable envelope: ${Q.reason}${Q.detail?` (${Q.detail})`:""}`);return}let P=Q.envelope,Z=P.payload?._batchId;if(Z)try{let{isBatchCancelled:W}=await Promise.resolve().then(() => (f(),u));if(await W(Z)){A.info(`[Queue] Skipping Redis job ${z.id} - batch ${Z} has been cancelled`),b--,K.markIdle(N);return}}catch{}await X("job:processing",{jobId:String(z.id),queueName:L});try{let{runJob:W}=await Promise.resolve().then(() => (Q1(),y1));if(await W(P.jobName,{payload:P.payload}),K.recordCompletion(N),await X("job:completed",{jobId:String(z.id),queueName:L,duration:Date.now()-O}),Z)try{let{recordBatchJobCompletion:x}=await Promise.resolve().then(() => (f(),u));await x(Z)}catch{}A.info(`[Queue] Redis job ${z.id} completed`)}catch(W){if(K.recordFailure(N),await X("job:failed",{jobId:String(z.id),queueName:L,error:W instanceof Error?W:Error(String(W)),duration:Date.now()-O}),Z)try{let{recordBatchJobFailure:x}=await Promise.resolve().then(() => (f(),u));await x(Z,String(z.id),W instanceof Error?W:Error(String(W)))}catch{}if(A.error(`[Queue] Redis job ${z.id} failed: ${W}`),V5(W)){A.info(`[Queue] Redis job ${z.id} hit a non-retryable error \u2014 skipping retry`);return}throw W}finally{b--,K.markIdle(N)}}),A.info(`Listening for Redis jobs on queue "${L}" with concurrency ${$}...`);while(Y1)await O4(1000);await U.close()}async function F5(L={}){let $=L.graceMs??1e4;if(Y1=!1,n.size>0&&$>0){A.info(`[queue] Draining ${n.size} in-flight job(s) (grace ${$}ms)`);let V=Promise.allSettled([...n]),Y=new Promise((U)=>setTimeout(()=>U("timeout"),$));if(await Promise.race([V.then(()=>"drained"),Y])==="timeout"&&n.size>0)A.warn(`[queue] Drain timed out with ${n.size} job(s) still active. Their reservations will be reclaimed by the next worker's sweep (Q-2).`)}if(N){let{getWorkerTracker:V}=await Promise.resolve().then(() => (v(),h));V().unregister(N)}A.info("Queue processor stopped")}async function x5(){let{db:L}=await import("@stacksjs/database"),$=await L.selectFrom("failed_jobs").selectAll().execute();for(let V of $)await qL(Number(V.id))}async function qL(L){let $=Math.floor(Date.now()/1000),V=new Date().toISOString().slice(0,19).replace("T"," "),{db:Y}=await import("@stacksjs/database"),U=(await Y.selectFrom("failed_jobs").where("id","=",L).selectAll().execute())[0];if(!U)throw Error(`Failed job ${L} not found`);let X=U.payload;try{let _=JSON.parse(U.payload||"{}");_._retriedFromFailed=!0,X=JSON.stringify(_)}catch{}await Y.insertInto("jobs").values({queue:U.queue,payload:X,attempts:0,reserved_at:null,available_at:$,created_at:V}).execute(),await Y.deleteFrom("failed_jobs").where("id","=",L).execute(),A.info(`Failed job ${L} has been re-queued`)}function H5(){return b}function Q5(){return Y1}function O4(L){return new Promise(($)=>setTimeout($,L))}async function D8(){let{RedisQueue:L}=await Promise.resolve().then(() => (_1(),z1));return L}export{u4 as withEvents,j4 as unquarantineJob,b2 as triggerJob,y2 as toJobOptions,z4 as stopScheduler,F5 as stopProcessor,h2 as startScheduler,G5 as startProcessor,N2 as setJobProgress,E4 as runTestJob,w1 as runJob,qL as retryFailedJob,v4 as retryDeadLetterJob,WL as resumeQueue,p1 as restore,c1 as releaseDispatchKey,f4 as recordFailureForPoison,L2 as recordDispatchedKey,KL as recordCircuitSuccess,zL as recordCircuitFailure,L4 as recordBatchJobFailure,e1 as recordBatchJobCompletion,m4 as quarantineJob,k4 as purgeDeadLetterJobs,_L as pauseQueue,B1 as parseEnvelope,g4 as onQueueEvent,r2 as notifyJobFailed,N1 as moveToDeadLetter,h4 as listQuarantined,q4 as listDeadLetterJobs,ZL as listCircuitState,d as jobRegistry,GL as jobBatch,VL as job,Q5 as isWorkerRunning,g2 as isSchedulerRunning,d2 as isQueueHealthy,J1 as isQuarantined,R2 as isJobCancelled,M4 as isFaked,XL as isCircuitOpen,LL as isBatchCancelled,Z1 as hashPayload,eL as hasDispatchedKey,O1 as getWorkerTracker,p2 as getSchedulerStatus,X4 as getScheduledJobs,u2 as getRegisteredJobs,D8 as getRedisQueue,e as getQueueEvents,S2 as getJobProgress,v2 as getJob,v1 as getGlobalMetrics,P4 as getFakeQueue,t2 as getFailedJobNotifier,F1 as getBatchCallbacks,k2 as getAllJobs,H5 as getActiveJobCount,h1 as fake,T4 as expectJobToFail,I2 as executeJob,x5 as executeFailedJobs,l as emitQueueEvent,U4 as discoverJobs,B4 as createQueueTester,i2 as createHealthCheckHandler,j as createEnvelope,s2 as configureFailedJobNotifications,C2 as clearJobState,uL as clearEnvelopeWarnings,l1 as claimDispatchKey,_4 as checkQueueHealth,J2 as cancelJob,E1 as QueueTester,q1 as QueueMetrics,C1 as QueueEvents,I1 as PendingBatch,b4 as OnQueueEvent,YL as Jobs,R4 as Job,gL as JOB_ENVELOPE_VERSION,W4 as FailedJobNotifier,p as DispatchedBatch,t1 as Batch};
31
+ \`\`\`${Y.exception.slice(0,200)}\`\`\``}}))];if(L.length>10)V.push({type:"section",text:{type:"mrkdwn",text:`_...and ${L.length-10} more failed jobs_`}});try{let Y=await fetch($.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel:$.channel,username:$.username||"Queue Monitor",icon_emoji:$.iconEmoji||":warning:",blocks:V})});if(!Y.ok)C.error(`Slack notification failed with status ${Y.status}`)}catch(Y){C.error("Failed to send Slack notification:",Y)}}async sendDiscord(L){let $=this.config.discord,V=L.slice(0,10).map((Y)=>({title:`\u274C ${Y.name}`,color:15158332,fields:[{name:"Queue",value:Y.queue,inline:!0},{name:"Attempts",value:`${Y.attempts}/${Y.maxAttempts}`,inline:!0},{name:"Failed At",value:Y.failedAt.toISOString(),inline:!0},{name:"Exception",value:`\`\`\`${Y.exception.slice(0,500)}\`\`\``}],timestamp:Y.failedAt.toISOString()}));try{let Y=await fetch($.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$.username||"Queue Monitor",avatar_url:$.avatarUrl,content:`\uD83D\uDEA8 **${L.length} Job(s) Failed**`,embeds:V})});if(!Y.ok)C.error(`Discord notification failed with status ${Y.status}`)}catch(Y){C.error("Failed to send Discord notification:",Y)}}async sendWebhook(L){let $=this.config.webhook,V={event:"jobs.failed",timestamp:new Date().toISOString(),count:L.length,jobs:L.map((G)=>({id:G.id,name:G.name,queue:G.queue,attempts:G.attempts,maxAttempts:G.maxAttempts,exception:G.exception,failedAt:G.failedAt.toISOString()}))},Y={"Content-Type":"application/json",...$.headers};if($.secret){let G=JSON.stringify(V),U=await this.generateSignature(G,$.secret);Y["X-Signature"]=U}try{let G=await fetch($.url,{method:"POST",headers:Y,body:JSON.stringify(V)});if(!G.ok)C.error(`Webhook notification failed with status ${G.status}`)}catch(G){C.error("Failed to send webhook notification:",G)}}async generateSignature(L,$){let V=new TextEncoder,Y=await crypto.subtle.importKey("raw",V.encode($),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),G=await crypto.subtle.sign("HMAC",Y,V.encode(L));return Array.from(new Uint8Array(G)).map((U)=>U.toString(16).padStart(2,"0")).join("")}logFailures(L){for(let $ of L)C.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 P1=null;function s2(L){return P1=new W4(L),P1}function t2(){return P1}async function r2(L){if(P1)await P1.notify(L)}f();U1();import{err as a2,ok as SL}from"@stacksjs/error-handling";import{log as A}from"@stacksjs/logging";import M1 from"process";import{env as o2}from"@stacksjs/env";M1.on("unhandledRejection",(L,$)=>{A.error(`Unhandled Rejection: ${L}`)});M1.on("uncaughtException",(L)=>{A.error(`Uncaught Exception: ${L.message}`)});var b=0,Y1=!1,N="",n=new Set;function e2(L){return n.add(L),L.finally(()=>n.delete(L)),L}function L5(){let L=M1.env.STACKS_QUEUE_RESERVATION_TTL_SEC,$=L===void 0?Number.NaN:Number.parseInt(L,10);return Number.isFinite($)&&$>0?$:3600}function $5(){let L=M1.env.STACKS_QUEUE_SWEEP_INTERVAL_SEC,$=L===void 0?Number.NaN:Number.parseInt(L,10);return(Number.isFinite($)&&$>0?$:60)*1000}async function JL(){let L=L5(),$=Math.floor(Date.now()/1000)-L,V=Math.floor(Date.now()/1000);try{let{db:Y}=await import("@stacksjs/database"),G=await Y.updateTable("jobs").set({reserved_at:null,available_at:V}).where("reserved_at","<=",$).executeTakeFirst(),U=o(G);if(U>0)A.warn(`[queue] Requeued ${U} job(s) whose reservation exceeded the ${L}s TTL \u2014 likely victims of a worker crash. Set `+"STACKS_QUEUE_RESERVATION_TTL_SEC to tune.");return U}catch(Y){return A.error("[queue] Reservation sweep failed",{reason:Y instanceof Error?Y.message:String(Y)}),0}}function V5(L){if(!L||typeof L!=="object")return!1;let $=L.status??L.statusCode;if(typeof $==="number"&&$>=400&&$<500)return!0;let V=L.name;if(typeof V==="string"&&(V==="ValidationError"||V==="ModelNotFoundError"))return!0;return!1}function Y5(){return o2.QUEUE_DRIVER||"sync"}async function G5(L,$={}){try{A.info("Starting queue processor..."),Y1=!0,N=`worker-${M1.pid}-${Date.now()}`;let V=$.concurrency||1,Y=Y5(),{getWorkerTracker:G,getGlobalMetrics:U}=await Promise.resolve().then(() => (v(),h));if(U(),G().register(N,L||"default"),Y==="redis")return A.info("Using Redis queue driver (bun-queue)"),await O5(L||"default",V),SL(void 0);let X;if(L)X=[L];else if(X=await CL(),X.length===0)X=["default"];return A.info(`Processing queues: ${X.join(", ")}`),await U5(X,V),SL(void 0)}catch(V){return Y1=!1,a2(V)}}async function CL(){try{let{db:L}=await import("@stacksjs/database"),V=(await L.selectFrom("jobs").select("queue").distinct().execute()).map((Y)=>Y.queue).filter((Y)=>Boolean(Y));return V.length>0?V:["default"]}catch{return["default"]}}async function U5(L,$){A.info("Listening for jobs...");let V=L,Y=Date.now(),G=1e4,U=Date.now(),X=$5();await JL();while(Y1)try{let _=Date.now();if(_-Y>G){try{let K=await CL();if(K.length>0)V=K}catch{}Y=_}if(_-U>X)await JL(),U=_;for(let K of V){try{let{isCircuitOpen:O}=await Promise.resolve().then(() => (A1(),f1));if(await O(K))continue}catch{}let z=[];try{z=await X5(K,$)}catch{continue}await Promise.all(z.map(async(O)=>{try{A.info(`Processing job ${O.id} from queue "${K}"`),await e2(K5(O))}catch{A.error(`Unexpected error processing job ${O.id}`)}}))}await O4(1000)}catch{await O4(3000)}}async function X5(L,$){let V=Math.floor(Date.now()/1000),{db:Y}=await import("@stacksjs/database"),G=[];for(let U=0;U<$;U++){let X=await Y.selectFrom("jobs").where("queue","=",L).whereNull("reserved_at").where("available_at","<=",V).orderBy("id","asc").limit(1).selectAll().executeTakeFirst();if(!X)break;let _=await Y.updateTable("jobs").set({reserved_at:V,attempts:(X.attempts||0)+1}).where("id","=",X.id).where("reserved_at","is",null).executeTakeFirst();if(o(_)>0)G.push(X)}return G}async function K5(L){let $=L.id,V=L.queue||"default";b++;let Y=Date.now(),{emitQueueEvent:G,getWorkerTracker:U}=await Promise.resolve().then(() => (v(),h)),X=U();X.markActive(N);let _;try{_=JSON.parse(L.payload||"{}").jobName}catch{}await G("job:processing",{jobId:String($),queueName:V,jobName:_});try{let O=JSON.parse(L.payload||"{}").payload?._batchId;if(O){let{isBatchCancelled:Q}=await Promise.resolve().then(() => (f(),u));if(await Q(O)){A.info(`[Queue] Skipping job ${$} - batch ${O} has been cancelled`),await Z4($),b--,X.markIdle(N);return}}}catch{}let K=null;try{let z=JSON.parse(L.payload||"{}"),O=W5(z);if(O===void 0)await RL(z);else await Z5(RL(z),O*1000,`Job ${$} exceeded ${O}s timeout`)}catch(z){K=z instanceof Error?z:Error(String(z))}if(!K)try{await Z4($),A.info(`[Queue] Job ${$} completed`),X.recordCompletion(N);try{let{recordCircuitSuccess:z}=await Promise.resolve().then(() => (A1(),f1));await z(L.queue??"default")}catch{}await G("job:completed",{jobId:String($),queueName:V,duration:Date.now()-Y});try{let O=JSON.parse(L.payload||"{}").payload?._batchId;if(O){let{recordBatchJobCompletion:Q}=await Promise.resolve().then(() => (f(),u));await Q(O)}}catch{}}catch{A.info(`[Queue] Failed to delete completed job ${$}`)}else{let z=K.message;A.info(`[Queue] Job ${$} failed: ${z}`),X.recordFailure(N),await G("job:failed",{jobId:String($),queueName:V,error:K,duration:Date.now()-Y,attemptsMade:(L.attempts||0)+1});let O=1,Q={};try{Q=JSON.parse(L.payload||"{}"),O=Q.options?.tries||1}catch{}let P=(L.attempts||0)+1;if(P>=O){A.info(`[Queue] Job ${$} exceeded max attempts (${P}/${O})`);let Z=!1;if(Q?._retriedFromFailed===!0)try{let{moveToDeadLetter:W}=await Promise.resolve().then(() => (S1(),I4));if(Z=await W({queue:L.queue,payload:L.payload,exception:K.stack||K.message},"repeat-failure",2),Z)A.info(`[Queue] Job ${$} re-failed after retry \u2014 moved to dead_letter_jobs`)}catch{Z=!1}if(!Z){A.info(`[Queue] Moving job ${$} to failed_jobs`);try{Z=await _5(L,K,{attempts:P,maxAttempts:O,durationMs:Date.now()-Y})}catch{Z=!1}}if(Z)try{await Z4($)}catch{A.info(`[Queue] Failed to delete failed job ${$}`)}else A.error(`[Queue] Job ${$} exhausted its retries but could NOT be persisted to failed_jobs \u2014 leaving it in the queue to avoid data loss (the reservation sweep will retry it). Check that the failed_jobs table exists and is writable.`);try{let{recordFailureForPoison:W}=await Promise.resolve().then(() => (R1(),p4)),{recordCircuitFailure:x}=await Promise.resolve().then(() => (A1(),f1)),s=Q?.jobName??"unknown";await W(s,Q?.payload),await x(L.queue??"default")}catch{}try{let W=Q.payload?._batchId;if(W){let{recordBatchJobFailure:x}=await Promise.resolve().then(() => (f(),u));await x(W,String($),K)}}catch{}}else{let Z=Q.options?.backoff,W=30;if(Array.isArray(Z)&&Z.length>0){let x=Math.min(P-1,Z.length-1);W=Z[x]}else if(typeof Z==="number"&&Z>0)W=Z;if(W=Number(W),!Number.isFinite(W)||W<0)W=30;A.info(`[Queue] Job ${$} will be retried in ${W}s (attempt ${P}/${O})`);try{await z5($,W),A.info(`[Queue] Job ${$} released for retry`)}catch{A.info(`[Queue] Failed to release job ${$} for retry`)}}}b--,X.markIdle(N)}async function Z4(L){let{db:$}=await import("@stacksjs/database");await $.deleteFrom("jobs").where("id","=",L).execute()}async function z5(L,$=30){let V=Math.floor(Date.now()/1000)+$;A.debug(`Releasing job ${L} for retry at ${V}`);try{let{db:Y}=await import("@stacksjs/database");await Y.updateTable("jobs").set({reserved_at:null,available_at:V}).where("id","=",L).execute(),A.debug(`Job ${L} released successfully`)}catch{A.error(`Failed to release job ${L}`)}}async function _5(L,$,V){try{let Y=new Date().toISOString().slice(0,19).replace("T"," "),G=crypto.randomUUID(),U=$.stack||$.message,{db:X}=await import("@stacksjs/database");return await X.insertInto("failed_jobs").values({uuid:G,connection:"database",queue:L.queue,payload:L.payload,exception:U,attempts:V.attempts,max_attempts:V.maxAttempts,duration_ms:V.durationMs,failed_at:Y}).execute(),!0}catch(Y){return A.error("Failed to log failed job:",Y),!1}}function W5(L){if(!L||typeof L!=="object")return;let V=L.options?.timeout;if(typeof V!=="number"||!Number.isFinite(V)||V<=0)return;return V}async function Z5(L,$,V){let Y,G=new Promise((U,X)=>{Y=setTimeout(()=>X(Error(V)),$)});try{return await Promise.race([L,G])}finally{if(Y!==void 0)clearTimeout(Y)}}async function RL(L){let $=B1(L);if(!$.ok)throw Error(`[queue] Cannot deserialize job envelope: ${$.reason}`+($.detail?` (${$.detail})`:""));let{runJob:V}=await Promise.resolve().then(() => (Q1(),y1));await V($.envelope.jobName,{payload:$.envelope.payload})}async function O5(L,$){let{RedisQueue:V}=await Promise.resolve().then(() => (_1(),z1)),{queue:Y}=await import("@stacksjs/config"),G=Y?.connections?.redis;if(!G)throw Error("Redis queue connection is not configured. Check config/queue.ts");let U=new V(L,G),{emitQueueEvent:X,getWorkerTracker:_}=await Promise.resolve().then(() => (v(),h)),K=_();U.process($,async(z)=>{b++,K.markActive(N);let O=Date.now(),Q=B1(z.data);if(!Q.ok){b--,K.markIdle(N),A.error(`[Queue] Skipping Redis job ${z.id} - unparseable envelope: ${Q.reason}${Q.detail?` (${Q.detail})`:""}`);return}let P=Q.envelope,Z=P.payload?._batchId;if(Z)try{let{isBatchCancelled:W}=await Promise.resolve().then(() => (f(),u));if(await W(Z)){A.info(`[Queue] Skipping Redis job ${z.id} - batch ${Z} has been cancelled`),b--,K.markIdle(N);return}}catch{}await X("job:processing",{jobId:String(z.id),queueName:L});try{let{runJob:W}=await Promise.resolve().then(() => (Q1(),y1));if(await W(P.jobName,{payload:P.payload}),K.recordCompletion(N),await X("job:completed",{jobId:String(z.id),queueName:L,duration:Date.now()-O}),Z)try{let{recordBatchJobCompletion:x}=await Promise.resolve().then(() => (f(),u));await x(Z)}catch{}A.info(`[Queue] Redis job ${z.id} completed`)}catch(W){if(K.recordFailure(N),await X("job:failed",{jobId:String(z.id),queueName:L,error:W instanceof Error?W:Error(String(W)),duration:Date.now()-O}),Z)try{let{recordBatchJobFailure:x}=await Promise.resolve().then(() => (f(),u));await x(Z,String(z.id),W instanceof Error?W:Error(String(W)))}catch{}if(A.error(`[Queue] Redis job ${z.id} failed: ${W}`),V5(W)){A.info(`[Queue] Redis job ${z.id} hit a non-retryable error \u2014 skipping retry`);return}throw W}finally{b--,K.markIdle(N)}}),A.info(`Listening for Redis jobs on queue "${L}" with concurrency ${$}...`);while(Y1)await O4(1000);await U.close()}async function F5(L={}){let $=L.graceMs??1e4;if(Y1=!1,n.size>0&&$>0){A.info(`[queue] Draining ${n.size} in-flight job(s) (grace ${$}ms)`);let V=Promise.allSettled([...n]),Y=new Promise((U)=>setTimeout(()=>U("timeout"),$));if(await Promise.race([V.then(()=>"drained"),Y])==="timeout"&&n.size>0)A.warn(`[queue] Drain timed out with ${n.size} job(s) still active. Their reservations will be reclaimed by the next worker's sweep (Q-2).`)}if(N){let{getWorkerTracker:V}=await Promise.resolve().then(() => (v(),h));V().unregister(N)}A.info("Queue processor stopped")}async function x5(){let{db:L}=await import("@stacksjs/database"),$=await L.selectFrom("failed_jobs").selectAll().execute();for(let V of $)await qL(Number(V.id))}async function qL(L){let $=Math.floor(Date.now()/1000),V=new Date().toISOString().slice(0,19).replace("T"," "),{db:Y}=await import("@stacksjs/database"),U=(await Y.selectFrom("failed_jobs").where("id","=",L).selectAll().execute())[0];if(!U)throw Error(`Failed job ${L} not found`);let X=U.payload;try{let _=JSON.parse(U.payload||"{}");_._retriedFromFailed=!0,X=JSON.stringify(_)}catch{}await Y.insertInto("jobs").values({queue:U.queue,payload:X,attempts:0,reserved_at:null,available_at:$,created_at:V}).execute(),await Y.deleteFrom("failed_jobs").where("id","=",L).execute(),A.info(`Failed job ${L} has been re-queued`)}function H5(){return b}function Q5(){return Y1}function O4(L){return new Promise(($)=>setTimeout($,L))}async function D8(){let{RedisQueue:L}=await Promise.resolve().then(() => (_1(),z1));return L}export{u4 as withEvents,j4 as unquarantineJob,b2 as triggerJob,y2 as toJobOptions,z4 as stopScheduler,F5 as stopProcessor,h2 as startScheduler,G5 as startProcessor,N2 as setJobProgress,E4 as runTestJob,w1 as runJob,qL as retryFailedJob,v4 as retryDeadLetterJob,WL as resumeQueue,p1 as restore,c1 as releaseDispatchKey,f4 as recordFailureForPoison,L2 as recordDispatchedKey,KL as recordCircuitSuccess,zL as recordCircuitFailure,L4 as recordBatchJobFailure,e1 as recordBatchJobCompletion,m4 as quarantineJob,k4 as purgeDeadLetterJobs,_L as pauseQueue,B1 as parseEnvelope,g4 as onQueueEvent,r2 as notifyJobFailed,N1 as moveToDeadLetter,h4 as listQuarantined,q4 as listDeadLetterJobs,ZL as listCircuitState,d as jobRegistry,GL as jobBatch,VL as job,Q5 as isWorkerRunning,g2 as isSchedulerRunning,d2 as isQueueHealthy,J1 as isQuarantined,R2 as isJobCancelled,M4 as isFaked,XL as isCircuitOpen,LL as isBatchCancelled,Z1 as hashPayload,eL as hasDispatchedKey,O1 as getWorkerTracker,p2 as getSchedulerStatus,X4 as getScheduledJobs,u2 as getRegisteredJobs,D8 as getRedisQueue,e as getQueueEvents,S2 as getJobProgress,v2 as getJob,v1 as getGlobalMetrics,P4 as getFakeQueue,t2 as getFailedJobNotifier,F1 as getBatchCallbacks,k2 as getAllJobs,H5 as getActiveJobCount,h1 as fake,T4 as expectJobToFail,I2 as executeJob,x5 as executeFailedJobs,l as emitQueueEvent,U4 as discoverJobs,B4 as createQueueTester,i2 as createHealthCheckHandler,j as createEnvelope,s2 as configureFailedJobNotifications,C2 as clearJobState,uL as clearEnvelopeWarnings,l1 as claimDispatchKey,_4 as checkQueueHealth,J2 as cancelJob,E1 as QueueTester,q1 as QueueMetrics,C1 as QueueEvents,I1 as PendingBatch,b4 as OnQueueEvent,YL as Jobs,R4 as Job,gL as JOB_ENVELOPE_VERSION,W4 as FailedJobNotifier,p as DispatchedBatch,t1 as Batch};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/queue",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.229",
5
+ "version": "0.70.230",
6
6
  "description": "The Stacks Queue system powered by bun-queue.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [