npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tuain/bpm-engine

v2.7.0

Published

Motor de gestión de procesos de negocio (BPM) de la plataforma Tuain. Software propietario.

Readme

@tuain/bpm-engine

Motor de gestión de procesos de negocio (BPM) de la plataforma Tuain.

SOFTWARE PROPIETARIO, DE DISTRIBUCIÓN ABIERTA. Copyright (c) 2022-2026 Imix Latam / TUAIN. Todos los derechos reservados. Desde la 2.4.0 el paquete se publica con acceso público y se instala y ejecuta sin token de ningún tipo, pero eso es una decisión de distribución, no un cambio de licencia: no es software libre ni de código abierto. Su uso sigue sujeto al archivo LICENSE — licencia de uso no exclusiva, no transferible y revocable, sin derecho a modificar, redistribuir, sublicenciar, descompilar ni aplicar ingeniería inversa.

Qué es

Motor de ejecución de procesos definidos como tareas, gateways y eventos, con persistencia en MongoDB/DocumentDB. Se compone de dos piezas:

  • ProcessManager — administra las definiciones (catálogo de procesos, tareas, gateways, eventos, variables y roles).
  • ProcessController — ejecuta instancias de un proceso. Se extiende en el servicio consumidor, que registra el comportamiento del flujo en customizeBehaviour().

El paquete se distribuye como un único bundle CommonJS minificado (dist/index.js) con sus definiciones de tipos (dist/index.d.ts). El código fuente no se publica.

Instalación

npm install @tuain/bpm-engine

Desde la 2.4.0 no hace falta nada más: el paquete se publica con acceso público, así que no se necesita .npmrc ni token de lectura. Si tu repo trae un .npmrc con //registry.npmjs.org/:_authToken puesto solo para este paquete, ya puedes quitarlo.

Las versiones 2.3.x y anteriores sí requerían token de lectura del scope @tuain.

Licencia de ejecución

Desde la 2.4.0 el motor no exige licencia: la verificación viene inerte y el arranque no requiere TUAIN_BPM_LICENSE. Si la variable está presente se ignora, así que un despliegue que la traiga de la 2.3.x no falla — puedes retirarla cuando quieras.

El motor sabe verificar una licencia Ed25519 firmada por el titular, presentada en TUAIN_BPM_LICENSE o como opción del constructor:

new ProcessManager(ecosystem, getDb, getDb, logger, errMgr, { license: miLicencia });

La verificación es offline: no hay llamadas a servicios del titular. manager.license expone el veredicto — en esta versión, siempre notConfigured. El titular puede reactivarlo en una versión posterior; si eso ocurre se anunciará como cambio de comportamiento en el CHANGELOG, no de forma silenciosa.

Uso

const { ProcessManager, ProcessController } = require('@tuain/bpm-engine');

const manager = new ProcessManager(ecosystem, getDb, getDb, logger, errMgr);

class MiProceso extends ProcessController {
  customizeBehaviour() {
    this.onTaskStart('miTarea', this.iniciarMiTarea.bind(this));
  }
  async iniciarMiTarea({ procId, id }) {
    await this.setVars(procId, { algo: true });
    this.completeTask(id);
  }
}

const proceso = new MiProceso('miProceso', manager, definicion);

Toda operación retorna la tupla [error, valor].

Arranque

El constructor no puede esperar la carga de la definición, así que la inicialización se expone en ready y conviene esperarla antes de empezar a atender tráfico:

const [initErr] = await proceso.ready; // nunca rechaza: informa por la tupla

Los puntos de entrada (newInstance, completeTask, triggerEvent) la esperan por su cuenta, de modo que una petición temprana espera en lugar de encontrarse sin definición ni callbacks registrados. Si la inicialización falló, esas operaciones retornan un error explícito de controlador no inicializado. proceso.initialized expone el estado.

Operación con múltiples instancias (desde 2.2.0)

El motor no tiene bucle de ejecución propio: la cascada de nodos avanza por recursión sobre el event loop de la instancia que atendió la entrada (newInstance, completeTask o el disparo de un temporizador). Varias instancias del mismo servicio pueden convivir sobre la misma base de datos, con dos condiciones:

