@autofleet/sheilta 2.7.3 → 2.8.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -225,14 +225,22 @@ interface JobStatus {
225
225
  durationMs: number | null;
226
226
  };
227
227
  }
228
+ interface PartialAckResult<Id = string | number> {
229
+ succeeded?: Id[];
230
+ failed?: {
231
+ id: Id;
232
+ error?: string;
233
+ }[];
234
+ }
228
235
  type MessageAck = () => Promise<void>;
229
236
  type MessageNack = (err?: Error, requeue?: boolean) => Promise<void>;
237
+ type MessagePartialAck = (result: PartialAckResult) => Promise<void>;
230
238
  type BulkPerIdHandler<T = any> = (args: {
231
239
  jobId?: string;
232
240
  ids: Id[];
233
241
  id?: Id;
234
242
  payload: T;
235
- }, ack: MessageAck, nack: MessageNack) => Promise<void>;
243
+ }, ack: MessageAck, nack: MessageNack, partialAck: MessagePartialAck) => Promise<void>;
236
244
  interface AdditionalIdsHookData<T = any> {
237
245
  rawPayload: {
238
246
  query: Record<string, any>;
@@ -534,14 +542,24 @@ declare class JobManager {
534
542
  completeEmptyJob(jobId: string): Promise<void>;
535
543
  /**
536
544
  * Handle successful message processing (ack)
537
- * Uses Redis Lua script for atomic operation
545
+ * Internally calls partialAck with all items succeeded
538
546
  */
539
547
  ack(jobId: string, count?: number): Promise<void>;
540
548
  /**
541
549
  * Handle failed message processing (nack)
550
+ * Internally calls partialAck with all items failed
551
+ */
552
+ nack(jobId: string, errorMsg: string, data: {
553
+ ids: Id[];
554
+ }, count?: number): Promise<void>;
555
+ /**
556
+ * Handle partial success/failure for a batch of items
542
557
  * Uses Redis Lua script for atomic operation
543
558
  */
544
- nack(jobId: string, errorMsg: string, data: any, count?: number): Promise<void>;
559
+ partialAck(jobId: string, succeededCount: number, failedResults: {
560
+ id: any;
561
+ error?: string;
562
+ }[]): Promise<void>;
545
563
  /**
546
564
  * Add a job to user's job list
547
565
  */
package/lib/index.js CHANGED
@@ -1,73 +1,66 @@
1
1
  import{Op as e,literal as t}from"sequelize";import n from"@autofleet/logger";import{BadRequest as r,UnexpectedError as i,handleError as a}from"@autofleet/errors";import o from"joi";import{customFields as s}from"@autofleet/common-types";import{randomInt as c,randomUUID as l}from"node:crypto";import{createClient as u}from"redis";import{Router as d}from"express";import{z as f,z as p}from"zod";import{EventEmitter as m}from"node:events";const h=[`eq`,`ne`,`gte`,`gt`,`lte`,`lt`,`not`,`in`,`notIn`,`is`,`like`,`iLike`,`notLike`,`between`,`and`,`or`,`overlap`,`contains`],g={$eq:`=`,$ne:`!=`,$gte:`>=`,$gt:`>`,$lte:`<=`,$lt:`<`,$not:`NOT`,$in:`IN`,$notIn:`NOT IN`,$is:`IS`,$like:`LIKE`,$iLike:`ILIKE`,$notLike:`NOT LIKE`,$and:`AND`,$or:`OR`},_=(t={Op:e})=>{let{Op:n}=t;return Object.fromEntries(h.map(e=>[`${`$`+e}`,n[e]]))},v=e=>`\$${e}\$`,y=(e,t)=>e.includes(`.`)&&t.includes(e.split(`.`,1)[0]),b=(e,t)=>{let n=e;return e.includes(`-`)&&([,n]=n.split(`-`,2)),y(n,t)&&([n]=n.split(`.`,1)),n},x=e=>e.includes(`-`),S=e=>{throw new r([Error(e)])},C=e=>e.split(`.`,2)[1],w=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(c(52))).join(``),T=(e,t)=>Object.fromEntries(t.map(t=>[t,e[t]])),E={length:`length`};function D(e){return e.replace(/(?!^)[A-Z]/g,e=>`_${e.toLowerCase()}`)}function ee(e){switch(e.action){case E.length:return[t(`jsonb_array_length(${D(e.columnName)})`),e.alias];default:return e.action,[]}}function te(e){return e.map(e=>ee(e))}function ne(e){return e.map(e=>{let n=D(e.columnName),r=`json_build_object(${e.keys.map(e=>`'${e}', ${n} -> '${e}'`).join(`, `)})`,i=e.alias||e.columnName;return[t(r),i]})}function O({select:e=[],computed:t=[]}={}){let n=ne(e),r=te(t);return[...n,...r]}const k=`DESC`,A=`customFields.`,{CUSTOM_FIELDS_FILTER_SCOPE:j,CUSTOM_FIELDS_SORT_SCOPE:M}=s,N=e=>[`string`,`number`].includes(typeof e)||Array.isArray(e)?e:Object.entries(e).map(([e,t])=>({operator:g[e],value:t})),re=(e,t={})=>{let{literalAttributes:n=[],DBFormatter:r=void 0}=t,[i,a]=e.reduce((e,t)=>{let[i,a=`ASC`]=Array.isArray(t)?t:[t],o=n?.find(e=>e.attribute===i);if(o){let t=r?r(`"${o.attribute}" ${a}`):`${o.attribute} ${a}`;e[1].push(o.literal),e[0].push([t])}else e[0].push(t);return e},[[],[]]);return[i,a]},P=e=>{let t={};return Object.entries(e).forEach(([e,n])=>{let r=w();if(t[r]=e.split(A,2)[1],Array.isArray(n))n.forEach(e=>{let n=w();t[n]=typeof e==`string`?e:e.value});else if(typeof n==`string`||typeof n==`number`){let e=w();t[e]=n}else if(n?.operator){let e=w();t[e]=n.value}}),t},ie=e=>{let t={};return e.forEach(e=>{if(e.startsWith(A)){let n=w();t[n]=e.split(A,2)[1]}else if(e.substring(1).startsWith(A)){let n=w();t[n]=e.substring(1).split(A,2)[1]}}),t},ae=(e,t)=>({...ie(e),...P(t)}),oe=({order:e,associationModels:t=[],replacementsMap:n={}})=>{let r=[],i=new Map;return e.forEach(e=>{if([e,e.substring(1)].some(e=>e.startsWith(A))){i.has(M)||i.set(M,{});let t=e.split(A,2)[1];i.get(M)[t]=x(e)?k:`ASC`;return}let n=[b(e,t)],a=x(e);y(a?e.split(`-`,2)[1]:e,t)&&n.push(C(e)),a&&n.push(k),r.push(n)}),{formattedOrders:r,replacementsMap:n,orderScopes:Array.from(i.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},se=e=>e||1,ce=e=>e||20,F=(e,t={})=>{let n=e.map(e=>{let n=t[typeof e==`string`?e:e.association||e.model];return{...typeof e!=`string`&&e,association:n,required:typeof e==`string`||e.required!==!1,...typeof e!=`string`&&e.include&&{include:F(e.include,n?.target?.associations)}}});return n=n.map(({model:e,...t})=>t),n},le=(e,t,n,r=[])=>{let i={},a={},o=new Map;return Object.entries(e).forEach(([e,n])=>{if(e.startsWith(A)){o.has(j)||o.set(j,{});let t=e.split(A,2)[1];o.get(j)[t]=N(n);return}if(r.includes(e)){a[e]=n;return}let s=y(e,t)?v(e):e;i[s]=n}),{formattedQuery:i,externalQueryValues:a,formattedScopes:Array.from(o.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},ue=(e,t,n)=>({$and:e.split(` `).map(e=>({$or:t.filter(e=>n[e].type.key===`STRING`).map(t=>({[t]:{$iLike:`%${e}%`}}))}))});var de=({order:e=[],page:t=1,perPage:n=20,include:r=[],query:i={},attributes:a=null,searchTerm:o=null,jsonAttributes:s={}},c,l)=>{let u=ae(e,i),d=Object.keys(c?.associations||{}),{formattedOrders:f,orderScopes:p}=oe({order:[...e,`id`],associationModels:d,replacementsMap:u}),[m,h]=re(f,l),g=O(s),_=[...h,...a??[],...g],v=a?.length?_:{include:_},y=F(r,c?.associations),b=se(t),x=ce(n),S=le(i,d,u,l?.additionalAllowedAttributes),{formattedScopes:C,externalQueryValues:w}=S,{formattedQuery:T}=S;if(o&&!l?.skipSearchTermFormat){let e=ue(o,a?.length?a:Object.keys(c.rawAttributes||{}),c.rawAttributes);T=!T||Object.keys(T).length===0?e:{$and:[T,e]}}return{query:T,order:m,page:b,perPage:x,include:y,scopes:[...C,...p],...v&&{attributes:v},...Object.keys(w).length>0&&{externalQueryValues:w}}};const fe=e=>h.includes(e.split(`$`,2)[1]),I=(e,t=[],n=[],r=[])=>{let i=e.startsWith(`$`)&&e.endsWith(`$`)?e.slice(1,-1):e;return[...t,...n].includes(i.includes(`.`)?i.split(`.`,1)[0]:i)||r.includes(i)},L=(e,t,n,r={})=>{let i=x(e);i&&!e.startsWith(`-`)&&S(`- must be only at the beginning of the word`);let a=i?e.split(`-`,2)[1]:e,o=y(a,n),s=b(e,n),c=r?.literalAttributes?.map(e=>e.attribute)?.includes(a);!o&&s.includes(`.`)&&([s]=s.split(`.`,1)),t.includes(s)||o||c||S(`${e} is invalid. isLiteralAttribute: ${c}`)},R=(e,t)=>{t.includes(e)||S(`${e} is invalid`)},z=(e,t,n=[],r={})=>{e.forEach(e=>L(e,t,n,r))},B=(e,t)=>{e.forEach(e=>R(e,t))},V=(e,t)=>{B([...e.select?.map(e=>e.columnName)??[],...e.computed?.map(e=>e.columnName)??[]],t)},pe=(e,t)=>{let n=Array.isArray(e)?e:Object.keys(e);if(!n?.length)return;let r=n.find(e=>!t?.enrichmentAttributes?.includes(e));r&&S(`enrichment attribute ${r} is invalid`)},H=(e,t,n=[],r=[])=>{Object.entries(e).forEach(([e,i])=>{Array.isArray(i)?i[0]&&typeof i[0]==`object`&&i.map(e=>H(e,t,n,r)):fe(e)||I(e,t,n,r)?i&&typeof i==`object`&&H(i,t,[],r):S(`invalid key: ${e}`)})},me=({page:e,perPage:t})=>{e<1&&S(`Page must be greater than 0`),(t>100||t<1)&&S(`PerPage must be between 1 to 100`)},he=(e,t)=>{let n=Object.keys(t);e.forEach(e=>{I(e.model,n);let r=t[e.model]?.target;r||S(`model not found in associations`);let{rawAttributes:i}=r,a=Object.keys(i);e.where&&H(e.where,a),e.order&&z(e.order,a),e.attributes&&B(e.attributes,a),[null,void 0,!0,!1].includes(e.required)||S(`include.required must be a boolean`)})},U=({query:e={},order:t=[],attributes:n=[],include:r=[],page:i=1,perPage:a=20,enrichments:o=[],group:s=[],jsonAttributes:c={}},l,u={})=>{let d=Object.keys(l.rawAttributes),f=Object.keys(l?.associations||{});return!n||n.length===0?n=d:B(n,d),z(t,d,f,u),H(e,d,f,u.additionalAllowedAttributes),pe(o,u),V(c,d),Array.isArray(s)||S(`group must be an array`),r.length&&typeof r==`object`?he(r,l?.associations):r&&typeof r!=`object`&&S(`include must be an array`),me({page:i,perPage:a}),!0},{object:W,string:G,number:K,any:ge,array:q,alternatives:_e}=o.types(),ve=n(),ye=W.keys({query:W,attributes:q.items(G),order:q.items(G),page:K,perPage:K,include:q.items(ge),searchTerm:G,group:q.items(G),enrichments:_e.try(q.items(G),W.pattern(G,{exclude:q.items(G)})),jsonAttributes:o.object({select:o.array().items(o.object({columnName:o.string().required(),keys:o.array().items(o.string().required()).required(),alias:o.string().optional()})).default([]),computed:o.array().items(o.object({columnName:o.string().required(),action:o.string().valid(...Object.values(E)).required(),alias:o.string().required()})).default([])}).default({})}),J=(e,t,n={})=>{let{query:i,attributes:a,order:o,page:s,perPage:c,include:l,group:u,enrichments:d,jsonAttributes:f}=t,p=ye.validate(t);if(p.error)throw new r([p.error]);U({query:i,attributes:a,order:o,page:s,perPage:c,include:l,enrichments:d,group:u,jsonAttributes:f},e,n)},be=(e,t={},n=`body`)=>(r,i,o)=>{try{J(e,r[n],t),o()}catch(e){let{query:o,attributes:s,order:c}=r[n];a(e,i,{logger:t.logger??ve,message:`error in query middleware`,payload:{error:e,query:o,attributes:s,order:c}})}},Y=(e,t,n={})=>{let{order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l,jsonAttributes:u}=t,{query:d,externalQueryValues:f,order:p,page:m,perPage:h,include:g,scopes:_,attributes:v}=de({query:s,order:r,page:i,perPage:a,include:o,attributes:c,searchTerm:l,jsonAttributes:u},e,n);t.query=d,t.externalQueryValues=f,t.order=p,t.attributes=v,t.page=m,t.perPage=h,t.include=g,t.scopes=_,n.includeRawPayload&&(t.rawPayload={order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l})},xe=(e,t={},n=`body`)=>(r,i,a)=>{Y(e,r[n],t),a()},Se=({model:e,logger:t,validationOptions:n,formatOptions:r,modelName:o=e.constructor?.name,additionalScopes:s=[],modifyQueryValues:c,onRowsRetrieved:l})=>async(u,d)=>{try{J(e,u.body,{...n,logger:t})}catch(e){a(e,d,{logger:t,message:`error in query endpoint`,payload:T(u.body,[`query`,`order`,`attributes`])});return}try{Y(e,u.body,r);let n=Object.assign(T(u.body,[`query`,`externalQueryValues`,`order`,`attributes`,`page`,`perPage`,`include`,`scopes`,`enrichments`]),{distinct:!0});t.info(`querying ${o}`,{queryValues:n});let i=c?.(n)??n,{scopes:a=[],query:f,perPage:p,page:m,enrichments:h,externalQueryValues:g,..._}=i,v=await e.scope([...s,...a]).findAndCountAll({where:f,limit:p,offset:(m-1)*p,..._});if(!v.rows.length||!l){d.json(v);return}let y=await l(v,i);d.json(y)}catch(e){a(new i(e),d,{logger:t,message:`Error while querying ${o}`,payload:{query:u.body}})}};var X=class e extends Error{constructor(e,t=!1){super(e),this.name=`BulkerError`,this.retryable=t,Object.setPrototypeOf(this,new.target.prototype)}static isBulkerError(t){return t instanceof e}static wrap(t,n,r=!1){return t instanceof e?new e(n??t.message,r||t.retryable):new e(n?`${n}: ${t instanceof Error?t.message:String(t)}`:String(t),r)}static retryable(t){return new e(t,!0)}static nonRetryable(t){return new e(t,!1)}};const Ce=e=>e.length===0?`No keys provided`:e.length===1?`Key "${e[0]}" is required`:`Exactly one of [${e.join(`, `)}] must be provided`;function we(e,t){return p.object(e).refine(e=>t.filter(t=>e[t]!==void 0&&e[t]!==null).length===1,{message:Ce(t)})}const Z=p.string().uuid();function Q(e){return we(Object.fromEntries(e.map(e=>[e,p.union([Z,p.array(Z)]).optional()])),e)}const Te=[`businessModelId`,`fleetId`,`demandSourceId`,`contextId`,`userId`,`businessAccountId`,`activeBusinessModelId`];var Ee=class{constructor(e,t){if(this.bulker=e,this.opts=t,this.rabbitQueueName=null,this.rabbit=null,this.bulkHandler=async(e,t,n)=>{try{let{query:n,payload:r,preview:i}=e.body??{},a=r;if(r&&this.payloadSchema)try{a=this.payloadSchema.parse(r)}catch(e){if(e instanceof p.ZodError)return t.status(400).json({error:`invalid_payload`,details:e.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});throw e}try{J(this.model,n,{logger:this.bulker.logger})}catch(e){return t.status(400).json({error:`invalid_query`,details:e.message||e})}let o=this.identityScopeSchema.safeParse(n?.query||{});if(!o.success)return t.status(400).json({error:`invalid_identity_scope`,details:o.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});Y(this.model,n,{includeRawPayload:!0});let{query:s,...c}=Object.assign(T(n,[`query`,`externalQueryValues`,`order`,`include`,`scopes`,`enrichments`]),{distinct:!0}),u=[];if(this.opts.additionalIdsHook){let{rawPayload:e}=n;try{u=await this.opts.additionalIdsHook({rawPayload:e,payload:a}),this.bulker.logger.info(`additionalIdsHook returned ${u.length} IDs for action ${this.action}`)}catch(e){return this.bulker.logger.error(`Error in additionalIdsHook for action ${this.action}: ${e.message||e}`,{err:e}),t.status(500).json({error:`additional_ids_hook_error`,details:e.message||e})}}let d=[];s&&typeof s==`object`&&Object.keys(s).length>0&&d.push(s),Array.isArray(u)&&u.length>0&&d.push({id:{$in:u}});let f;if(d.length===0)return t.status(400).json({error:`no_query`,details:`No valid query provided to select records`});if(d.length===1){let[e]=d;f=e}else f={$or:d};this.bulker.logger.info(`Constructed final where clause for action ${this.action}`,{where:f});let m={where:f,...c},h=await this.model.scope(this.modelScopes).count({...m,col:this.idField});if(i)return t.json({estimatedCount:h});let g=l();return await this.bulker.jobManager.initJob(g,{status:`queued`,total:h,action:this.action}),this.bulker.emitEvent(`job:created`,{jobId:g,action:this.action,total:h}),setImmediate(()=>{this.rabbitScanAndEnqueue(g,m,a).catch(e=>this.bulker.logger.error(e))}),t.status(202).json({jobId:g,estimatedCount:h})}catch(e){return this.bulker.logger.error(`Error in bulkHandler for action ${this.action}: ${e.message||e}`,{err:e}),n(e)}},this.action=t.action,this.model=t.model,this.modelScopes=t.modelScopes??[],this.consumer=t.consumer,this.rabbit=e.rabbit,this.rabbitQueueName=t.rabbitQueueName??`bulk-${this.action}-queue`,this.consumerOptions={enableRabbitTrace:!0,...this.opts.consumerOptions},this.payloadSchema=t.payloadSchema,!/^[a-zA-Z0-9-_]+$/.test(this.action))throw Error(`BulkRoute action must be alphanumeric`);if(!this.model||typeof this.model.findAll!=`function`||typeof this.model.count!=`function`)throw Error(`BulkRoute model must be a valid Sequelize model`);if(typeof this.consumer!=`function`)throw Error(`BulkRoute consumer must be a function`);if(this.payloadSchema&&!(this.payloadSchema instanceof p.ZodType))throw Error(`BulkRoute payloadSchema must be a Zod schema`);if(this.modelScopes&&!Array.isArray(this.modelScopes))throw Error(`BulkRoute scopes must be an array of strings`);if(this.opts.additionalIdsHook&&typeof this.opts.additionalIdsHook!=`function`)throw Error(`BulkRoute additionalIdsHook must be a function`);this.idField=t.idField??e.defaults.idField,this.pageSize=t.pageSize??e.defaults.pageSize,this.consumerBatchSize=t.consumerBatchSize??e.defaults.consumerBatchSize;let n=this.model.rawAttributes||{},r=(t.identityScopes&&t.identityScopes.length>0?t.identityScopes:Te).filter(e=>!!n[e]);r.length===0&&this.bulker.logger.warn(`BulkRoute for action ${this.action} has no valid identityScopes configured - all records will be accessible`),this.bulker.logger.info(`BulkRoute for action ${this.action} using idField ${this.idField}, pageSize ${this.pageSize},
2
- consumerBatchSize ${this.consumerBatchSize}, identityScopes: ${r.join(`, `)}`),this.identityScopeSchema=Q(r),this.startRabbitWorker().catch(e=>{this.bulker.logger.error(`Failed to start RabbitMQ worker for queue ${this.rabbitQueueName}: ${e.message||e}`)})}async getUserJobs(e,t=20){return this.bulker.jobManager.getUserJobs(e,t)}batchIds(e){let t=[];for(let n=0;n<e.length;n+=this.consumerBatchSize)t.push(e.slice(n,n+this.consumerBatchSize));return t}async rabbitScanAndEnqueue(t,n,r){if(!this.rabbit)throw Error(`RabbitMQ not configured in Bulker`);let i=Date.now().toString();if(this.bulker.getUserId){let e=this.bulker.getUserId();e&&await this.bulker.jobManager.addUserJob(e,t)}await this.bulker.jobManager.setJobFields(t,{status:`running`,startTime:i}),this.bulker.emitEvent(`job:started`,{jobId:t,action:this.action});let a=null,o=0,s=0;for(;;){let i=await this.bulker.jobManager.getJobField(t,`status`);if(!i||i===`canceled`){this.bulker.logger.info(`Job ${t} was canceled, stopping scan and enqueue`);break}let c=a?{[e.and]:[n.where,{[this.idField]:{[e.gt]:a}}]}:n.where,l=await this.model.scope(this.modelScopes).findAll({where:c,...T(n,[`include`,`order`,`scopes`]),attributes:[this.idField],order:[[this.idField,`ASC`]],limit:this.pageSize,raw:!0,subQuery:!1});if(l.length===0)break;let u,d=l.map(e=>e[this.idField]);this.consumerBatchSize===1&&d.length===1&&(u=d[0]);let f=this.batchIds(d).map(e=>({jobId:t,ids:e,payload:r,id:u}));this.bulker.logger.info(`Enqueuing ${f.length} batched messages (${d.length} total IDs) to RabbitMQ queue ${this.rabbitQueueName}`);let p=(await Promise.allSettled(f.map(e=>this.rabbit.sendToQueue(this.rabbitQueueName,e)))).filter(e=>e.status===`rejected`);if(p.length>0)throw this.bulker.logger.error(`Failed to enqueue ${p.length} messages to RabbitMQ`,{rejected:p}),Error(`Failed to enqueue ${p.length} messages to RabbitMQ`);o+=l.length,await this.bulker.jobManager.incrJobField(t,`queued`,l.length),a=l[l.length-1][this.idField],s+=1,this.bulker.emitEvent(`scan:page`,{jobId:t,action:this.action,pageNumber:s,itemsInPage:l.length});let m=await this.bulker.jobManager.getJob(t);m&&this.bulker.emitEvent(`job:queued`,{jobId:t,action:this.action,queued:o,total:m.total})}o===0&&await this.bulker.jobManager.completeEmptyJob(t)}async startRabbitWorker(){return this.bulker.logger.info(`Starting RabbitMQ consumer for queue ${this.rabbitQueueName}`),this.bulker.emitEvent(`worker:started`,{action:this.action,queueName:this.rabbitQueueName}),this.rabbit?.consume(this.rabbitQueueName,async(e,t,n)=>{if(!e)return;let{jobId:r,ids:i,payload:a}=e.content,o=!1,s=async()=>{if(!o){o=!0,await Promise.all([this.bulker.jobManager.ack(r,i.length),t()]);for(let e of i)this.bulker.emitEvent(`item:processed`,{jobId:r,action:this.action,id:e})}},c=async(e,t)=>{if(o)return;o=!0;let s=t;s===void 0&&(s=X.isBulkerError(e)?e.retryable:!1),await Promise.all([this.bulker.jobManager.nack(r,e?e.message||String(e):`nacked`,{ids:i,payload:a},i.length),n(void 0,{skipRetry:!s})]);for(let t of i)s?this.bulker.emitEvent(`item:retrying`,{jobId:r,action:this.action,id:t}):this.bulker.emitEvent(`item:failed`,{jobId:r,action:this.action,id:t,error:e?e.message||String(e):`nacked`})},l=await this.bulker.jobManager.getJobField(r,`status`);if(!l||l===`canceled`){await this.bulker.jobManager.incrJobField(r,`processed`,i.length);return}try{this.bulker.logger.info(`Started processing job ${r} action ${this.action} ids amount: ${i.length}`);for(let e of i)this.bulker.emitEvent(`item:processing`,{jobId:r,action:this.action,id:e});await this.consumer({jobId:r,ids:i,id:this.consumerBatchSize===1?i[0]:void 0,payload:a},s,c),await s()}catch(e){this.bulker.logger.error(`Error processing job ${r} action ${this.action}: ${e.message||e}`,{err:e}),this.bulker.emitEvent(`worker:error`,{action:this.action,error:e.message||String(e)}),await c(e,!1)}},this.consumerOptions)}},De=class{constructor(e,t,n=`/bulk-actions`){this.routes=[],this.actionsToRouteMap=new Map,this.bulkHandler=async(e,t,n)=>{try{let{action:r}=e.body??{};if(!r)return t.status(400).json({error:`missing_action`,message:`Request body must include an "action" field`});let i=this.actionsToRouteMap.get(r);return i?i.bulkHandler(e,t,n):t.status(404).json({error:`unknown_action`,message:`Action "${r}" is not registered`,availableActions:Array.from(this.actionsToRouteMap.keys())})}catch(e){return this.bulker.logger.error(`Error in BulkRouter bulkHandler: ${e.message||e}`,{err:e}),n(e),null}},this.getJobHandler=async(e,t,n)=>{try{if(!e.params.id)return t.status(400).json({error:`missing_id`});let n=await this.bulker.jobManager.getJob(e.params.id);return n?t.json(n):t.status(404).json({error:`not_found`})}catch(e){return n(e)}},this.cancelJobHandler=async(e,t,n)=>{try{return e.params.id?await this.bulker.jobManager.cancelJob(e.params.id)?(this.bulker.logger.info(`Job ${e.params.id} cancel requested`),t.json({ok:!0})):t.status(404).json({error:`not_found`}):t.status(400).json({error:`missing_id`})}catch(e){return n(e)}},this.getMyJobsHandler=async(e,t,n)=>{try{if(!this.bulker.getUserId)return t.status(400).json({error:`user_id_function_not_configured`,message:`Bulker instance does not have a getUserId function configured`});let e=this.bulker.getUserId();if(!e)return t.json([]);let n=(await this.bulker.jobManager.getUserJobs(e)).filter(e=>e!==null);return n.sort((e,t)=>{let n=e.createdAt?new Date(e.createdAt).getTime():0;return(t.createdAt?new Date(t.createdAt).getTime():0)-n}),t.json(n)}catch(e){return n(e)}},this.bulker=e,this.router=t??d(),this.staticRoute=n,this.registerStaticRoutes()}registerStaticRoutes(){this.router.post(this.staticRoute,this.bulkHandler),this.router.get(`/jobs/:id`,this.getJobHandler),this.router.get(`/jobs`,this.getMyJobsHandler),this.router.post(`/jobs/:id/cancel`,this.cancelJobHandler)}addAction(e,t){let n=new Ee(this.bulker,{action:e,...t});if(this.bulker.logger.info(`Registering action handler: ${e}`),this.actionsToRouteMap.has(e))throw Error(`Action "${e}" is already registered`);return this.actionsToRouteMap.set(e,n),this.routes.push(n),n}getRouter(){return this.router}},$=class e{constructor(e,t){this.redis=e,this.defaults={maxJobsPerUser:t?.maxJobsPerUser??-1,jobTtlSeconds:t?.jobTtlSeconds??168*3600,errorLogLimit:t?.errorLogLimit??10}}setEventEmitter(e){this.eventEmitter=e}static jobKey(e){return`bulker:job:${e}`}static userJobsKey(e){return`bulker:user:${e}:jobs`}async initJob(t,n){let r=Date.now().toString(),i=e.jobKey(t),a=this.redis.multi();a.hSet(i,{status:n.status,total:String(n.total),queued:`0`,processed:`0`,succeeded:`0`,failed:`0`,action:n.action,errors:JSON.stringify([]),createdAt:r,updatedAt:r,startTime:r,endTime:``}),a.expire(i,this.defaults.jobTtlSeconds),await a.exec()}async getJob(t){let n=e.jobKey(t),r=await this.redis.hGetAll(n);if(!r||Object.keys(r).length===0)return null;let i=r.startTime?Number(r.startTime):null,a=r.endTime&&r.endTime!==``?Number(r.endTime):null,o=Date.now(),s={startTime:i?new Date(i).toISOString():null,endTime:a?new Date(a).toISOString():null,durationMs:i?(a||o)-i:null};return{jobId:t,status:r.status,action:r.action,total:Number(r.total??0),queued:Number(r.queued??0),processed:Number(r.processed??0),succeeded:Number(r.succeeded??0),failed:Number(r.failed??0),errors:r.errors?JSON.parse(r.errors):[],createdAt:r.createdAt?new Date(Number(r.createdAt)).toISOString():void 0,updatedAt:r.updatedAt?new Date(Number(r.updatedAt)).toISOString():void 0,duration:s}}async setJobField(t,n,r){let i=e.jobKey(t);if(!await this.redis.exists(i))return!1;let a=this.redis.multi();return a.hSet(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString()),await a.exec(),!0}async setJobFields(t,n){let r=e.jobKey(t);if(!await this.redis.exists(r))return!1;let i=this.redis.multi();return i.hSet(r,n),i.hSet(r,`updatedAt`,Date.now().toString()),await i.exec(),!0}async incrJobField(t,n,r=1){let i=e.jobKey(t),a=this.redis.multi();a.hIncrBy(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString());let o=await a.exec();return o?.[0]&&o[0][0]===null?o[0][1]:0}async getJobField(t,n){let r=e.jobKey(t);return this.redis.hGet(r,n)}async cancelJob(e){return this.setJobField(e,`status`,`canceled`)}async completeEmptyJob(e){let t=Date.now().toString();await this.setJobFields(e,{status:`completed`,endTime:t})}async ack(t,n=1){let r=e.jobKey(t);await this.redis.eval(`
2
+ consumerBatchSize ${this.consumerBatchSize}, identityScopes: ${r.join(`, `)}`),this.identityScopeSchema=Q(r),this.startRabbitWorker().catch(e=>{this.bulker.logger.error(`Failed to start RabbitMQ worker for queue ${this.rabbitQueueName}: ${e.message||e}`)})}async getUserJobs(e,t=20){return this.bulker.jobManager.getUserJobs(e,t)}batchIds(e){let t=[];for(let n=0;n<e.length;n+=this.consumerBatchSize)t.push(e.slice(n,n+this.consumerBatchSize));return t}async rabbitScanAndEnqueue(t,n,r){if(!this.rabbit)throw Error(`RabbitMQ not configured in Bulker`);let i=Date.now().toString();if(this.bulker.getUserId){let e=this.bulker.getUserId();e&&await this.bulker.jobManager.addUserJob(e,t)}await this.bulker.jobManager.setJobFields(t,{status:`running`,startTime:i}),this.bulker.emitEvent(`job:started`,{jobId:t,action:this.action});let a=null,o=0,s=0;for(;;){let i=await this.bulker.jobManager.getJobField(t,`status`);if(!i||i===`canceled`){this.bulker.logger.info(`Job ${t} was canceled, stopping scan and enqueue`);break}let c=a?{[e.and]:[n.where,{[this.idField]:{[e.gt]:a}}]}:n.where,l=await this.model.scope(this.modelScopes).findAll({where:c,...T(n,[`include`,`order`,`scopes`]),attributes:[this.idField],order:[[this.idField,`ASC`]],limit:this.pageSize,raw:!0,subQuery:!1});if(l.length===0)break;let u,d=l.map(e=>e[this.idField]);this.consumerBatchSize===1&&d.length===1&&(u=d[0]);let f=this.batchIds(d).map(e=>({jobId:t,ids:e,payload:r,id:u}));this.bulker.logger.info(`Enqueuing ${f.length} batched messages (${d.length} total IDs) to RabbitMQ queue ${this.rabbitQueueName}`);let p=(await Promise.allSettled(f.map(e=>this.rabbit.sendToQueue(this.rabbitQueueName,e)))).filter(e=>e.status===`rejected`);if(p.length>0)throw this.bulker.logger.error(`Failed to enqueue ${p.length} messages to RabbitMQ`,{rejected:p}),Error(`Failed to enqueue ${p.length} messages to RabbitMQ`);o+=l.length,await this.bulker.jobManager.incrJobField(t,`queued`,l.length),a=l[l.length-1][this.idField],s+=1,this.bulker.emitEvent(`scan:page`,{jobId:t,action:this.action,pageNumber:s,itemsInPage:l.length});let m=await this.bulker.jobManager.getJob(t);m&&this.bulker.emitEvent(`job:queued`,{jobId:t,action:this.action,queued:o,total:m.total})}o===0&&await this.bulker.jobManager.completeEmptyJob(t)}async startRabbitWorker(){return this.bulker.logger.info(`Starting RabbitMQ consumer for queue ${this.rabbitQueueName}`),this.bulker.emitEvent(`worker:started`,{action:this.action,queueName:this.rabbitQueueName}),this.rabbit?.consume(this.rabbitQueueName,async(e,t,n)=>{if(!e)return;let{jobId:r,ids:i,payload:a}=e.content,o=!1,s=async()=>{if(!o){o=!0,await Promise.all([this.bulker.jobManager.ack(r,i.length),t()]);for(let e of i)this.bulker.emitEvent(`item:processed`,{jobId:r,action:this.action,id:e})}},c=async(e,t)=>{if(o)return;o=!0;let a=t;a===void 0&&(a=X.isBulkerError(e)?e.retryable:!1),await Promise.all([this.bulker.jobManager.nack(r,e?e.message||String(e):`nacked`,{ids:i},i.length),n(void 0,{skipRetry:!a})]);for(let t of i)a?this.bulker.emitEvent(`item:retrying`,{jobId:r,action:this.action,id:t}):this.bulker.emitEvent(`item:failed`,{jobId:r,action:this.action,id:t,error:e?e.message||String(e):`nacked`})},l=async e=>{if(o)return;o=!0;let n=e.succeeded??[],i=e.failed??[];await Promise.all([this.bulker.jobManager.partialAck(r,n.length,i),t()]);for(let e of n)this.bulker.emitEvent(`item:processed`,{jobId:r,action:this.action,id:e});for(let e of i)this.bulker.emitEvent(`item:failed`,{jobId:r,action:this.action,id:e.id,error:e.error||`Item failed`})},u=await this.bulker.jobManager.getJobField(r,`status`);if(!u||u===`canceled`){await this.bulker.jobManager.incrJobField(r,`processed`,i.length);return}try{this.bulker.logger.info(`Started processing job ${r} action ${this.action} ids amount: ${i.length}`);for(let e of i)this.bulker.emitEvent(`item:processing`,{jobId:r,action:this.action,id:e});await this.consumer({jobId:r,ids:i,id:this.consumerBatchSize===1?i[0]:void 0,payload:a},s,c,l),await s()}catch(e){this.bulker.logger.error(`Error processing job ${r} action ${this.action}: ${e.message||e}`,{err:e}),this.bulker.emitEvent(`worker:error`,{action:this.action,error:e.message||String(e)}),await c(e,!1)}},this.consumerOptions)}},De=class{constructor(e,t,n=`/bulk-actions`){this.routes=[],this.actionsToRouteMap=new Map,this.bulkHandler=async(e,t,n)=>{try{let{action:r}=e.body??{};if(!r)return t.status(400).json({error:`missing_action`,message:`Request body must include an "action" field`});let i=this.actionsToRouteMap.get(r);return i?i.bulkHandler(e,t,n):t.status(404).json({error:`unknown_action`,message:`Action "${r}" is not registered`,availableActions:Array.from(this.actionsToRouteMap.keys())})}catch(e){return this.bulker.logger.error(`Error in BulkRouter bulkHandler: ${e.message||e}`,{err:e}),n(e),null}},this.getJobHandler=async(e,t,n)=>{try{if(!e.params.id)return t.status(400).json({error:`missing_id`});let n=await this.bulker.jobManager.getJob(e.params.id);return n?t.json(n):t.status(404).json({error:`not_found`})}catch(e){return n(e)}},this.cancelJobHandler=async(e,t,n)=>{try{return e.params.id?await this.bulker.jobManager.cancelJob(e.params.id)?(this.bulker.logger.info(`Job ${e.params.id} cancel requested`),t.json({ok:!0})):t.status(404).json({error:`not_found`}):t.status(400).json({error:`missing_id`})}catch(e){return n(e)}},this.getMyJobsHandler=async(e,t,n)=>{try{if(!this.bulker.getUserId)return t.status(400).json({error:`user_id_function_not_configured`,message:`Bulker instance does not have a getUserId function configured`});let e=this.bulker.getUserId();if(!e)return t.json([]);let n=(await this.bulker.jobManager.getUserJobs(e)).filter(e=>e!==null);return n.sort((e,t)=>{let n=e.createdAt?new Date(e.createdAt).getTime():0;return(t.createdAt?new Date(t.createdAt).getTime():0)-n}),t.json(n)}catch(e){return n(e)}},this.bulker=e,this.router=t??d(),this.staticRoute=n,this.registerStaticRoutes()}registerStaticRoutes(){this.router.post(this.staticRoute,this.bulkHandler),this.router.get(`/jobs/:id`,this.getJobHandler),this.router.get(`/jobs`,this.getMyJobsHandler),this.router.post(`/jobs/:id/cancel`,this.cancelJobHandler)}addAction(e,t){let n=new Ee(this.bulker,{action:e,...t});if(this.bulker.logger.info(`Registering action handler: ${e}`),this.actionsToRouteMap.has(e))throw Error(`Action "${e}" is already registered`);return this.actionsToRouteMap.set(e,n),this.routes.push(n),n}getRouter(){return this.router}},$=class e{constructor(e,t){this.redis=e,this.defaults={maxJobsPerUser:t?.maxJobsPerUser??-1,jobTtlSeconds:t?.jobTtlSeconds??168*3600,errorLogLimit:t?.errorLogLimit??10}}setEventEmitter(e){this.eventEmitter=e}static jobKey(e){return`bulker:job:${e}`}static userJobsKey(e){return`bulker:user:${e}:jobs`}async initJob(t,n){let r=Date.now().toString(),i=e.jobKey(t),a=this.redis.multi();a.hSet(i,{status:n.status,total:String(n.total),queued:`0`,processed:`0`,succeeded:`0`,failed:`0`,action:n.action,errors:JSON.stringify([]),createdAt:r,updatedAt:r,startTime:r,endTime:``}),a.expire(i,this.defaults.jobTtlSeconds),await a.exec()}async getJob(t){let n=e.jobKey(t),r=await this.redis.hGetAll(n);if(!r||Object.keys(r).length===0)return null;let i=r.startTime?Number(r.startTime):null,a=r.endTime&&r.endTime!==``?Number(r.endTime):null,o=Date.now(),s={startTime:i?new Date(i).toISOString():null,endTime:a?new Date(a).toISOString():null,durationMs:i?(a||o)-i:null};return{jobId:t,status:r.status,action:r.action,total:Number(r.total??0),queued:Number(r.queued??0),processed:Number(r.processed??0),succeeded:Number(r.succeeded??0),failed:Number(r.failed??0),errors:r.errors?JSON.parse(r.errors):[],createdAt:r.createdAt?new Date(Number(r.createdAt)).toISOString():void 0,updatedAt:r.updatedAt?new Date(Number(r.updatedAt)).toISOString():void 0,duration:s}}async setJobField(t,n,r){let i=e.jobKey(t);if(!await this.redis.exists(i))return!1;let a=this.redis.multi();return a.hSet(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString()),await a.exec(),!0}async setJobFields(t,n){let r=e.jobKey(t);if(!await this.redis.exists(r))return!1;let i=this.redis.multi();return i.hSet(r,n),i.hSet(r,`updatedAt`,Date.now().toString()),await i.exec(),!0}async incrJobField(t,n,r=1){let i=e.jobKey(t),a=this.redis.multi();a.hIncrBy(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString());let o=await a.exec();return o?.[0]&&o[0][0]===null?o[0][1]:0}async getJobField(t,n){let r=e.jobKey(t);return this.redis.hGet(r,n)}async cancelJob(e){return this.setJobField(e,`status`,`canceled`)}async completeEmptyJob(e){let t=Date.now().toString();await this.setJobFields(e,{status:`completed`,endTime:t})}async ack(e,t=1){await this.partialAck(e,t,[])}async nack(e,t,n,r=1){let i=[],a=e=>n.ids&&Array.isArray(n.ids)?n.ids[e]??`unknown-${e}`:`unknown-${e}`;for(let e=0;e<r;e++)i.push({id:a(e),error:t});await this.partialAck(e,0,i)}async partialAck(t,n,r){let i=e.jobKey(t);await this.redis.eval(`
3
3
  local jobKey = KEYS[1]
4
- local count = tonumber(ARGV[1]) or 1
5
- local updatedAt = ARGV[2]
4
+ local succeededCount = tonumber(ARGV[1]) or 0
5
+ local failedCount = tonumber(ARGV[2]) or 0
6
+ local failedResults = ARGV[3]
7
+ local errorLogLimit = tonumber(ARGV[4])
8
+ local updatedAt = ARGV[5]
9
+
6
10
  local total = tonumber(redis.call('HGET', jobKey, 'total'))
7
11
  if not total then
8
12
  return 0
9
13
  end
10
14
 
11
- local processed = redis.call('HINCRBY', jobKey, 'processed', count)
12
- redis.call('HINCRBY', jobKey, 'succeeded', count)
15
+ -- Update counters
16
+ local totalCount = succeededCount + failedCount
17
+ redis.call('HINCRBY', jobKey, 'succeeded', succeededCount)
18
+ redis.call('HINCRBY', jobKey, 'failed', failedCount)
19
+ local processed = redis.call('HINCRBY', jobKey, 'processed', totalCount)
13
20
  redis.call('HSET', jobKey, 'updatedAt', updatedAt)
14
21
 
15
- if processed >= total then
16
- redis.call('HSET', jobKey, 'status', 'completed')
17
- redis.call('HSET', jobKey, 'endTime', updatedAt)
18
- end
19
-
20
- return processed
21
- `,{keys:[r],arguments:[n.toString(),Date.now().toString()]});let i=await this.getJob(t);if(i&&i.status===`completed`){let e=i.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:i.action,processed:i.processed,failed:i.failed,duration:e})}}async nack(t,n,r,i=1){let a=e.jobKey(t);await this.redis.eval(`
22
- local jobKey = KEYS[1]
23
- local errorMsg = ARGV[1]
24
- local errorData = ARGV[2]
25
- local errorLogLimit = tonumber(ARGV[3])
26
- local count = tonumber(ARGV[4]) or 1
27
- local updatedAt = ARGV[5]
28
-
29
- local total = tonumber(redis.call('HGET', jobKey, 'total'))
30
- if not total then
31
- return 0
32
- end
22
+ -- Process failed results and add errors
23
+ if failedCount > 0 then
24
+ local errorsJson = redis.call('HGET', jobKey, 'errors') or '[]'
25
+ local errors = cjson.decode(errorsJson)
26
+ local failedData = cjson.decode(failedResults)
33
27
 
34
- -- Add errors (one per failed item in the batch)
35
- local errorsJson = redis.call('HGET', jobKey, 'errors') or '[]'
36
- local errors = cjson.decode(errorsJson)
37
- local errorData = cjson.decode(errorData)
28
+ for i = 1, #failedData do
29
+ if #errors >= errorLogLimit then
30
+ -- Keep only the last (errorLogLimit - 1) entries
31
+ local newErrors = {}
32
+ for j = #errors - errorLogLimit + 2, #errors do
33
+ table.insert(newErrors, errors[j])
34
+ end
35
+ errors = newErrors
36
+ end
38
37
 
39
- -- Add one error entry per failed item
40
- for i = 1, count do
41
- if #errors >= errorLogLimit then
42
- -- Keep only the last (errorLogLimit - 1) entries
43
- local newErrors = {}
44
- for j = #errors - errorLogLimit + 2, #errors do
45
- table.insert(newErrors, errors[j])
38
+ local failedItem = failedData[i]
39
+ local errorData
40
+ -- If id is a table (object), store it as-is (for backwards compat with nack)
41
+ -- Otherwise, wrap simple values in { id: value } structure (for partialAck)
42
+ if type(failedItem.id) == "table" then
43
+ errorData = failedItem.id
44
+ else
45
+ errorData = { id = failedItem.id }
46
46
  end
47
- errors = newErrors
48
- end
49
47
 
50
- -- Add error with individual ID if ids array is provided, otherwise use batch data
51
- local errorEntry = { message = errorMsg, data = errorData }
52
- if errorData.ids and errorData.ids[i] then
53
- errorEntry.data = { id = errorData.ids[i], payload = errorData.payload }
48
+ local errorEntry = {
49
+ message = failedItem.error or 'Item failed',
50
+ data = errorData
51
+ }
52
+ table.insert(errors, errorEntry)
54
53
  end
55
54
 
56
- table.insert(errors, errorEntry)
55
+ redis.call('HSET', jobKey, 'errors', cjson.encode(errors))
57
56
  end
58
57
 
59
- redis.call('HSET', jobKey, 'errors', cjson.encode(errors))
60
-
61
- -- Update counters
62
- redis.call('HINCRBY', jobKey, 'failed', count)
63
- local processed = redis.call('HINCRBY', jobKey, 'processed', count)
64
- redis.call('HSET', jobKey, 'updatedAt', updatedAt)
65
-
58
+ -- Check if job is complete
66
59
  if processed >= total then
67
60
  redis.call('HSET', jobKey, 'status', 'completed')
68
61
  redis.call('HSET', jobKey, 'endTime', updatedAt)
69
62
  end
70
63
 
71
64
  return processed
72
- `,{keys:[a],arguments:[n,JSON.stringify(r),String(this.defaults.errorLogLimit),i.toString(),Date.now().toString()]});let o=await this.getJob(t);if(o&&o.status===`completed`){let e=o.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:o.action,processed:o.processed,failed:o.failed,duration:e})}}async addUserJob(t,n){let r=e.userJobsKey(t),i=this.redis.multi();i.lPush(r,n),this.defaults.maxJobsPerUser>0&&i.lTrim(r,0,this.defaults.maxJobsPerUser-1),i.expire(r,3600),await i.exec()}async removeUserJob(t,n){let r=e.userJobsKey(t);await this.redis.lRem(r,0,n)}async getUserJobs(t,n=20){let r=e.userJobsKey(t),i=await this.redis.lRange(r,0,n-1);return await Promise.all(i.map(e=>this.getJob(e)))}};const Oe={emitEvent:()=>{}};var ke=class extends m{constructor(e){if(super(),this.bulkRouters=[],this.sequelize=e.sequelize,this.logger=e.logger,this.rabbit=e.rabbit,this.eventsEnabled=e.emitEvents??!1,e.redis&&typeof e.redis.connect==`function`)this.redis=e.redis,this.redis.isOpen||this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)});else{let t=e.redis;this.redis=u({socket:{host:t.host,port:t.port},commandsQueueMaxLength:5e3,disableOfflineQueue:!1,password:t.password,database:t.db}),this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)})}this.getUserId=e.getUserId??(()=>null),this.defaults={maxJobsPerUser:e.defaults?.maxJobsPerUser??-1,pageSize:e.defaults?.pageSize??1e3,idField:e.defaults?.idField??`id`,consumerBatchSize:e.defaults?.consumerBatchSize??1,workerConcurrency:e.defaults?.workerConcurrency??16,jobTtlSeconds:e.defaults?.jobTtlSeconds??168*3600,errorLogLimit:e.defaults?.errorLogLimit??10},this.jobManager=new $(this.redis,{maxJobsPerUser:this.defaults.maxJobsPerUser,jobTtlSeconds:this.defaults.jobTtlSeconds,errorLogLimit:this.defaults.errorLogLimit}),this.jobManager.setEventEmitter(this.eventsEnabled?{emitEvent:this.emitEvent.bind(this)}:Oe)}pingRedis(){return this.redis.ping()}createBulkRouter(e,t){let n=new De(this,e,t);return this.bulkRouters.push(n),n}emitEvent(e,...t){this.eventsEnabled&&this.emit(e,...t)}};export{ke as Bulker,X as BulkerError,$ as BulkerJobManager,_ as formatOperators,P as generateFilterReplacements,xe as queryFormatMiddleware,Se as queryHandler,be as queryValidationMiddleware,U as validatePayload,f as z};
65
+ `,{keys:[i],arguments:[String(n),String(r.length),JSON.stringify(r),String(this.defaults.errorLogLimit),Date.now().toString()]});let a=await this.getJob(t);if(a&&a.status===`completed`){let e=a.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:a.action,processed:a.processed,failed:a.failed,duration:e})}}async addUserJob(t,n){let r=e.userJobsKey(t),i=this.redis.multi();i.lPush(r,n),this.defaults.maxJobsPerUser>0&&i.lTrim(r,0,this.defaults.maxJobsPerUser-1),i.expire(r,3600),await i.exec()}async removeUserJob(t,n){let r=e.userJobsKey(t);await this.redis.lRem(r,0,n)}async getUserJobs(t,n=20){let r=e.userJobsKey(t),i=await this.redis.lRange(r,0,n-1);return await Promise.all(i.map(e=>this.getJob(e)))}};const Oe={emitEvent:()=>{}};var ke=class extends m{constructor(e){if(super(),this.bulkRouters=[],this.sequelize=e.sequelize,this.logger=e.logger,this.rabbit=e.rabbit,this.eventsEnabled=e.emitEvents??!1,e.redis&&typeof e.redis.connect==`function`)this.redis=e.redis,this.redis.isOpen||this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)});else{let t=e.redis;this.redis=u({socket:{host:t.host,port:t.port},commandsQueueMaxLength:5e3,disableOfflineQueue:!1,password:t.password,database:t.db}),this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)})}this.getUserId=e.getUserId??(()=>null),this.defaults={maxJobsPerUser:e.defaults?.maxJobsPerUser??-1,pageSize:e.defaults?.pageSize??1e3,idField:e.defaults?.idField??`id`,consumerBatchSize:e.defaults?.consumerBatchSize??1,workerConcurrency:e.defaults?.workerConcurrency??16,jobTtlSeconds:e.defaults?.jobTtlSeconds??168*3600,errorLogLimit:e.defaults?.errorLogLimit??10},this.jobManager=new $(this.redis,{maxJobsPerUser:this.defaults.maxJobsPerUser,jobTtlSeconds:this.defaults.jobTtlSeconds,errorLogLimit:this.defaults.errorLogLimit}),this.jobManager.setEventEmitter(this.eventsEnabled?{emitEvent:this.emitEvent.bind(this)}:Oe)}pingRedis(){return this.redis.ping()}createBulkRouter(e,t){let n=new De(this,e,t);return this.bulkRouters.push(n),n}emitEvent(e,...t){this.eventsEnabled&&this.emit(e,...t)}};export{ke as Bulker,X as BulkerError,$ as BulkerJobManager,_ as formatOperators,P as generateFilterReplacements,xe as queryFormatMiddleware,Se as queryHandler,be as queryValidationMiddleware,U as validatePayload,f as z};
73
66
  //# sourceMappingURL=index.js.map