@bobtail.software/b-durable 1.0.7 → 1.0.9
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/compiler/cli.mjs +48 -38
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +8 -8
- package/package.json +1 -1
package/dist/compiler/cli.mjs
CHANGED
|
@@ -1,64 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
Procesando archivo: ${
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import q from"path";import{createHash as X}from"crypto";import{existsSync as K,mkdirSync as Y,readFileSync as Q,rmSync as Z,writeFileSync as ee}from"fs";import N from"path";import*as j from"prettier";import{Node as a,Project as te,SyntaxKind as P,ts as U,VariableDeclarationKind as _}from"ts-morph";var ne="bDurable",L=U.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope|U.TypeFormatFlags.NoTruncation;function se(e){return X("sha256").update(e).digest("hex")}async function re(e){let t=e.getFilePath(),o=e.getFullText(),n=await j.resolveConfig(t),i=await j.format(o,{...n,parser:"typescript"});e.replaceWithText(i)}async function H(e){console.log("Iniciando compilador de workflows duraderos...");let{inputDir:t,outputDir:o,packageName:n,mode:i}=e,c=N.resolve(process.cwd(),"durable.lock.json"),l={};if(K(c))try{l=JSON.parse(Q(c,"utf-8"))}catch(p){console.warn("Advertencia: durable.lock.json corrupto, iniciando uno nuevo.",p)}let r=new te({tsConfigFilePath:N.resolve(process.cwd(),"tsconfig.json")}),g=r.addSourceFilesAtPaths(`${t}/**/*.ts`);K(o)&&(console.log(`Limpiando directorio de salida: ${o}`),Z(o,{recursive:!0,force:!0})),Y(o,{recursive:!0});let d=r.createDirectory(o);console.log(`Encontrados ${g.length} archivos de workflow para procesar.`);let s=[],S=[],m=!1;for(let p of g){console.log(`
|
|
3
|
+
Procesando archivo: ${p.getBaseName()}`);let u=p.getDescendantsOfKind(P.CallExpression).filter(y=>y.getExpression().getText()===ne);if(u.length!==0)for(let y of u){let f=y.getParentIfKind(P.VariableDeclaration);if(!f)continue;let b=f.getName();console.log(` -> Transformando workflow: ${b}`);let[v]=y.getArguments();if(!a.isObjectLiteralExpression(v))continue;let I=v.getProperty("workflow");if(!I||!a.isPropertyAssignment(I))continue;let $=I.getInitializer();if(!$||!a.isArrowFunction($))continue;let w=v.getProperty("version"),h="unknown";if(w&&a.isPropertyAssignment(w)){let R=w.getInitializer();R&&a.isStringLiteral(R)&&(h=R.getLiteralValue())}let x=p.getBaseName().replace(/\.ts$/,".compiled.mts"),E=N.join(d.getPath(),x),C=r.createSourceFile(E,"",{overwrite:!0});S.push(C),oe(b,$,y,C,n);let M=C.getFullText(),F=se(M),T=l[b]?.[h];if(T)if(T!==F){if(i==="prod")throw new Error(`
|
|
4
|
+
\u{1F6D1} ERROR DE INTEGRIDAD (PROD) \u{1F6D1}
|
|
5
|
+
El workflow '${b}' (v${h}) ha cambiado, pero el lockfile no se actualiz\xF3.
|
|
6
|
+
Hash esperado: ${T.substring(0,8)}...
|
|
7
|
+
Hash actual: ${F.substring(0,8)}...
|
|
8
|
+
|
|
9
|
+
Soluci\xF3n:
|
|
10
|
+
1. Incrementa la versi\xF3n en tu c\xF3digo (ej: v${h} -> v${parseFloat(h)+.1}).
|
|
11
|
+
2. O ejecuta en modo dev para actualizar el lockfile: 'b-durable-compiler dev ...'
|
|
12
|
+
`);console.log(` \u26A0\uFE0F v${h} modificada. Actualizando hash en lockfile.`),l[b]||(l[b]={}),l[b][h]=F,m=!0}else console.log(` \u2705 v${h} verificada.`);else console.log(` \u{1F195} v${h} registrada.`),l[b]||(l[b]={}),l[b][h]=F,m=!0;console.log(` -> Archivo generado: ${N.relative(process.cwd(),E)}`);let D=x;s.push({name:b,importPath:`./${D}`})}}if(m){if(i==="prod")throw new Error("El lockfile ha cambiado durante un build de PROD. Esto no est\xE1 permitido.");ee(c,JSON.stringify(l,null,2)),console.log(`
|
|
13
|
+
\u{1F512} durable.lock.json actualizado.`)}if(s.length>0){let p=N.join(d.getPath(),"index.mts"),u=r.createSourceFile(p,"",{overwrite:!0});S.push(u),u.addStatements(`// Este archivo fue generado autom\xE1ticamente. NO EDITAR MANUALMENTE.
|
|
14
|
+
`),u.addImportDeclaration({isTypeOnly:!0,moduleSpecifier:n,namedImports:["DurableFunction"]});for(let f of s)u.addImportDeclaration({moduleSpecifier:f.importPath,namedImports:[f.name]});u.addExportDeclaration({namedExports:s.map(f=>f.name)}),u.addStatements(`
|
|
15
|
+
`),u.addVariableStatement({declarationKind:_.Const,declarations:[{name:"durableFunctions",type:"Map<string, DurableFunction<any, any, any, any>>",initializer:"new Map()"}]});let y=s.map(f=>`durableFunctions.set(${f.name}.name, ${f.name});`);u.addStatements(y),u.addStatements(`
|
|
16
|
+
`),u.addExportAssignment({isExportEquals:!1,expression:"durableFunctions"}),console.log(`
|
|
17
|
+
-> Archivo de \xEDndice generado: ${N.basename(p)}`)}console.log(`
|
|
18
|
+
Formateando archivos generados con Prettier...`);for(let p of S)await re(p);await r.save(),console.log(`
|
|
19
|
+
Compilaci\xF3n completada exitosamente.`)}function oe(e,t,o,n,i){let c=t.getBody();if(!a.isBlock(c))throw new Error(`El cuerpo del workflow '${e}' debe ser un bloque {}.`);let[l]=o.getArguments();if(!a.isObjectLiteralExpression(l))throw new Error("El argumento de bDurable debe ser un objeto.");let r=l.getProperty("version");if(!r||!a.isPropertyAssignment(r))throw new Error(`El workflow '${e}' debe tener una propiedad 'version'.`);let g=r.getInitializer();if(!g||!a.isStringLiteral(g))throw new Error(`La versi\xF3n del workflow '${e}' debe ser un string literal.`);let d=l.getProperty("retryOptions"),s="undefined";d&&a.isPropertyAssignment(d)&&(s=d.getInitializer()?.getText()||"undefined");let{clauses:S}=A(c.getStatements(),{step:0,persistedVariables:new Map}),m=t.getReturnType();m.getSymbol()?.getName()==="Promise"&&m.isObject()&&(m=m.getTypeArguments()[0]||m);let p=m.getText(void 0,L),u=new Set,y=t.getSourceFile(),f=o.getTypeArguments(),b=f.length>0?f[0].getText():"unknown",v=f.length>2?f[2].getText():"Record<string, never>",I=f.length>3?f[3].getText():"Record<string, never>";y.getImportDeclarations().forEach(x=>{if(x.getModuleSpecifierValue()===i)return;let E=x.getModuleSpecifierValue();if(E.includes(".workflow")){let T=N.parse(E),D=N.join(T.dir,T.base+".compiled.mts");!D.startsWith(".")&&!N.isAbsolute(D)&&(D="./"+D),E=D.replace(/\\/g,"/")}else E.startsWith(".")&&N.extname(E)===""&&(E+=".mjs");let C=[],M=[];x.getNamedImports().forEach(T=>{let D=T.getName(),R=T.getAliasNode()?.getText(),W=R?`${D} as ${R}`:D,z=(T.getNameNode().getSymbol()?.getAliasedSymbol()??T.getNameNode().getSymbol())?.getDeclarations()??[],J=z.some(B=>a.isEnumDeclaration(B));T.isTypeOnly()||!J&&z.every(B=>a.isInterfaceDeclaration(B)||a.isTypeAliasDeclaration(B))?M.push(W):C.push(W)}),C.length>0&&n.addImportDeclaration({moduleSpecifier:E,namedImports:C}),M.length>0&&n.addImportDeclaration({isTypeOnly:!0,moduleSpecifier:E,namedImports:M});let F=x.getDefaultImport();F&&n.addImportDeclaration({moduleSpecifier:E,defaultImport:F.getText()})}),y.getInterfaces().forEach(x=>{u.add(x.getText().startsWith("export")?x.getText():`export ${x.getText()}`)}),y.getTypeAliases().forEach(x=>{u.add(x.getText().startsWith("export")?x.getText():`export ${x.getText()}`)});let[$]=t.getParameters(),w="";if($){let x=$.getNameNode().getText();x!=="input"&&(w=`const ${x} = input;`)}n.addImportDeclaration({isTypeOnly:!0,moduleSpecifier:i,namedImports:["DurableFunction","WorkflowContext","Instruction"]}),u.size>0&&(n.addStatements(`
|
|
20
|
+
`),n.addStatements(Array.from(u))),n.addStatements(`
|
|
11
21
|
// Este archivo fue generado autom\xE1ticamente. NO EDITAR MANUALMENTE.
|
|
12
|
-
`);let
|
|
22
|
+
`);let h=g.getLiteralValue(),V=`{
|
|
13
23
|
__isDurable: true,
|
|
14
24
|
name: '${e}',
|
|
15
|
-
version: '${
|
|
16
|
-
retryOptions: ${
|
|
17
|
-
async execute(context: WorkflowContext<${
|
|
25
|
+
version: '${h}',
|
|
26
|
+
retryOptions: ${s},
|
|
27
|
+
async execute(context: WorkflowContext<${b}>): Promise<Instruction<${p}>> {
|
|
18
28
|
const { input, state, result, log, workflowId } = context;
|
|
19
|
-
${
|
|
29
|
+
${w}
|
|
20
30
|
while (true) {
|
|
21
31
|
switch (context.step) {
|
|
22
|
-
${
|
|
32
|
+
${S.join(`
|
|
23
33
|
`)}
|
|
24
34
|
default:
|
|
25
35
|
throw new Error(\`Paso desconocido: \${context.step}\`);
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
|
-
}`;
|
|
30
|
-
${
|
|
39
|
+
}`;n.addVariableStatement({isExported:!0,declarationKind:_.Const,declarations:[{name:e,type:`DurableFunction<${b}, ${p}, ${v}, ${I}>`,initializer:V}]}),n.organizeImports()}function A(e,t){if(e.length===0){let u=[];if(t.pendingStateAssignment){let y=`case ${t.step}: {
|
|
40
|
+
${t.pendingStateAssignment}
|
|
31
41
|
return { type: 'COMPLETE', result: undefined };
|
|
32
|
-
}`;
|
|
33
|
-
`),
|
|
34
|
-
return { type: 'COMPLETE', result: undefined };`;return{clauses:[`case ${
|
|
35
|
-
${
|
|
36
|
-
}`],nextStep:
|
|
37
|
-
`),
|
|
38
|
-
${
|
|
39
|
-
}`,
|
|
40
|
-
`),
|
|
42
|
+
}`;u.push(y)}return{clauses:u,nextStep:t.step+1}}let{syncBlock:o,durableStatement:n,nextStatements:i}=de(e),{rewrittenSyncStatements:c,newlyPersistedVariables:l}=ce(o,n?[n,...i]:[],t.persistedVariables);t.pendingStateAssignment&&c.unshift(t.pendingStateAssignment);let r=new Map([...t.persistedVariables,...l]);if(!n){let u=c.join(`
|
|
43
|
+
`),f=o.length>0&&a.isReturnStatement(o[o.length-1])?"":`
|
|
44
|
+
return { type: 'COMPLETE', result: undefined };`;return{clauses:[`case ${t.step}: {
|
|
45
|
+
${u}${f}
|
|
46
|
+
}`],nextStep:t.step+1}}if(a.isIfStatement(n))return ae(n,i,{...t,persistedVariables:r},c);if(a.isTryStatement(n))return ie(n,i,{...t,persistedVariables:r},c);let{instruction:g,nextPendingStateAssignment:d}=pe(n,r);c.push(g);let s=c.join(`
|
|
47
|
+
`),S=`case ${t.step}: {
|
|
48
|
+
${s}
|
|
49
|
+
}`,m={step:t.step+1,persistedVariables:r,pendingStateAssignment:d},p=A(i,m);return{clauses:[S,...p.clauses],nextStep:p.nextStep}}function ae(e,t,o,n){let i=k(e.getExpression(),o.persistedVariables),c=e.getThenStatement(),l=a.isBlock(c)?c.getStatements():[c],r=A(l,{step:o.step+1,persistedVariables:new Map(o.persistedVariables)}),g,d=e.getElseStatement();if(d){let y=a.isBlock(d)?d.getStatements():[d];g=A(y,{step:r.nextStep,persistedVariables:new Map(o.persistedVariables)})}let s=g?g.nextStep:r.nextStep,S=A(t,{step:s,persistedVariables:o.persistedVariables}),m=n.join(`
|
|
50
|
+
`),p=r.nextStep;return{clauses:[`
|
|
41
51
|
case ${o.step}: {
|
|
42
|
-
${
|
|
43
|
-
if (${
|
|
52
|
+
${m}
|
|
53
|
+
if (${i}) {
|
|
44
54
|
context.step = ${o.step+1};
|
|
45
55
|
} else {
|
|
46
|
-
${
|
|
56
|
+
${d?`context.step = ${p};`:`context.step = ${s};`}
|
|
47
57
|
}
|
|
48
58
|
break;
|
|
49
59
|
}
|
|
50
|
-
`,...r.clauses,...
|
|
51
|
-
case ${
|
|
52
|
-
${
|
|
60
|
+
`,...r.clauses,...g?g.clauses:[],...S.clauses],nextStep:S.nextStep}}function ie(e,t,o,n){let{step:i,persistedVariables:c}=o,l=e.getTryBlock(),r=e.getCatchClause(),g=e.getFinallyBlock(),d=A(l.getStatements(),{step:i+1,persistedVariables:new Map(c)}),s,S,m=d.nextStep;if(r){let w=r.getBlock(),h=r.getVariableDeclaration();h&&(S=h.getName()),s=A(w.getStatements(),{step:m,persistedVariables:new Map(c)})}let p,u=s?s.nextStep:m;g&&(p=A(g.getStatements(),{step:u,persistedVariables:new Map(c)}));let y=p?p.nextStep:u,f=A(t,{step:y,persistedVariables:c}),b=`{ catchStep: ${r?m:"undefined"}, finallyStep: ${g?u:"undefined"} }`,v=`
|
|
61
|
+
case ${i}: {
|
|
62
|
+
${n.join(`
|
|
53
63
|
`)}
|
|
54
64
|
state.tryCatchStack = state.tryCatchStack || [];
|
|
55
|
-
state.tryCatchStack.push(${
|
|
56
|
-
context.step = ${
|
|
65
|
+
state.tryCatchStack.push(${b});
|
|
66
|
+
context.step = ${i+1}; // Salta al inicio del bloque try
|
|
57
67
|
break;
|
|
58
68
|
}
|
|
59
|
-
`,I=
|
|
60
|
-
const ${
|
|
61
|
-
Please split them into separate lines/variables to ensure safe state persistence.`);let
|
|
69
|
+
`,I=d.clauses.pop()||"",$=g?u:y;if(d.clauses.push(I.replace(/return { type: 'COMPLETE'.* };/,`context.step = ${$}; break;`)),s){if(S){let h=s.clauses[0]||`case ${m}: {}`;s.clauses[0]=h.replace("{",`{
|
|
70
|
+
const ${S} = result as unknown;`)}let w=s.clauses.pop()||"";s.clauses.push(w.replace(/return { type: 'COMPLETE'.* };/,`context.step = ${$}; break;`))}if(p){let w=p.clauses.pop()||"";p.clauses.push(w.replace(/return { type: 'COMPLETE'.* };/,`state.tryCatchStack?.pop(); context.step = ${y}; break;`))}return{clauses:[v,...d.clauses,...s?s.clauses:[],...p?p.clauses:[],...f.clauses],nextStep:f.nextStep}}function ce(e,t,o){let n=[],i=new Map,c=ue(t),l=new Map(o);for(let r of e){if(a.isVariableStatement(r))for(let d of r.getDeclarations()){let s=d.getInitializer();if(!s)continue;let S=le(d),m=S.filter(p=>c.has(p.name));if(m.length>0){let p=k(s,o);for(let{name:u,type:y}of m){i.set(u,{type:y}),l.set(u,{type:y});let f=S.length>1?`${p}.${u}`:p;n.push(`state.${u} = ${f};`)}}}n.push(k(r,l))}return{rewrittenSyncStatements:n,newlyPersistedVariables:i}}function le(e){let t=e.getNameNode(),o=[];if(a.isIdentifier(t)){let n=e.getType().getText(e,L);o.push({name:t.getText(),type:n})}else if(a.isObjectBindingPattern(t))for(let n of t.getElements()){let i=n.getName(),c=n.getType().getText(n,L);o.push({name:i,type:c})}return o}function pe(e,t){if(a.isReturnStatement(e))return{instruction:`return { type: 'COMPLETE', result: ${e.getExpression()?k(e.getExpressionOrThrow(),t):"undefined"} };`,nextPendingStateAssignment:void 0};if(e.getDescendantsOfKind(P.AwaitExpression).length>1)throw new Error(`[b-durable Compiler Error] Multiple 'await' expressions found in a single statement at line ${e.getStartLineNumber()}.
|
|
71
|
+
Please split them into separate lines/variables to ensure safe state persistence.`);let n,i=e.getFirstDescendantByKind(P.VariableDeclaration);if(i){let l=i.getName(),r=i.getType().getText(i,L);t.set(l,{type:r}),n=`state.${l} = result;`}let c=e.getFirstDescendantByKind(P.AwaitExpression);if(c){let l=c.getParent();if(!(a.isExpressionStatement(l)||a.isVariableDeclaration(l)||a.isCallExpression(l)&&a.isExpressionStatement(l.getParent()))&&i&&i.getInitializer()!==c)throw new Error(`[b-durable Compiler Error] Complex 'await' usage detected at line ${e.getStartLineNumber()}.
|
|
62
72
|
The 'await' keyword must be the direct value of the assignment.
|
|
63
73
|
Invalid: const x = 1 + await foo();
|
|
64
|
-
Valid: const temp = await foo(); const x = 1 + temp;`);let
|
|
74
|
+
Valid: const temp = await foo(); const x = 1 + temp;`);let g=c.getExpression();if(a.isCallExpression(g))return{instruction:`return ${ge(g,t)};`,nextPendingStateAssignment:n}}return{instruction:k(e,t),nextPendingStateAssignment:n}}function ue(e){let t=new Set;for(let o of e)o.getDescendantsOfKind(P.Identifier).forEach(n=>{t.add(n.getText())});return t}function k(e,t){let o=e.getText(),n=e.getStart(),i=[],c=e.getDescendantsOfKind(P.Identifier);a.isIdentifier(e)&&c.push(e),c.forEach(r=>{let g=r.getText();if(!t.has(g))return;let d=r.getSymbol();if(d&&d.getDeclarations().some(x=>x.getStart()>=e.getStart()&&x.getEnd()<=e.getEnd()))return;let s=r.getParent(),S=a.isVariableDeclaration(s)&&s.getNameNode()===r,m=a.isPropertyAccessExpression(s)&&s.getNameNode()===r||a.isPropertyAssignment(s)&&s.getNameNode()===r,p=a.isBindingElement(s)&&s.getNameNode()===r,u=a.isShorthandPropertyAssignment(s)&&s.getNameNode()===r,y=a.isParameterDeclaration(s)&&s.getNameNode()===r,f=a.isFunctionDeclaration(s)&&s.getNameNode()===r,b=a.isClassDeclaration(s)&&s.getNameNode()===r,v=a.isInterfaceDeclaration(s)&&s.getNameNode()===r,I=a.isTypeAliasDeclaration(s)&&s.getNameNode()===r,$=a.isEnumDeclaration(s)&&s.getNameNode()===r,w=a.isMethodDeclaration(s)&&s.getNameNode()===r;if(!S&&!m&&!p&&!y&&!f&&!b&&!v&&!I&&!$&&!w){let h=t.get(g),V=r.getStart()-n,x=r.getEnd()-n,E=`(state.${g} as ${h.type})`;u&&(E=`${g}: ${E}`),i.push({start:V,end:x,text:E})}}),i.sort((r,g)=>g.start-r.start);let l=o;for(let{start:r,end:g,text:d}of i)l=l.substring(0,r)+d+l.substring(g);return l}function ge(e,t){let o=e.getExpression(),n,i=!1;a.isPropertyAccessExpression(o)?(o.getExpression().getText()==="context"&&(i=!0),n=o.getName()):n=o.getText();let c=e.getArguments().map(s=>k(s,t)).join(", ");if(i)switch(n){case"bSleep":return`{ type: 'SCHEDULE_SLEEP', duration: ${c} }`;case"bWaitForEvent":return`{ type: 'WAIT_FOR_SIGNAL', signalName: ${c} }`;case"bExecute":{let[s,S]=e.getArguments(),m=s.getText(),p=S?k(S,t):"undefined";return`{ type: 'EXECUTE_SUBWORKFLOW', workflowName: ${m}.name, input: ${p} }`}case"bSignal":{let[s,S]=e.getArguments().map(m=>k(m,t));return`{ type: 'EMIT_EVENT', eventName: ${s}, payload: ${S} }`}default:throw new Error(`Funci\xF3n de contexto durable desconocida: '${n}'.`)}let l=o.getSymbol();if(!l)throw new Error(`S\xEDmbolo no encontrado para '${n}'.`);let r=l.getDeclarations()[0]?.asKind(P.ImportSpecifier);if(!r)throw new Error(`'${n}' debe ser importada.`);let g=r.getImportDeclaration().getModuleSpecifierSourceFileOrThrow();return`{ type: 'SCHEDULE_TASK', modulePath: '${N.relative(process.cwd(),g.getFilePath()).replace(/\\/g,"/")}', exportName: '${n}', args: [${c}] }`}function O(e){for(let t of e.getDescendantsOfKind(P.AwaitExpression)){let o=t.getExpressionIfKind(P.CallExpression);if(o){let n=o.getExpression();if(a.isPropertyAccessExpression(n)){let i=n.getName();if(n.getExpression().getText()==="context"&&(i==="bSleep"||i==="bWaitForEvent"||i==="bExecute"||i==="bSignal")||n.getSymbol()?.getDeclarations()[0]?.isKind(P.ImportSpecifier))return!0}else if(n.getSymbol()?.getDeclarations()[0]?.isKind(P.ImportSpecifier))return!0}}if(a.isTryStatement(e)&&(O(e.getTryBlock())||e.getCatchClause()&&O(e.getCatchClause().getBlock())||e.getFinallyBlock()&&O(e.getFinallyBlock())))return!0;if(a.isIfStatement(e)){let t=O(e.getThenStatement()),o=e.getElseStatement()?O(e.getElseStatement()):!1;return t||o}return a.isBlock(e)?e.getStatements().some(O):!1}function de(e){for(let t=0;t<e.length;t++){let o=e[t];if(a.isReturnStatement(o)||O(o)||a.isTryStatement(o))return{syncBlock:e.slice(0,t),durableStatement:o,nextStatements:e.slice(t+1)}}return{syncBlock:e,durableStatement:null,nextStatements:[]}}var G=e=>{let t=process.argv.indexOf(e);if(t!==-1&&process.argv.length>t+1)return process.argv[t+1]};async function me(){let e=G("--in"),t=G("--out"),n=process.argv.slice(2).includes("dev")?"dev":"prod";(!e||!t)&&(console.error("Uso: b-durable-compiler <dev|prod> --in <src> --out <generated>"),process.exit(1));let i=q.resolve(process.cwd(),e),c=q.resolve(process.cwd(),t);await H({inputDir:i,outputDir:c,packageName:"@bobtail.software/b-durable",mode:n})}me().catch(e=>{console.error("Error durante la compilaci\xF3n:",e),process.exit(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