El Engine es un módulo Go distinto de los servicios de Fetcher. Su ruta de módulo es
github.com/LerianStudio/fetcher/pkg/engine, y la de los servicios es github.com/LerianStudio/fetcher/v2. El Engine tiene su propia línea de versiones, con tags prefijados por ruta (pkg/engine/vX.Y.Z). Importar el Engine no arrastra ninguna dependencia de los servicios.Fetcher es source-available bajo la Elastic License 2.0, y el Engine lleva la misma licencia. Puedes leer las reglas de extracción que embebes.El modelo de tres capas
El principio rector: el Engine es dueño de lo que hace que Fetcher sea Fetcher, y las aplicaciones anfitrionas son dueñas de cómo corre Fetcher.
Por eso, cada producto que embebe el Engine comparte un único dueño canónico del comportamiento de datasources y de extracción. Un cambio de regla en el núcleo llega a todos los hosts en el siguiente salto de módulo, y ningún host lo reimplementa.
La frontera de importación
El módulo del Engine declara cero dependencias de terceros. Su
go.mod no tiene bloque require, y un test lo mantiene así en cada compilación.
Dos guardas corren dentro del módulo en cada go test ./..., y el workflow de CI del módulo las ejecuta con el workspace de Go desactivado:
- La lista de permitidos. Cada dependencia transitiva de
pkg/enginedebe ser un paquete de la biblioteca estándar de Go o un paquete local del módulo del Engine. Cualquier otra familia de imports falla la compilación, incluso una familia que ninguna lista de denegados nombre. - Una lista de denegados explícita. Clases enumeradas fallan además de la lista de permitidos. Cubren frameworks HTTP, brokers de mensajes, drivers de bases de datos, SDKs de object storage, middleware de runtime multi-tenant, las bibliotecas de autenticación y de licencia, y envolturas de la biblioteca estándar como
database/sql,net/httpyos/exec.
require en el go.mod del Engine y falla el job cuando la encuentra.
Por qué esto importa cuando embebes
- Sin conflictos de dependencias. El Engine no puede meter un driver, un cliente de broker o un framework HTTP en el grafo de tu módulo. Tu host mantiene el control total de sus propias versiones.
- Sin E/S oculta. El núcleo no puede abrir un socket, un archivo ni una base de datos por su cuenta. Cada byte que entra y sale cruza un puerto que tú proveíste.
- Un punto de sustitución estable. Ejecuta todo el Engine contra la implementación en memoria en tus tests. Cambia a los adaptadores reales en producción, sin tocar el código que lo llama.
Modo Direct y modo Store
Un plan de extracción lleva un modo con tres valores:
direct, store y el valor cero auto.
El Engine resuelve auto a partir de los puertos que conectaste. Con un ResultSink configurado elige el modo Store, y sin él elige el modo Direct. Así, el modo es una consecuencia de tu composición, no un interruptor aparte. Una petición explícita de store sin sink falla de entrada con un error de validación, antes de que el Engine toque ningún datasource.
Los dos modos devuelven formas distintas. Exactamente una rama del resultado es no nula, y el JSON omite por completo la rama sin usar.
Modo Direct
El Engine ejecuta los pasos del plan y fusiona las filas en un solo mapa. Las claves del mapa son el nombre de la configuración del datasource y luego la tabla calificada. El Engine serializa ese mapa una vez como JSON indentado y devuelve los bytes inline.
La salida del modo Direct es determinista. El serializador ordena las claves del mapa. Por eso, la misma entrada te da un JSON idéntico byte a byte y el mismo digest, sin importar en qué orden terminaron los pasos paralelos.
Tu host es dueño de todo lo que viene después. El Worker de Fetcher, por ejemplo, firma el texto plano con HMAC-SHA256 y lo cifra antes de guardar los bytes.
Modo Store
El Engine abre un stream en tuResultSink y escribe el resultado de forma incremental, en memoria constante. Nunca retiene el resultado completo. Un único escritor drena las goroutines de extracción estrictamente en orden ascendente de paso del plan, y el digest de integridad cubre exactamente los bytes escritos.
La forma en el cable es NDJSON — un objeto JSON por línea, terminada en salto de línea, sin array que lo envuelva:
Ante un aborto — un error de escritura, un límite de tamaño superado o un contexto cancelado — el Engine abandona el escritor y nunca llama a
Close. Por eso un resultado parcial nunca se convierte en una referencia devuelta. Trata un escritor sin cerrar como una escritura descartada.
Contratos de fallo y de resultado
- Fallo inmediato entre datasources. El primer paso que falla detiene la corrida. El Engine nunca devuelve un resultado parcial.
- Errores redactados. Un error de driver puede llevar dentro un DSN, una credencial o detalles internos del driver, así que el Engine lo descarta y devuelve un mensaje fijo en su lugar.
- Once categorías de error.
validation,not_found,unauthorized,forbidden,limit_exceeded,conflict,unavailable,connect,timeout,canceledeinternal. Tu host las mapea a sus propios códigos de transporte.connectse mantiene distinto deunavailable, ytimeoutse mantiene distinto decanceled. - Cinco estados de ejecución.
pending,running,completed,failedycanceled. Los tres últimos son terminales. Una cancelación del host registracanceled, y un plazo excedido registrafailed. - Conectores cerrados. Cada conector que abre el Engine se vuelve a cerrar, en el camino de éxito y en cada camino de fallo.
Límites y alcance por tenant
El Engine nunca corre sin límites. Cuando no aportas ninguno, aplica estos valores por defecto:
Una petición puede bajar cualquier límite, y nunca subirlo. Un override por encima del valor por defecto falla con un error de validación que nombra el campo infringido. Un override cero o negativo mantiene el valor por defecto.
El Engine aplica el techo de tamaño del resultado dos veces: una cota inferior barata por paso, que aborta temprano, y una comprobación autoritativa sobre el payload indentado final. Un resultado por encima del límite nunca te llega inline ni llega a tu sink.
El alcance por tenant es igual de estrecho. Cada operación lleva un tenant ID y nada más — el Engine no tiene concepto de organización ni de producto. Valida el tenant ID antes de cualquier acceso a recursos, y ahí rechaza un valor vacío o mal formado.
Próximos pasos
Embeber el Engine
Impórtalo, provee los puertos y constrúyelo con un ejemplo ejecutable.
Referencia de puertos
Cada puerto, si es obligatorio y qué pasa sin él.