Temporizadores. Cada instancia agenda todos los timers programados, así que la exclusión se resuelve al disparar: triggerTimerEvent reclama el timer con un compare-and-swap de scheduled a executed sobre la colección timers y solo la ganadora ejecuta los callbacks y avanza el flujo. No requiere índices nuevos. La colección TimerLocks quedó sin uso (su índice nunca fue unique, por lo que no excluía nada) y puede eliminarse.

Los timers reclamados registran claimedBy (instancia), execution (fecha), attempts (reclamos consumidos), leaseExpiresAt (vigencia del reclamo) y flowAdvanced.

Recuperación de temporizadores estancados (desde 2.5.0). El reclamo es exclusivo pero ya no es definitivo. Si la instancia que lo gana no alcanza a completar el avance —la mataron a mitad del despliegue, un callback del consumidor reventó o quedó colgado, la base de datos falló entre una escritura y la siguiente— el temporizador no queda detenido para siempre:

  • Cuando el motor ve la falla (excepción o error de goToNode), devuelve el temporizador a scheduled con la fecha de despertar corrida por timerRetryBackoff (1 minuto) y lo reagenda de inmediato. El documento queda con lastFailure.
  • Cuando no la ve —la instancia dejó de existir—, la barrida recoverStalledTimers() lo devuelve a scheduled al vencer la concesión (timerLeaseTimeout, 10 minutos). Corre en cada ciclo de mantenimiento, junto al refresco, y recupera hasta timerRecoveryBatch (50) por pasada.
  • Los callbacks del evento no se repiten en el reintento: el temporizador queda con callbacksDone: true y el segundo reclamo solo reintenta el avance del flujo.
  • Tras timerMaxAttempts (5) reclamos fallidos el temporizador queda en status: 'failed' con el motivo en failure — una falla determinista no se reintenta para siempre. Es el estado a vigilar: significa que ese proceso necesita intervención manual.

El motor crea por su cuenta el índice stalledTimersRecovery (status, flowAdvanced, leaseExpiresAt) sobre timers la primera vez que barre. Los temporizadores executed de versiones anteriores, que no escribían flowAdvanced, quedan fuera de la barrida y no se re-disparan.

Un controlador por proceso (desde 2.6.0). El refresco y la barrida de temporizadores están acotados por ecosystem y processName. Antes no lo estaban: como un servicio instancia un controlador por proceso sobre las mismas colecciones, cada controlador agendaba y reclamaba los temporizadores de todos los demás, y el que ganaba el reclamo no tenía ese evento en su definición — el temporizador quedaba consumido y el controlador dueño no lo disparaba nunca. Si tu servicio hospeda más de un proceso BPM, esto afectaba a todos.

Recuperación del avance de tareas y gateways (desde 2.6.0). Cerrar un nodo y crear el siguiente son operaciones separadas y sin transacción. El cierre es atómico y excluyente, pero de ahí al nodo siguiente había un tramo sin red: la tarea quedaba complete sin sucesor, o el join complete sin destino disparado, y nada volvía a mirarlos.

Ahora el cierre escribe, en su misma operación atómica, un marcador de avance pendiente (flowAdvanced: false, advancedBy, advanceLease, advanceAttempts), y recoverStalledNodes() retoma lo que quede vencido — en cada ciclo de mantenimiento y también al arrancar el controlador, porque un reinicio es justo lo que deja nodos a medias:

  • No se rehace el cierre. Una tarea completada no se reabre ni repite sus callbacks: la recuperación solo crea el nodo siguiente. Es la diferencia con los temporizadores, que sí se re-encolan completos.
  • Los gateways registran el progreso destino por destino (destinations.<nombre>.advanced), así que un avance a medias se retoma sin repetir los destinos ya creados.
  • Tras advanceMaxAttempts (5) intentos el nodo queda con advanceFailed: true y el motivo en advanceFailure. Es el otro estado a vigilar, junto con los timers en failed. status no se toca, para no alterar las consultas de historial del consumidor.
  • El gateway estándar ahora se cierra después de disparar sus destinos y en la misma escritura que su marcador; antes el cierre iba en paralelo con ellos y podía quedar complete con destinos sin crear.

