@bobtail.software/b-durable 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +8 -8
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -23,6 +23,7 @@ interface WorkflowStateInfo<TInput = unknown, TOutput = unknown> {
|
|
|
23
23
|
name: string;
|
|
24
24
|
attempts: number;
|
|
25
25
|
};
|
|
26
|
+
tags?: string[];
|
|
26
27
|
}
|
|
27
28
|
interface WorkflowState {
|
|
28
29
|
tryCatchStack?: {
|
|
@@ -121,6 +122,7 @@ type Compute<T> = {
|
|
|
121
122
|
type StartOptions<TInput, TEvents> = Compute<{
|
|
122
123
|
input: TInput;
|
|
123
124
|
workflowId?: string;
|
|
125
|
+
tags?: string[];
|
|
124
126
|
} & WorkflowEventListeners<TEvents>>;
|
|
125
127
|
interface StartedWorkflowHandle {
|
|
126
128
|
workflowId: string;
|
|
@@ -185,6 +187,7 @@ declare class DurableRuntime {
|
|
|
185
187
|
private propagateFailureToParent;
|
|
186
188
|
signal<T>(workflowId: string, signalName: string, payload: T): Promise<void>;
|
|
187
189
|
cancel(workflowId: string, reason: string): Promise<void>;
|
|
190
|
+
cancelByTag(tag: string, reason: string): Promise<void>;
|
|
188
191
|
private startScheduler;
|
|
189
192
|
private checkDelayedTasks;
|
|
190
193
|
private checkSleepers;
|
|
@@ -260,6 +263,7 @@ interface BDurableAPI {
|
|
|
260
263
|
runtime: DurableRuntime;
|
|
261
264
|
cancel: (workflowId: string, reason: string) => Promise<void>;
|
|
262
265
|
getState: (workflowId: string) => Promise<WorkflowStateInfo | null>;
|
|
266
|
+
cancelByTag: (tag: string, reason: string) => Promise<void>;
|
|
263
267
|
/**
|
|
264
268
|
* Obtiene un "handle" para una instancia de workflow existente.
|
|
265
269
|
* Permite enviar señales (Input) y escuchar eventos (Output).
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{randomUUID as
|
|
1
|
+
import{randomUUID as B}from"crypto";import q from"ioredis";var m="queue:tasks",y="durable:sleepers",A="worker:heartbeat:",k="durable:workers",P="queue:dead",L="queue:tasks:delayed",I="index:tag:";function N(i){return`workflow:${i}`}var c={RUNNING:"RUNNING",SLEEPING:"SLEEPING",COMPLETED:"COMPLETED",FAILED:"FAILED",AWAITING_SIGNAL:"AWAITING_SIGNAL",AWAITING_SUBWORKFLOW:"AWAITING_SUBWORKFLOW",CANCELLING:"CANCELLING",CANCELLED:"CANCELLED",VERSION_MISMATCH:"VERSION_MISMATCH"};var l,b;function C(i){if(l||b){console.warn("[Persistence] Los clientes de Redis ya han sido configurados. Omitiendo.");return}l=i.commandClient,b=i.blockingClient}import{randomUUID as K}from"crypto";import v from"ms";import{resolve as Y}from"path";var S=class extends Error{isCancellation=!0;constructor(t){super(t),this.name="WorkflowCancellationError"}};var _=`
|
|
2
2
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
3
3
|
return redis.call("del", KEYS[1])
|
|
4
4
|
else
|
|
@@ -10,7 +10,7 @@ if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
|
10
10
|
else
|
|
11
11
|
return 0
|
|
12
12
|
end
|
|
13
|
-
`,
|
|
13
|
+
`,W=`
|
|
14
14
|
local lockKey = KEYS[1]
|
|
15
15
|
local workflowKey = KEYS[2]
|
|
16
16
|
local token = ARGV[1]
|
|
@@ -24,7 +24,7 @@ if redis.call("get", lockKey) == token then
|
|
|
24
24
|
else
|
|
25
25
|
return 0
|
|
26
26
|
end
|
|
27
|
-
`,
|
|
27
|
+
`,F=`
|
|
28
28
|
local lockKey = KEYS[1]
|
|
29
29
|
local workflowKey = KEYS[2]
|
|
30
30
|
local token = ARGV[1]
|
|
@@ -34,7 +34,7 @@ if redis.call("get", lockKey) == token then
|
|
|
34
34
|
else
|
|
35
35
|
return -1
|
|
36
36
|
end
|
|
37
|
-
|
|
37
|
+
`,$=`
|
|
38
38
|
local lockKey = KEYS[1]
|
|
39
39
|
local workflowKey = KEYS[2]
|
|
40
40
|
local token = ARGV[1]
|
|
@@ -47,7 +47,7 @@ if redis.call("get", lockKey) == token then
|
|
|
47
47
|
else
|
|
48
48
|
return 0
|
|
49
49
|
end
|
|
50
|
-
|
|
50
|
+
`,G=`
|
|
51
51
|
local lockKey = KEYS[1]
|
|
52
52
|
local workflowKey = KEYS[2]
|
|
53
53
|
local token = ARGV[1]
|
|
@@ -60,7 +60,7 @@ if redis.call("get", lockKey) == token then
|
|
|
60
60
|
else
|
|
61
61
|
return 0
|
|
62
62
|
end
|
|
63
|
-
`,
|
|
63
|
+
`,U=`
|
|
64
64
|
local limit = tonumber(ARGV[1])
|
|
65
65
|
local now = tonumber(ARGV[2])
|
|
66
66
|
local key = KEYS[1]
|
|
@@ -70,7 +70,7 @@ end
|
|
|
70
70
|
redis.call('zrem', key, unpack(ids))
|
|
71
71
|
end
|
|
72
72
|
return ids
|
|
73
|
-
`,
|
|
73
|
+
`,M=`
|
|
74
74
|
local sourceZSet = KEYS[1]
|
|
75
75
|
local destList = KEYS[2]
|
|
76
76
|
local now = tonumber(ARGV[1])
|
|
@@ -88,4 +88,4 @@ end
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
return #tasks
|
|
91
|
-
`;import M from"superjson";function h(i){return M.stringify(i)}function m(i){try{return M.parse(i)}catch(t){try{return JSON.parse(i)}catch(e){throw new Error(`Failed to deserialize data: ${t} ${e}`)}}}var Y={info:(i,t)=>console.log(`[INFO] ${i}`,t||""),error:(i,t)=>console.error(`[ERROR] ${i}`,t||""),warn:(i,t)=>console.warn(`[WARN] ${i}`,t||""),debug:(i,t)=>console.debug(`[DEBUG] ${i}`,t||"")},K=class{constructor(t){this.retention=t}getKey(t){return`workflow:${t}`}getLockKey(t){return`workflow:${t}:lock`}async acquireLock(t,e=10){let s=this.getLockKey(t),n=W();return await l.set(s,n,"EX",e,"NX")==="OK"?n:null}async releaseLock(t,e){await l.eval(P,1,this.getLockKey(t),e)}async renewLock(t,e,s){return await l.eval(D,1,this.getLockKey(t),e,s)===1}async get(t){let e=await l.hgetall(this.getKey(t));return!e||Object.keys(e).length===0?null:{workflowId:e.workflowId,name:e.name,version:e.version,status:e.status,step:parseInt(e.step,10),input:m(e.input),state:m(e.state),result:e.result?m(e.result):void 0,error:e.error,parentId:e.parentId,subWorkflowId:e.subWorkflowId,awaitingSignal:e.awaitingSignal||e.awaitingEvent,createdAt:e.createdAt?parseInt(e.createdAt,10):0,updatedAt:e.updatedAt?parseInt(e.updatedAt,10):0}}async create(t){let e=Date.now(),s={...t,step:0,state:{},createdAt:e,updatedAt:e},n={...s,input:h(s.input),state:h(s.state)};n.version===void 0&&delete n.version;let r=l.pipeline();r.hset(this.getKey(s.workflowId),n),await r.exec()}async updateState(t,e,s,n){if(await l.eval(A,2,this.getLockKey(t),this.getKey(t),n,h(e),Date.now(),s)===0)throw new Error(`Lock lost for workflow ${t}`)}async updateStatus(t,e,s={}){await l.hset(this.getKey(t),{status:e,...s,updatedAt:Date.now()})}async incrementStep(t,e){let s=await l.eval(_,2,this.getLockKey(t),this.getKey(t),e);if(s===-1)throw new Error(`Lock lost for workflow ${t}`);return s}async applyRetention(t){if(this.retention){let e=v(this.retention)/1e3;e>0&&await l.expire(this.getKey(t),e)}}async complete(t,e,s){if(await l.eval(F,2,this.getLockKey(t),this.getKey(t),s,h(e??null),c.COMPLETED)===0)throw new Error(`Lock lost for workflow ${t}`);await this.applyRetention(t)}async fail(t,e,s,n=c.FAILED){s?await l.eval($,2,this.getLockKey(t),this.getKey(t),s,e.message,n)===0&&console.warn(`Could not fail workflow ${t} safely: Lock lost.`):await l.hset(this.getKey(t),{status:n,error:e.message}),await this.applyRetention(t)}async scheduleSleep(t,e){await this.updateStatus(t,c.SLEEPING),await l.zadd(I,e,t)}async getWorkflowsToWake(t=100){let e=Date.now();return await l.eval(G,1,I,t,e)}async enqueueTask(t){await l.lpush(E,h(t))}async resumeForCatch(t,e,s,n){if(await l.eval(A,2,this.getLockKey(t),this.getKey(t),n,h(e),Date.now(),s)===0)throw new Error(`Lock lost for workflow ${t}`);await l.hset(this.getKey(t),{status:c.RUNNING})}async moveToDLQ(t,e){let s={...t,failedAt:Date.now(),error:e.message,stack:e.stack};await l.lpush(x,h(s))}async scheduleTaskRetry(t,e){let s=Date.now()+e;await l.zadd(L,s,h(t))}async moveDueTasksToQueue(t=100){return await l.eval(U,2,L,E,Date.now(),t)}},b=class{durableFns=new Map;repo;workerId=W();isRunning=!1;schedulerInterval=null;heartbeatInterval=null;sourceRoot;pollingInterval;logger;maxTaskRetries=3;constructor(t){this.sourceRoot=t.sourceRoot,this.repo=new K(t.retention),this.pollingInterval=t.pollingInterval||5e3,this.logger=t.logger||Y}async getState(t){let e=await this.repo.get(t);return e?{workflowId:e.workflowId,name:e.name,version:e.version,status:e.status,step:e.step,input:e.input,output:e.result,state:e.state,error:e.error,createdAt:e.createdAt,updatedAt:e.updatedAt}:null}async start(t,e,s){let n=e.workflowId||W();if(e.workflowId){let r=await this.repo.get(e.workflowId);if(r&&r.status!==c.COMPLETED&&r.status!==c.FAILED)throw new Error(`Workflow with ID '${e.workflowId}' already exists and is in a running state (${r.status}).`)}return this.logger.info(`[RUNTIME] Iniciando workflow '${t.name}' v${t.version} con ID: ${n}`),await this.repo.create({workflowId:n,name:t.name,version:t.version,status:c.RUNNING,input:e.input,parentId:s}),setImmediate(()=>{this._executeStep(n,t).catch(r=>{this.logger.error("Error fatal en ejecuci\xF3n inicial",{error:r,workflowId:n})})}),{workflowId:n,unsubscribe:async()=>{}}}async scheduleExecution(t,e,s,n){setImmediate(()=>{this._executeStep(t,e,s,n).catch(r=>{this.logger.error("Error no manejado en scheduleExecution",{error:r,workflowId:t})})})}async _executeStep(t,e,s,n){let r=await this.repo.acquireLock(t);if(!r)return;let o=setInterval(()=>{this.repo.renewLock(t,r,10).catch(a=>this.logger.warn(`Error renovando lock para ${t}`,{error:a}))},5e3);try{if(n)throw n;let a=await this.repo.get(t);if(!a)return;if(a.status===c.CANCELLING)throw new S(a.error||"Workflow cancelled");if(a.status!==c.RUNNING)return;let p=a.version==="undefined"?void 0:a.version,d=e.version==="undefined"?void 0:e.version;if(String(p??"")!==String(d??"")){let w=new Error(`Version mismatch: DB=${p}, Code=${d}`);await this.repo.fail(t,w,r,c.VERSION_MISMATCH);return}let u={workflowId:t,step:a.step,input:a.input,state:a.state,result:s,log:(w,T)=>this.logger.info(w,{...T,workflowId:t,step:a.step})},g=await e.execute(u);await this.repo.updateState(t,u.state,u.step,r),await this.handleInstruction(g,u,a.name,r)&&(await this.repo.incrementStep(t,r),this.scheduleExecution(t,e,void 0))}catch(a){let p=a instanceof Error?a:new Error(String(a));this.logger.error("Error en workflow",{workflowId:t,error:p.message}),await this.handleFailure(t,p,e,r)}finally{clearInterval(o),await this.repo.releaseLock(t,r)}}async handleInstruction(t,e,s,n){let{workflowId:r}=e;switch(t.type){case"SCHEDULE_TASK":return await this.repo.enqueueTask({workflowId:r,durableFunctionName:s,...t}),!1;case"SCHEDULE_SLEEP":{let o=v(t.duration);if(typeof o!="number")throw new Error(`Invalid time value provided to bSleep: "${t.duration}"`);let a=Date.now()+o;return await this.repo.scheduleSleep(r,a),!1}case"WAIT_FOR_SIGNAL":return await this.repo.updateStatus(r,c.AWAITING_SIGNAL,{awaitingSignal:t.signalName}),await l.sadd(`signals:awaiting:${t.signalName}`,r),!1;case"EXECUTE_SUBWORKFLOW":{let o=this.durableFns.get(t.workflowName);if(!o)throw new Error(`Sub-workflow '${t.workflowName}' no encontrado.`);let{workflowId:a}=await this.start(o,{input:t.input},r);return await this.repo.updateStatus(r,c.AWAITING_SUBWORKFLOW,{subWorkflowId:a}),!1}case"EMIT_EVENT":{let o=`event:${r}`,a=h({eventName:t.eventName,payload:t.payload});return await l.publish(o,a),!0}case"COMPLETE":{let o=`event:${r}`,a=h({eventName:"workflow:completed",payload:t.result});return await l.publish(o,a),await this.repo.complete(r,t.result,n),await this.resumeParentWorkflow(r),!1}}}async handleFailure(t,e,s,n){let r=n;if(!r&&(r=await this.repo.acquireLock(t,20),!r)){this.logger.warn(`No se pudo adquirir lock para fallo en ${t}`);return}try{if(e instanceof S){await this.repo.fail(t,e,r,c.CANCELLED);let u=await this.repo.get(t);u?.subWorkflowId&&await this.cancel(u.subWorkflowId,`Parent workflow ${t} was cancelled`);return}let o=await this.repo.get(t);if(!o||o.status===c.FAILED||o.status===c.COMPLETED)return;let a=o.state.tryCatchStack;if(a&&a.length>0){let g=a.pop()?.catchStep;if(g!==void 0){this.logger.info(`Capturando error en step ${g}`,{workflowId:t}),await this.repo.resumeForCatch(t,o.state,g,r),this.scheduleExecution(t,s,{name:e.name,message:e.message,stack:e.stack});return}}let p=`event:${t}`,d=h({eventName:"workflow:failed",payload:{message:e.message}});await l.publish(p,d),await this.repo.fail(t,e,r),await this.propagateFailureToParent(t,e)}finally{!n&&r&&await this.repo.releaseLock(t,r)}}async resumeParentWorkflow(t){let e=await this.repo.get(t);if(!e?.parentId)return;let s=e.parentId,n=await this.repo.get(s);if(!n||n.status!==c.AWAITING_SUBWORKFLOW||n.subWorkflowId!==t)return;let r=this.durableFns.get(n.name);if(!r){await this.repo.fail(s,new Error(`Definici\xF3n del workflow '${n.name}' no encontrada.`),null);return}await this.repo.updateStatus(s,c.RUNNING,{subWorkflowId:""});let o=await this.repo.acquireLock(s);if(o)try{await this.repo.incrementStep(s,o),this.scheduleExecution(s,r,e.result)}finally{await this.repo.releaseLock(s,o)}else throw this.logger.warn(`Could not lock parent ${s} to resume. Retrying later...`),new Error(`Temporary Lock Failure: Could not acquire parent lock for ${s}`)}async propagateFailureToParent(t,e){let s=await this.repo.get(t);if(!s?.parentId)return;let n=s.parentId,r=await this.repo.get(n);if(!r||r.status!==c.AWAITING_SUBWORKFLOW||r.subWorkflowId!==t)return;let o=this.durableFns.get(r.name);if(!o){await this.repo.fail(n,new Error(`Definici\xF3n del workflow '${r.name}' no encontrada al propagar fallo.`),null);return}await this.repo.updateStatus(n,c.RUNNING,{subWorkflowId:""});let a=new Error(`Sub-workflow '${s.name}' (${t}) fall\xF3: ${e.message}`);a.stack=e.stack,this.scheduleExecution(n,o,void 0,a)}async signal(t,e,s){let n=null;for(let r=0;r<3&&(n=await this.repo.acquireLock(t),!n);r++)await new Promise(o=>setTimeout(o,50));if(!n)return this.logger.warn("Lock timeout en signal",{workflowId:t});try{let r=await this.repo.get(t);if(!r)return this.logger.warn("Se\xF1al para workflow inexistente",{workflowId:t});if(r.status!==c.AWAITING_SIGNAL||r.awaitingSignal!==e)return this.logger.warn("Workflow no esperaba esta se\xF1al",{workflowId:t,expected:r.awaitingSignal,received:e});let o=this.durableFns.get(r.name);if(!o){await this.repo.fail(t,new Error(`Funci\xF3n durable '${r.name}' no encontrada.`),n);return}await this.repo.updateStatus(t,c.RUNNING,{awaitingSignal:""}),await l.srem(`signals:awaiting:${e}`,t),await this.repo.incrementStep(t,n),this.scheduleExecution(t,o,s)}catch(r){let o=r instanceof Error?r:new Error(String(r)),a=(await this.repo.get(t))?.name||"",p=this.durableFns.get(a);await this.handleFailure(t,o,p,n)}finally{n&&await this.repo.releaseLock(t,n)}}async cancel(t,e){let s=await this.repo.acquireLock(t);if(!s)return await new Promise(n=>setTimeout(n,100)),this.cancel(t,e);try{let n=await this.repo.get(t);if(!n||[c.COMPLETED,c.FAILED,c.CANCELLED].includes(n.status))return;if(await this.repo.updateStatus(t,c.CANCELLING,{error:e}),n.status===c.SLEEPING){await l.zrem(I,t);let r=this.durableFns.get(n.name);this.scheduleExecution(t,r)}if(n.status===c.AWAITING_SIGNAL){let r=this.durableFns.get(n.name);this.scheduleExecution(t,r)}}finally{await this.repo.releaseLock(t,s)}}startScheduler(){if(this.schedulerInterval)return;this.logger.info(`Scheduler iniciado (${this.pollingInterval}ms)`);let t=async()=>{await this.checkSleepers(),await this.checkDelayedTasks(),await this.reapDeadWorkers()};this.schedulerInterval=setInterval(t,this.pollingInterval)}async checkDelayedTasks(){try{let t=await this.repo.moveDueTasksToQueue(50);t>0&&this.logger.debug(`Scheduler movi\xF3 ${t} tareas diferidas a la cola activa`)}catch(t){this.logger.error("Error chequeando tareas diferidas",{error:t})}}async checkSleepers(){let e=await this.repo.getWorkflowsToWake(50);e.length!==0&&await Promise.all(e.map(async s=>{let n=await this.repo.acquireLock(s);if(n)try{let r=await this.repo.get(s);if(r){let o=this.durableFns.get(r.name);o&&(this.logger.info("Despertando workflow",{workflowId:s}),await this.repo.updateStatus(s,c.RUNNING),await this.repo.incrementStep(s,n),this.scheduleExecution(s,o,void 0))}}finally{await this.repo.releaseLock(s,n)}}))}async reapDeadWorkers(){let t="0";do{let[e,s]=await l.sscan(k,t,"COUNT",100);t=e;for(let n of s){if(await l.exists(`${R}${n}`))continue;this.logger.warn(`Worker muerto ${n}. Recuperando tareas.`);let r=`${E}:processing:${n}`,o=await l.rpoplpush(r,E);for(;o;)o=await l.rpoplpush(r,E);await l.del(r),await l.srem(k,n)}}while(t!=="0")}startHeartbeat(){let t=`${R}${this.workerId}`,e=Math.max(Math.ceil(this.pollingInterval*3/1e3),5),s=()=>{this.isRunning&&l.set(t,Date.now().toString(),"EX",e).catch(()=>{})};this.heartbeatInterval=setInterval(s,this.pollingInterval),s()}startWorker(){if(this.isRunning)return;this.isRunning=!0;let t=`${E}:processing:${this.workerId}`;this.logger.info(`Worker ${this.workerId} iniciado`),this.startHeartbeat(),(async()=>{for(await l.sadd(k,this.workerId);this.isRunning;)try{let s=await y.brpoplpush(E,t,2);if(!s)continue;let n=m(s);this.logger.debug(`Ejecutando tarea: ${n.exportName}`,{workflowId:n.workflowId});try{let r;n.modulePath.startsWith("virtual:")?r=await import(n.modulePath):r=await import(V(this.sourceRoot,n.modulePath));let o=r[n.exportName];if(typeof o!="function")throw new Error(`'${n.exportName}' no es una funci\xF3n.`);let a=await o(...n.args),p=this.durableFns.get(n.durableFunctionName);if(p){let d=await this.repo.acquireLock(n.workflowId);if(d)try{await this.repo.incrementStep(n.workflowId,d),this.scheduleExecution(n.workflowId,p,a)}finally{await this.repo.releaseLock(n.workflowId,d)}else this.logger.warn(`No se pudo adquirir lock para avanzar workflow ${n.workflowId} tras tarea`,{task:n.exportName})}await l.lrem(t,1,s)}catch(r){let o=r instanceof Error?r:new Error(String(r));this.logger.error(`Fallo en tarea ${n.exportName}`,{workflowId:n.workflowId,error:o.message});let a=this.durableFns.get(n.durableFunctionName),p=a?.retryOptions||{},d=p.maxAttempts??3,u=(n.attempts||0)+1;if(n.attempts=u,u<=d){let g=p.initialInterval?v(p.initialInterval):1e3,f=p.backoffCoefficient??2,w=p.maxInterval?v(p.maxInterval):36e5,T=g*Math.pow(f,u-1);T>w&&(T=w),this.logger.warn(`Reintentando tarea en ${v(T)} (intento ${u}/${d===1/0?"Inf":d})`,{workflowId:n.workflowId}),T>0?await this.repo.scheduleTaskRetry(n,T):await l.lpush(E,h(n)),await l.lrem(t,1,s)}else this.logger.error("Reintentos agotados. Moviendo a DLQ.",{workflowId:n.workflowId}),await this.repo.moveToDLQ(n,o),a?await this.handleFailure(n.workflowId,o,a,null):await this.repo.fail(n.workflowId,new Error(`Def missing for ${n.durableFunctionName}`),null),await l.lrem(t,1,s)}}catch(s){if(!this.isRunning)break;this.logger.error("Error infraestructura worker",{error:s}),await new Promise(n=>setTimeout(n,5e3))}})()}run(t){this.durableFns=t,this.startWorker(),this.startScheduler()}async stop(){this.isRunning=!1,this.schedulerInterval&&clearInterval(this.schedulerInterval),this.heartbeatInterval&&clearInterval(this.heartbeatInterval),await l.srem(k,this.workerId),this.logger.info("Runtime detenido")}};var H=i=>({...i,__isDurable:!0});var B={info:(i,t)=>console.log(`[INFO] ${i}`,t||""),error:(i,t)=>console.error(`[ERROR] ${i}`,t||""),warn:(i,t)=>console.warn(`[WARN] ${i}`,t||""),debug:(i,t)=>console.debug(`[DEBUG] ${i}`,t||"")},Q=i=>{if(!i.startsWith("on")||i.length<=2)return null;let t=i.slice(2);return t.charAt(0).toLowerCase()+t.slice(1)};function Rt(i){let t=i.logger||B;t.info("--- Inicializando Sistema Durable ---"),N({commandClient:i.redisClient,blockingClient:i.blockingRedisClient});let e=new b({sourceRoot:i.sourceRoot,retention:i.retention,pollingInterval:i.pollingInterval,logger:t});e.run(i.durableFunctions);let s=new q(i.redisClient.options),n=new Map;s.psubscribe("event:*",o=>{o&&t.error("Error fatal al suscribirse a los canales de eventos:",{error:o})}),s.on("pmessage",(o,a,p)=>{let d=n.get(a);if(d&&d.length>0)try{let u=m(p),g={name:u.eventName||u.signalName,payload:u.payload};[...d].forEach(f=>f(g))}catch(u){t.error(`Error al parsear evento en ${a}`,{error:u})}});let r=(o,a)=>{let p=`event:${a}`,d=u=>(n.has(p)||n.set(p,[]),n.get(p)?.push(u),()=>{let g=n.get(p);if(g){let f=g.indexOf(u);f>-1&&g.splice(f,1),g.length===0&&n.delete(p)}});return{workflowId:a,signal:async(u,g)=>{await e.signal(a,u,g)},on:async(u,g)=>{let f=C(a),w=await i.redisClient.hgetall(f);return u==="workflow:completed"&&w.status===c.COMPLETED?(g(m(w.result||"null")),{unsubscribe:()=>{}}):u==="workflow:failed"&&w.status===c.FAILED?(g({message:w.error||"Unknown"}),{unsubscribe:()=>{}}):{unsubscribe:d(O=>{O.name===u&&g(O.payload)})}},subscribe:async u=>({unsubscribe:d(f=>{u(f)})})}};return{start:async(o,a)=>{let p=a.workflowId||z(),d=[],u=`event:${p}`,g={};if(Object.keys(a).forEach(f=>{let w=Q(f);w&&typeof a[f]=="function"&&(g[w]=a[f])}),Object.keys(g).length>0){n.has(u)||n.set(u,[]);let f=w=>{let T=g[w.name];T&&T(w.payload)};n.get(u)?.push(f),d.push(()=>{let w=n.get(u);if(w){let T=w.indexOf(f);T>-1&&w.splice(T,1)}})}return await e.start(o,{workflowId:p,input:a.input}),{workflowId:p,unsubscribe:async()=>{d.forEach(f=>f())}}},stop:()=>{e.stop(),s.quit().catch(()=>{})},runtime:e,cancel:(o,a)=>e.cancel(o,a),getState:o=>e.getState(o),getHandle:(o,a)=>r(o,a)}}export{S as WorkflowCancellationError,H as bDurable,Rt as bDurableInitialize};
|
|
91
|
+
`;import V from"superjson";function h(i){return V.stringify(i)}function E(i){try{return V.parse(i)}catch(t){try{return JSON.parse(i)}catch(e){throw new Error(`Failed to deserialize data: ${t} ${e}`)}}}var H={info:(i,t)=>console.log(`[INFO] ${i}`,t||""),error:(i,t)=>console.error(`[ERROR] ${i}`,t||""),warn:(i,t)=>console.warn(`[WARN] ${i}`,t||""),debug:(i,t)=>console.debug(`[DEBUG] ${i}`,t||"")},x=class{constructor(t){this.retention=t}getKey(t){return`workflow:${t}`}getLockKey(t){return`workflow:${t}:lock`}async acquireLock(t,e=10){let r=this.getLockKey(t),n=K();return await l.set(r,n,"EX",e,"NX")==="OK"?n:null}async releaseLock(t,e){await l.eval(_,1,this.getLockKey(t),e)}async renewLock(t,e,r){return await l.eval(D,1,this.getLockKey(t),e,r)===1}async get(t){let e=await l.hgetall(this.getKey(t));return!e||Object.keys(e).length===0?null:{workflowId:e.workflowId,name:e.name,version:e.version,status:e.status,step:parseInt(e.step,10),input:E(e.input),state:E(e.state),result:e.result?E(e.result):void 0,error:e.error,parentId:e.parentId,subWorkflowId:e.subWorkflowId,awaitingSignal:e.awaitingSignal||e.awaitingEvent,createdAt:e.createdAt?parseInt(e.createdAt,10):0,updatedAt:e.updatedAt?parseInt(e.updatedAt,10):0,tags:e.tags?JSON.parse(e.tags):[]}}async create(t){let e=Date.now(),r={...t,step:0,state:{},createdAt:e,updatedAt:e,tags:t.tags||[]},n={...r,input:h(r.input),state:h(r.state),tags:JSON.stringify(r.tags)};n.version===void 0&&delete n.version;let s=l.pipeline();if(s.hset(this.getKey(r.workflowId),n),r.tags&&r.tags.length>0)for(let a of r.tags)s.sadd(`${I}${a}`,r.workflowId);await s.exec()}async getIdsByTag(t){return l.smembers(`${I}${t}`)}async removeTagIndex(t,e){if(!e.length)return;let r=l.pipeline();for(let n of e)r.srem(`${I}${n}`,t);await r.exec()}async updateState(t,e,r,n){if(await l.eval(W,2,this.getLockKey(t),this.getKey(t),n,h(e),Date.now(),r)===0)throw new Error(`Lock lost for workflow ${t}`)}async updateStatus(t,e,r={}){await l.hset(this.getKey(t),{status:e,...r,updatedAt:Date.now()})}async incrementStep(t,e){let r=await l.eval(F,2,this.getLockKey(t),this.getKey(t),e);if(r===-1)throw new Error(`Lock lost for workflow ${t}`);return r}async applyRetention(t){if(this.retention){let e=v(this.retention)/1e3;e>0&&await l.expire(this.getKey(t),e)}}async complete(t,e,r){if(await l.eval($,2,this.getLockKey(t),this.getKey(t),r,h(e??null),c.COMPLETED)===0)throw new Error(`Lock lost for workflow ${t}`);await this.applyRetention(t)}async fail(t,e,r,n=c.FAILED){r?await l.eval(G,2,this.getLockKey(t),this.getKey(t),r,e.message,n)===0&&console.warn(`Could not fail workflow ${t} safely: Lock lost.`):await l.hset(this.getKey(t),{status:n,error:e.message}),await this.applyRetention(t)}async scheduleSleep(t,e){await this.updateStatus(t,c.SLEEPING),await l.zadd(y,e,t)}async getWorkflowsToWake(t=100){let e=Date.now();return await l.eval(U,1,y,t,e)}async enqueueTask(t){await l.lpush(m,h(t))}async resumeForCatch(t,e,r,n){if(await l.eval(W,2,this.getLockKey(t),this.getKey(t),n,h(e),Date.now(),r)===0)throw new Error(`Lock lost for workflow ${t}`);await l.hset(this.getKey(t),{status:c.RUNNING})}async moveToDLQ(t,e){let r={...t,failedAt:Date.now(),error:e.message,stack:e.stack};await l.lpush(P,h(r))}async scheduleTaskRetry(t,e){let r=Date.now()+e;await l.zadd(L,r,h(t))}async moveDueTasksToQueue(t=100){return await l.eval(M,2,L,m,Date.now(),t)}},R=class{durableFns=new Map;repo;workerId=K();isRunning=!1;schedulerInterval=null;heartbeatInterval=null;sourceRoot;pollingInterval;logger;maxTaskRetries=3;constructor(t){this.sourceRoot=t.sourceRoot,this.repo=new x(t.retention),this.pollingInterval=t.pollingInterval||5e3,this.logger=t.logger||H}async getState(t){let e=await this.repo.get(t);return e?{workflowId:e.workflowId,name:e.name,version:e.version,status:e.status,step:e.step,input:e.input,output:e.result,state:e.state,error:e.error,createdAt:e.createdAt,updatedAt:e.updatedAt,tags:e.tags}:null}async start(t,e,r){let n=e.workflowId||K();if(e.workflowId){let s=await this.repo.get(e.workflowId);if(s&&s.status!==c.COMPLETED&&s.status!==c.FAILED)throw new Error(`Workflow with ID '${e.workflowId}' already exists and is in a running state (${s.status}).`)}return this.logger.info(`[RUNTIME] Iniciando workflow '${t.name}' v${t.version} con ID: ${n}`),await this.repo.create({workflowId:n,name:t.name,version:t.version,status:c.RUNNING,input:e.input,parentId:r,tags:e.tags||[]}),setImmediate(()=>{this._executeStep(n,t).catch(s=>{this.logger.error("Error fatal en ejecuci\xF3n inicial",{error:s,workflowId:n})})}),{workflowId:n,unsubscribe:async()=>{}}}async scheduleExecution(t,e,r,n){setImmediate(()=>{this._executeStep(t,e,r,n).catch(s=>{this.logger.error("Error no manejado en scheduleExecution",{error:s,workflowId:t})})})}async _executeStep(t,e,r,n){let s=await this.repo.acquireLock(t);if(!s)return;let a=setInterval(()=>{this.repo.renewLock(t,s,10).catch(o=>this.logger.warn(`Error renovando lock para ${t}`,{error:o}))},5e3);try{if(n)throw n;let o=await this.repo.get(t);if(!o)return;if(o.status===c.CANCELLING)throw new S(o.error||"Workflow cancelled");if(o.status!==c.RUNNING)return;let p=o.version==="undefined"?void 0:o.version,d=e.version==="undefined"?void 0:e.version;if(String(p??"")!==String(d??"")){let w=new Error(`Version mismatch: DB=${p}, Code=${d}`);await this.repo.fail(t,w,s,c.VERSION_MISMATCH);return}let u={workflowId:t,step:o.step,input:o.input,state:o.state,result:r,log:(w,T)=>this.logger.info(w,{...T,workflowId:t,step:o.step})},g=await e.execute(u);await this.repo.updateState(t,u.state,u.step,s),await this.handleInstruction(g,u,o.name,s)&&(await this.repo.incrementStep(t,s),this.scheduleExecution(t,e,void 0))}catch(o){let p=o instanceof Error?o:new Error(String(o));this.logger.error("Error en workflow",{workflowId:t,error:p.message}),await this.handleFailure(t,p,e,s)}finally{clearInterval(a),await this.repo.releaseLock(t,s)}}async handleInstruction(t,e,r,n){let{workflowId:s}=e;switch(t.type){case"SCHEDULE_TASK":return await this.repo.enqueueTask({workflowId:s,durableFunctionName:r,...t}),!1;case"SCHEDULE_SLEEP":{let a=v(t.duration);if(typeof a!="number")throw new Error(`Invalid time value provided to bSleep: "${t.duration}"`);let o=Date.now()+a;return await this.repo.scheduleSleep(s,o),!1}case"WAIT_FOR_SIGNAL":return await this.repo.updateStatus(s,c.AWAITING_SIGNAL,{awaitingSignal:t.signalName}),await l.sadd(`signals:awaiting:${t.signalName}`,s),!1;case"EXECUTE_SUBWORKFLOW":{let a=this.durableFns.get(t.workflowName);if(!a)throw new Error(`Sub-workflow '${t.workflowName}' no encontrado.`);let{workflowId:o}=await this.start(a,{input:t.input},s);return await this.repo.updateStatus(s,c.AWAITING_SUBWORKFLOW,{subWorkflowId:o}),!1}case"EMIT_EVENT":{let a=`event:${s}`,o=h({eventName:t.eventName,payload:t.payload});return await l.publish(a,o),!0}case"COMPLETE":{let a=`event:${s}`,o=h({eventName:"workflow:completed",payload:t.result});return await l.publish(a,o),await this.repo.complete(s,t.result,n),await this.resumeParentWorkflow(s),!1}}}async handleFailure(t,e,r,n){let s=n;if(!s&&(s=await this.repo.acquireLock(t,20),!s)){this.logger.warn(`No se pudo adquirir lock para fallo en ${t}`);return}try{if(e instanceof S){await this.repo.fail(t,e,s,c.CANCELLED);let u=await this.repo.get(t);u?.subWorkflowId&&await this.cancel(u.subWorkflowId,`Parent workflow ${t} was cancelled`);return}let a=await this.repo.get(t);if(!a||a.status===c.FAILED||a.status===c.COMPLETED)return;let o=a.state.tryCatchStack;if(o&&o.length>0){let g=o.pop()?.catchStep;if(g!==void 0){this.logger.info(`Capturando error en step ${g}`,{workflowId:t}),await this.repo.resumeForCatch(t,a.state,g,s),this.scheduleExecution(t,r,{name:e.name,message:e.message,stack:e.stack});return}}let p=`event:${t}`,d=h({eventName:"workflow:failed",payload:{message:e.message}});await l.publish(p,d),await this.repo.fail(t,e,s),await this.propagateFailureToParent(t,e)}finally{!n&&s&&await this.repo.releaseLock(t,s)}}async resumeParentWorkflow(t){let e=await this.repo.get(t);if(!e?.parentId)return;let r=e.parentId,n=await this.repo.get(r);if(!n||n.status!==c.AWAITING_SUBWORKFLOW||n.subWorkflowId!==t)return;let s=this.durableFns.get(n.name);if(!s){await this.repo.fail(r,new Error(`Definici\xF3n del workflow '${n.name}' no encontrada.`),null);return}await this.repo.updateStatus(r,c.RUNNING,{subWorkflowId:""});let a=await this.repo.acquireLock(r);if(a)try{await this.repo.incrementStep(r,a),this.scheduleExecution(r,s,e.result)}finally{await this.repo.releaseLock(r,a)}else throw this.logger.warn(`Could not lock parent ${r} to resume. Retrying later...`),new Error(`Temporary Lock Failure: Could not acquire parent lock for ${r}`)}async propagateFailureToParent(t,e){let r=await this.repo.get(t);if(!r?.parentId)return;let n=r.parentId,s=await this.repo.get(n);if(!s||s.status!==c.AWAITING_SUBWORKFLOW||s.subWorkflowId!==t)return;let a=this.durableFns.get(s.name);if(!a){await this.repo.fail(n,new Error(`Definici\xF3n del workflow '${s.name}' no encontrada al propagar fallo.`),null);return}await this.repo.updateStatus(n,c.RUNNING,{subWorkflowId:""});let o=new Error(`Sub-workflow '${r.name}' (${t}) fall\xF3: ${e.message}`);o.stack=e.stack,this.scheduleExecution(n,a,void 0,o)}async signal(t,e,r){let n=null;for(let s=0;s<3&&(n=await this.repo.acquireLock(t),!n);s++)await new Promise(a=>setTimeout(a,50));if(!n)return this.logger.warn("Lock timeout en signal",{workflowId:t});try{let s=await this.repo.get(t);if(!s)return this.logger.warn("Se\xF1al para workflow inexistente",{workflowId:t});if(s.status!==c.AWAITING_SIGNAL||s.awaitingSignal!==e)return this.logger.warn("Workflow no esperaba esta se\xF1al",{workflowId:t,expected:s.awaitingSignal,received:e});let a=this.durableFns.get(s.name);if(!a){await this.repo.fail(t,new Error(`Funci\xF3n durable '${s.name}' no encontrada.`),n);return}await this.repo.updateStatus(t,c.RUNNING,{awaitingSignal:""}),await l.srem(`signals:awaiting:${e}`,t),await this.repo.incrementStep(t,n),this.scheduleExecution(t,a,r)}catch(s){let a=s instanceof Error?s:new Error(String(s)),o=(await this.repo.get(t))?.name||"",p=this.durableFns.get(o);await this.handleFailure(t,a,p,n)}finally{n&&await this.repo.releaseLock(t,n)}}async cancel(t,e){let r=await this.repo.acquireLock(t);if(!r)return await new Promise(n=>setTimeout(n,100)),this.cancel(t,e);try{let n=await this.repo.get(t);if(!n||[c.COMPLETED,c.FAILED,c.CANCELLED].includes(n.status))return;if(await this.repo.updateStatus(t,c.CANCELLING,{error:e}),n.status===c.SLEEPING){await l.zrem(y,t);let s=this.durableFns.get(n.name);this.scheduleExecution(t,s)}if(n.status===c.AWAITING_SIGNAL){let s=this.durableFns.get(n.name);this.scheduleExecution(t,s)}}finally{await this.repo.releaseLock(t,r)}}async cancelByTag(t,e){this.logger.info(`[RUNTIME] Cancelando grupo de workflows por tag: ${t}`);let r=await this.repo.getIdsByTag(t);if(r.length===0){this.logger.debug(`No se encontraron workflows para el tag: ${t}`);return}let n=r.map(s=>this.cancel(s,e).catch(a=>{this.logger.error(`Error cancelando workflow ${s} del grupo ${t}`,{error:a})}));await Promise.all(n),this.logger.info(`[RUNTIME] Se enviaron se\xF1ales de cancelaci\xF3n a ${r.length} workflows del tag: ${t}`)}startScheduler(){if(this.schedulerInterval)return;this.logger.info(`Scheduler iniciado (${this.pollingInterval}ms)`);let t=async()=>{await this.checkSleepers(),await this.checkDelayedTasks(),await this.reapDeadWorkers()};this.schedulerInterval=setInterval(t,this.pollingInterval)}async checkDelayedTasks(){try{let t=await this.repo.moveDueTasksToQueue(50);t>0&&this.logger.debug(`Scheduler movi\xF3 ${t} tareas diferidas a la cola activa`)}catch(t){this.logger.error("Error chequeando tareas diferidas",{error:t})}}async checkSleepers(){let e=await this.repo.getWorkflowsToWake(50);e.length!==0&&await Promise.all(e.map(async r=>{let n=await this.repo.acquireLock(r);if(n)try{let s=await this.repo.get(r);if(s){let a=this.durableFns.get(s.name);a&&(this.logger.info("Despertando workflow",{workflowId:r}),await this.repo.updateStatus(r,c.RUNNING),await this.repo.incrementStep(r,n),this.scheduleExecution(r,a,void 0))}}finally{await this.repo.releaseLock(r,n)}}))}async reapDeadWorkers(){let t="0";do{let[e,r]=await l.sscan(k,t,"COUNT",100);t=e;for(let n of r){if(await l.exists(`${A}${n}`))continue;this.logger.warn(`Worker muerto ${n}. Recuperando tareas.`);let s=`${m}:processing:${n}`,a=await l.rpoplpush(s,m);for(;a;)a=await l.rpoplpush(s,m);await l.del(s),await l.srem(k,n)}}while(t!=="0")}startHeartbeat(){let t=`${A}${this.workerId}`,e=Math.max(Math.ceil(this.pollingInterval*3/1e3),5),r=()=>{this.isRunning&&l.set(t,Date.now().toString(),"EX",e).catch(()=>{})};this.heartbeatInterval=setInterval(r,this.pollingInterval),r()}startWorker(){if(this.isRunning)return;this.isRunning=!0;let t=`${m}:processing:${this.workerId}`;this.logger.info(`Worker ${this.workerId} iniciado`),this.startHeartbeat(),(async()=>{for(await l.sadd(k,this.workerId);this.isRunning;)try{let r=await b.brpoplpush(m,t,2);if(!r)continue;let n=E(r);this.logger.debug(`Ejecutando tarea: ${n.exportName}`,{workflowId:n.workflowId});try{let s;n.modulePath.startsWith("virtual:")?s=await import(n.modulePath):s=await import(Y(this.sourceRoot,n.modulePath));let a=s[n.exportName];if(typeof a!="function")throw new Error(`'${n.exportName}' no es una funci\xF3n.`);let o=await a(...n.args),p=this.durableFns.get(n.durableFunctionName);if(p){let d=await this.repo.acquireLock(n.workflowId);if(d)try{await this.repo.incrementStep(n.workflowId,d),this.scheduleExecution(n.workflowId,p,o)}finally{await this.repo.releaseLock(n.workflowId,d)}else this.logger.warn(`No se pudo adquirir lock para avanzar workflow ${n.workflowId} tras tarea`,{task:n.exportName})}await l.lrem(t,1,r)}catch(s){let a=s instanceof Error?s:new Error(String(s));this.logger.error(`Fallo en tarea ${n.exportName}`,{workflowId:n.workflowId,error:a.message});let o=this.durableFns.get(n.durableFunctionName),p=o?.retryOptions||{},d=p.maxAttempts??3,u=(n.attempts||0)+1;if(n.attempts=u,u<=d){let g=p.initialInterval?v(p.initialInterval):1e3,f=p.backoffCoefficient??2,w=p.maxInterval?v(p.maxInterval):36e5,T=g*Math.pow(f,u-1);T>w&&(T=w),this.logger.warn(`Reintentando tarea en ${v(T)} (intento ${u}/${d===1/0?"Inf":d})`,{workflowId:n.workflowId}),T>0?await this.repo.scheduleTaskRetry(n,T):await l.lpush(m,h(n)),await l.lrem(t,1,r)}else this.logger.error("Reintentos agotados. Moviendo a DLQ.",{workflowId:n.workflowId}),await this.repo.moveToDLQ(n,a),o?await this.handleFailure(n.workflowId,a,o,null):await this.repo.fail(n.workflowId,new Error(`Def missing for ${n.durableFunctionName}`),null),await l.lrem(t,1,r)}}catch(r){if(!this.isRunning)break;this.logger.error("Error infraestructura worker",{error:r}),await new Promise(n=>setTimeout(n,5e3))}})()}run(t){this.durableFns=t,this.startWorker(),this.startScheduler()}async stop(){this.isRunning=!1,this.schedulerInterval&&clearInterval(this.schedulerInterval),this.heartbeatInterval&&clearInterval(this.heartbeatInterval),await l.srem(k,this.workerId),this.logger.info("Runtime detenido")}};var z=i=>({...i,__isDurable:!0});var Q={info:(i,t)=>console.log(`[INFO] ${i}`,t||""),error:(i,t)=>console.error(`[ERROR] ${i}`,t||""),warn:(i,t)=>console.warn(`[WARN] ${i}`,t||""),debug:(i,t)=>console.debug(`[DEBUG] ${i}`,t||"")},j=i=>{if(!i.startsWith("on")||i.length<=2)return null;let t=i.slice(2);return t.charAt(0).toLowerCase()+t.slice(1)};function At(i){let t=i.logger||Q;t.info("--- Inicializando Sistema Durable ---"),C({commandClient:i.redisClient,blockingClient:i.blockingRedisClient});let e=new R({sourceRoot:i.sourceRoot,retention:i.retention,pollingInterval:i.pollingInterval,logger:t});e.run(i.durableFunctions);let r=new q(i.redisClient.options),n=new Map;r.psubscribe("event:*",a=>{a&&t.error("Error fatal al suscribirse a los canales de eventos:",{error:a})}),r.on("pmessage",(a,o,p)=>{let d=n.get(o);if(d&&d.length>0)try{let u=E(p),g={name:u.eventName||u.signalName,payload:u.payload};[...d].forEach(f=>f(g))}catch(u){t.error(`Error al parsear evento en ${o}`,{error:u})}});let s=(a,o)=>{let p=`event:${o}`,d=u=>(n.has(p)||n.set(p,[]),n.get(p)?.push(u),()=>{let g=n.get(p);if(g){let f=g.indexOf(u);f>-1&&g.splice(f,1),g.length===0&&n.delete(p)}});return{workflowId:o,signal:async(u,g)=>{await e.signal(o,u,g)},on:async(u,g)=>{let f=N(o),w=await i.redisClient.hgetall(f);return u==="workflow:completed"&&w.status===c.COMPLETED?(g(E(w.result||"null")),{unsubscribe:()=>{}}):u==="workflow:failed"&&w.status===c.FAILED?(g({message:w.error||"Unknown"}),{unsubscribe:()=>{}}):{unsubscribe:d(O=>{O.name===u&&g(O.payload)})}},subscribe:async u=>({unsubscribe:d(f=>{u(f)})})}};return{start:async(a,o)=>{let p=o.workflowId||B(),d=[],u=`event:${p}`,g={};if(Object.keys(o).forEach(f=>{let w=j(f);w&&typeof o[f]=="function"&&(g[w]=o[f])}),Object.keys(g).length>0){n.has(u)||n.set(u,[]);let f=w=>{let T=g[w.name];T&&T(w.payload)};n.get(u)?.push(f),d.push(()=>{let w=n.get(u);if(w){let T=w.indexOf(f);T>-1&&w.splice(T,1)}})}return await e.start(a,{workflowId:p,input:o.input,tags:o.tags}),{workflowId:p,unsubscribe:async()=>{d.forEach(f=>f())}}},stop:()=>{e.stop(),r.quit().catch(()=>{})},runtime:e,cancel:(a,o)=>e.cancel(a,o),getState:a=>e.getState(a),getHandle:(a,o)=>s(a,o),cancelByTag:(a,o)=>e.cancelByTag(a,o)}}export{S as WorkflowCancellationError,z as bDurable,At as bDurableInitialize};
|
package/package.json
CHANGED