Índice stalledNodeAdvanceRecovery (ecosystem, procName, flowAdvanced, advanceLease) sobre tasks y gateways, creado por el motor. Los nodos cerrados por versiones anteriores no tienen marcador y quedan fuera de la barrida.

Consultas de vigilancia:

db.tasks.countDocuments({ flowAdvanced: false, advanceFailed: { $exists: false } }); // avances pendientes
db.tasks.countDocuments({ advanceFailed: true }); // agotados: requieren intervención
db.gateways.countDocuments({ advanceFailed: true });
db.timers.countDocuments({ status: 'failed' });

Instancias de tarea. Dos ramas que llegan al mismo nodo a la vez convergen en una sola instancia activa: el upsert va respaldado por un índice único sparse sobre activeKey (uniqueActiveTaskInstance, creado por el propio motor la primera vez que instancia una tarea). La clave se libera al completar, cerrar o cancelar la tarea, de modo que un bucle pueda reentrar al mismo nodo en el siguiente ciclo. Las instancias creadas por versiones anteriores no tienen la clave y se siguen reutilizando con normalidad.

El callback de inicio corre una sola vez por instancia (desde 2.7.0). onTaskStart corría en cada llegada, también cuando la rama convergía sobre una instancia ya existente: en un join o un fork se ejecutaba una vez por rama, duplicando los efectos externos que hubiera dentro. Ahora solo corre cuando la instancia se creó de verdad. Si tu callback dependía de correr por cada llegada, hay que moverlo a onTaskComplete o a la lógica del gateway.

Instancias de gateway (desde 2.7.0). Los gateways que no son join —estándar y fork— también se crean con exclusión mutua, con la clave formada por el nodo y la llegada que lo produjo (origin). Así un avance re-disparado —lo que hace la recuperación al retomar el nodo anterior— converge sobre la instancia existente en lugar de insertar una segunda y duplicar la rama completa aguas abajo, mientras que una llegada distinta, o el mismo nodo alcanzado de nuevo en un ciclo, sigue creando su propia instancia. La clave se libera al cerrar el gateway. Un efecto colateral visible: los gateways fork ahora quedan en complete al terminar de disparar sus destinos; antes se quedaban active para siempre y no aparecían en getProcessGatewaysHistory.

Finalización de tareas. completeTask reclama la tarea (activecomplete) antes de ejecutar los callbacks y avanzar el flujo, en una sola operación atómica. Así un doble envío desde la UI, un reintento del cliente ante un timeout o la misma petición atendida por dos instancias no duplican la cascada: quien pierde el reclamo retorna [null, { alreadyCompleted: true }], sin efectos. La tarea queda con completedBy para saber qué instancia la completó. No requiere índices nuevos.

Cierre ordenado. Ante la señal de terminación hay que invocar await controller.shutdown() por cada controlador: cancela el refresco y los temporizadores agendados en memoria, y espera a que terminen las cascadas ya desprendidas del flujo (plazo shutdownDrainTimeout, 15 s por defecto). Sin esa llamada, un despliegue normal mata cascadas a medio camino y deja procesos parqueados en el nodo donde iban. El plazo debe quedar por debajo del periodo de gracia del orquestador (terminationGracePeriodSeconds en Kubernetes).

controller.pendingWork() expone cuántas operaciones desprendidas hay en vuelo; sirve como métrica de drenaje y para readiness probes.

Desarrollo

npm test                # mocha
npx eslint lib config test
npx prettier --check lib config test
npm run copyright       # refresca los encabezados de copyright en los fuentes
npm run build           # genera dist/index.js + dist/index.d.ts

npm publish ejecuta build automáticamente (prepublishOnly) y publica con acceso público.

Contacto

Imix Latam / TUAIN — titular de los derechos. Para autorizaciones de uso, contactar al titular.