From a48fd438806f2ea4d58aa0a90a67fe5e883f1ad5 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 3 Jul 2026 22:23:22 +0200 Subject: [PATCH] =?UTF-8?q?docs(arts):=20A2=20ES->EN=20=E2=80=94=20orca=20?= =?UTF-8?q?(full=20translation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate the last Spanish README in the arts reconciliation (~1457 lines). Prose only; all code blocks, ORCA_* constants, type names and the A1 API corrections that landed in 9e48b80b are preserved verbatim. arts:check 0/0/22. Co-Authored-By: Claude Opus 4.8 --- src/arts/orca/README.md | 1488 +++++++++++++++++++-------------------- 1 file changed, 738 insertions(+), 750 deletions(-) diff --git a/src/arts/orca/README.md b/src/arts/orca/README.md index e0d39db4d..f0b726fef 100644 --- a/src/arts/orca/README.md +++ b/src/arts/orca/README.md @@ -1,199 +1,199 @@ # orca -`orca` es el artefacto de orquestacion activa del ecosistema. Su nombre viene -de **ORChestration Active** y su funcion no es transportar eventos, ni conocer -modulos concretos, ni reemplazar a `bus`. Su responsabilidad es ejecutar -acciones declarativas cuando llegan eventos del bus, con orden, dependencias, -resultados, politicas de fallo, timers, transacciones opcionales y trazabilidad. +`orca` is the ecosystem's active orchestration artifact. Its name comes +from **ORChestration Active** and its job is not to transport events, nor to know +concrete modules, nor to replace `bus`. Its responsibility is to run declarative +actions when events arrive from the bus, with ordering, dependencies, +results, failure policies, timers, optional transactions and traceability. -La idea central: +The central idea: ```txt -modulos / servidor / connection - -> bus publica eventos tipados - -> orca selecciona acciones registradas - -> orca ejecuta un pipeline - -> las acciones devuelven Result y tokens - -> orca decide continuar, abortar, saltar o finalizar +modules / server / connection + -> bus publishes typed events + -> orca selects registered actions + -> orca runs a pipeline + -> actions return Result and tokens + -> orca decides to continue, abort, skip or finish ``` -`orca` no sabe que existen `session`, `cache`, `perm`, `connection`, `storage` o -`http`. La aplicacion registra acciones que llaman a esos modulos. Esto evita -que el acoplamiento inter-modulo quede escondido en `bus`, `connection` o +`orca` does not know that `session`, `cache`, `perm`, `connection`, `storage` or +`http` exist. The application registers actions that call those modules. This +avoids inter-module coupling being hidden inside `bus`, `connection` or `active-app`. -## Estado Del Documento +## Document Status -Este README es la referencia de diseño de orca. **v1 está cerrado al -100%** y la suite global pasa (115 archivos / 1471 tests, 161 sobre -orca entre engine y wrapper reactivo). El motor expone: +This README is orca's design reference. **v1 is 100% closed** and the global suite +passes (115 files / 1471 tests, 161 on orca between engine and reactive wrapper). +The engine exposes: - `createEngineOrca({ bus, timers, logger?, maxRuns?, reentry? })` — - motor imperativo -- `createActiveOrca({ ... })` — wrapper Svelte 5 con snapshots - reactivos (`runningSnapshot`, `recentRunsSnapshot`, `latestRun`, + imperative engine +- `createActiveOrca({ ... })` — Svelte 5 wrapper with reactive + snapshots (`runningSnapshot`, `recentRunsSnapshot`, `latestRun`, `committedSnapshot`, `disposedSnapshot`) - `onEvent(event, action) → detach` - `configureEvent(event, { queuePolicy })` — `fifo` (default) / `replace` / `drop-latest` / `parallel` -- `validate()` — análisis estático del grafo (issues con severity - error o warn) -- `commit()` — congela el grafo; `onEvent` posterior lanza +- `validate()` — static analysis of the graph (issues with severity + error or warn) +- `commit()` — freezes the graph; a later `onEvent` throws `OrcaFrozenError` -- `onChange(listener)` — notifica register/detach/run-start/run-end/ - commit/dispose para integraciones reactivas -- Stages canónicos (`guard / pre / main / post / cleanup / finally`) -- Run-queue per evento + `OrcaEnvelope` con metadata runtime +- `onChange(listener)` — notifies register/detach/run-start/run-end/ + commit/dispose for reactive integrations +- Canonical stages (`guard / pre / main / post / cleanup / finally`) +- Per-event run-queue + `OrcaEnvelope` with runtime metadata (`eventId`, `traceId`, `parentEventId`, `depth`, `stack`) - `OrcaActionContext`: `eventId`, `traceId`, `depth`, `signal`, `emit()`, `tokens`, `tokenPayloads`, `logger` - `OrcaResult`: `success / skipped / error / interrupted / timeout / fatal` -- `OrcaRunResult` con `tokens`, `tokenPayloads`, `actions[]` y +- `OrcaRunResult` with `tokens`, `tokenPayloads`, `actions[]` and `compensations[]` - Reentry guards: `maxDepth`, `maxEventsPerTrace`, - `repeatedEventLimit`, `dedupeKey` con políticas + `repeatedEventLimit`, `dedupeKey` with policies `skip / abort-trace / error` -- `actionTimeoutMs` por acción — race contra timer + abort - per-acción, aislado de hermanas -- `OrcaFatal` con precedencia `FATAL > TIMEOUT > ABORTED > +- `actionTimeoutMs` per action — race against timer + per-action + abort, isolated from siblings +- `OrcaFatal` with precedence `FATAL > TIMEOUT > ABORTED > INTERRUPTED > PARTIAL > SUCCESS` -- Gates `unless` → `abortOn` → `fanIn` → `after` (orden de - evaluación); `fanIn: { tokens, min }` para quórum k-of-n -- `parallel: true` por acción — waves concurrentes con `Promise.all` -- `transaction: 'tx-id'` — grupos atómicos con rollback LIFO - inmediato cuando un miembro falla -- Tokens con payload (`emits: [{ token, payload }]`) + +- Gates `unless` → `abortOn` → `fanIn` → `after` (evaluation + order); `fanIn: { tokens, min }` for k-of-n quorum +- `parallel: true` per action — concurrent waves with `Promise.all` +- `transaction: 'tx-id'` — atomic groups with immediate LIFO + rollback when a member fails +- Tokens with payload (`emits: [{ token, payload }]`) + `ctx.tokenPayloads` -- `compensate` por acción — rollback LIFO antes de `FINALLY`, +- `compensate` per action — LIFO rollback before `FINALLY`, best-effort, run-once -- Bus interception — `bus.publish` desde dentro de una acción se - atribuye al run/acción activos vía `AsyncLocalStorage` (Node/Bun; - fallback root-event en navegadores sin `AsyncContext`) -- Diagnostics estructurados (`orca.run.*`, `orca.action.*`, +- Bus interception — `bus.publish` from within an action is + attributed to the active run/action via `AsyncLocalStorage` (Node/Bun; + root-event fallback in browsers without `AsyncContext`) +- Structured diagnostics (`orca.run.*`, `orca.action.*`, `orca.event.emitted`, `orca.reentry.blocked`, `orca.trace.aborted`, `orca.compensation.*`, `orca.queue.dropped`) -- `applyStandardOrca(App)` agregador de presets en +- `applyStandardOrca(App)` preset aggregator in `arts/active-app/presets/` -Lo aceptado para v2 está descrito en la sección "Roadmap v2" al -final. +What is accepted for v2 is described in the "Roadmap v2" section at the +end. -Las decisiones arquitectónicas que sostienen el diseño: +The architectural decisions that underpin the design: -- `bus` transporta eventos locales. -- `orca` ejecuta acciones asociadas a eventos del bus. -- `timer` coordina timers, timeouts y clocks. -- `logger` recibe diagnostics/logs. -- La aplicación decide qué módulos toca cada acción. -- Nada destructivo se ejecuta por defecto sin estar registrado. -- Payload limpio, envelope runtime separado: el módulo nunca conoce a - `orca`; sólo las `OrcaAction` reciben `ctx`. +- `bus` transports local events. +- `orca` runs actions associated with bus events. +- `timer` coordinates timers, timeouts and clocks. +- `logger` receives diagnostics/logs. +- The application decides which modules each action touches. +- Nothing destructive runs by default without being registered. +- Clean payload, separate runtime envelope: the module never knows + `orca`; only the `OrcaAction`s receive `ctx`. ## Naming -| Concepto | Nombre | -| ------------------- | ----------------------------------- | -| Artefacto | `orca` | -| Raiz imperativa | `createEngineOrca()` / `EngineOrca` | -| Raiz reactiva | `createActiveOrca()` / `ActiveOrca` | -| Accion | `OrcaAction` | -| Resultado | `OrcaResult` | -| Ejecucion de evento | `OrcaRunResult` | -| Token semantico | `OrcaToken` | +| Concept | Name | +| --------------- | ----------------------------------- | +| Artifact | `orca` | +| Imperative root | `createEngineOrca()` / `EngineOrca` | +| Reactive root | `createActiveOrca()` / `ActiveOrca` | +| Action | `OrcaAction` | +| Result | `OrcaResult` | +| Event execution | `OrcaRunResult` | +| Semantic token | `OrcaToken` | -El alias publico previsto seria: +The intended public alias would be: ```ts import { createEngineOrca, orcaSuccess } from '$orca'; ``` -## Que Problema Resuelve +## What Problem It Solves -Sin `orca`, las reacciones inter-modulo tienden a acabar repartidas: +Without `orca`, inter-module reactions tend to end up scattered: ```txt -session conoce cache -cache conoce perm -connection conoce session -active-app conoce todo +session knows cache +cache knows perm +connection knows session +active-app knows everything ``` -Eso escala mal. Un cambio de identidad, permisos, tenant, conectividad o un -evento remoto del futuro `active-server` puede implicar varias acciones: +That scales badly. A change of identity, permissions, tenant, connectivity or a +remote event from the future `active-server` can involve several actions: ```txt APP_EVENT_USER_IDENTITY_CHANGED - -> cancelar HTTP privado - -> limpiar cache privada - -> invalidar permisos - -> reautenticar conexiones - -> limpiar storage privado - -> registrar audit/diagnostics + -> cancel private HTTP + -> clear private cache + -> invalidate permissions + -> reauthenticate connections + -> clear private storage + -> record audit/diagnostics ``` -`orca` permite declarar ese flujo en un sitio unico, testeable y trazable, sin -que los modulos se conozcan entre si. +`orca` lets that flow be declared in a single place, testable and traceable, +without the modules knowing about each other. -## Que No Es +## What It Is Not -`orca` no es: +`orca` is not: -- Un bus de eventos. Eso es `bus`. -- Un transporte realtime. Eso es `connection`. -- Un motor de permisos. Eso es `perm`. -- Una cache. Eso es `cache`. -- Un replacement de `timer`, `logger` o `active-app`. -- Un workflow durable server-side al estilo Temporal. -- Un sistema que decide negocio por si mismo. -- Una dependencia interna que otros artefactos consumen para funcionar. +- An event bus. That's `bus`. +- A realtime transport. That's `connection`. +- A permissions engine. That's `perm`. +- A cache. That's `cache`. +- A replacement for `timer`, `logger` or `active-app`. +- A durable server-side workflow in the Temporal style. +- A system that decides business logic on its own. +- An internal dependency that other artifacts consume in order to work. -La regla: +The rule: ```txt -orca no conoce modulos; orca conoce eventos, acciones y resultados. +orca does not know modules; orca knows events, actions and results. ``` -Invariante de ecosistema: +Ecosystem invariant: ```txt -los artefactos publican eventos en bus -la aplicacion decide que orquestar con orca +artifacts publish events on bus +the application decides what to orchestrate with orca ``` -`session`, `cache`, `perm`, `connection`, `auth` o `http` no deben depender de -`orca` para sus flujos internos. Si un artefacto necesita coordinar su propio -estado interno, lo hace dentro del artefacto. Si una aplicacion quiere -coordinar varios artefactos cuando ocurre algo, registra acciones en `orca`. +`session`, `cache`, `perm`, `connection`, `auth` or `http` must not depend on +`orca` for their internal flows. If an artifact needs to coordinate its own +internal state, it does so inside the artifact. If an application wants to +coordinate several artifacts when something happens, it registers actions in +`orca`. -## Referentes +## Prior Art -`orca` toma ideas de varios ecosistemas, pero no copia ninguno: +`orca` takes ideas from several ecosystems, but copies none of them: -| Referente | Idea aprovechable | Diferencia de `orca` | -| --------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------- | -| Redux Toolkit listener middleware | listeners, async workflows, cancelacion, `take`, `condition`, `fork` | `orca` no esta ligado a Redux ni reducers | -| Redux-Saga | concurrencia, `fork`, `join`, `race`, `takeLatest` | `orca` evita generators y usa Result explicito | -| NgRx Effects | aislar side-effects de componentes | `orca` no depende de RxJS ni Angular | -| redux-observable | actions in, actions out | `orca` no fuerza stream Rx | -| Effector | eventos, efectos, scopes, `allSettled` | `orca` define stages, tokens y policies | -| Effect-TS | Result, timeout, retry, schedules | `orca` debe ser mucho mas pequeno y adapter-driven | -| Temporal / Durable Functions | workflows con pasos, retries y timers | `orca` v0 no es durable ni server-authoritative | +| Reference | Usable idea | Difference from `orca` | +| --------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------- | +| Redux Toolkit listener middleware | listeners, async workflows, cancellation, `take`, `condition`, `fork` | `orca` is not tied to Redux or reducers | +| Redux-Saga | concurrency, `fork`, `join`, `race`, `takeLatest` | `orca` avoids generators and uses explicit Result | +| NgRx Effects | isolate side-effects from components | `orca` does not depend on RxJS or Angular | +| redux-observable | actions in, actions out | `orca` does not force an Rx stream | +| Effector | events, effects, scopes, `allSettled` | `orca` defines stages, tokens and policies | +| Effect-TS | Result, timeout, retry, schedules | `orca` must be much smaller and adapter-driven | +| Temporal / Durable Functions | workflows with steps, retries and timers | `orca` v0 is not durable or server-authoritative | -Ideas concretas que conviene robar: +Concrete ideas worth stealing: -- RTK listener middleware: politicas de cancelacion y concurrencia sin DSL de - generators. -- XState v5 `setup()`: declarar eventos, tokens y acciones antes de construir - el runtime para ganar inferencia y validacion. -- Effect Workflow: separar accion/actividad de workflow/run, y estudiar - compensaciones explicitas. -- Temporal: mantener disciplina determinista para un posible replay/audit - futuro. +- RTK listener middleware: cancellation and concurrency policies without a + generator DSL. +- XState v5 `setup()`: declare events, tokens and actions before building + the runtime to gain inference and validation. +- Effect Workflow: separate action/activity from workflow/run, and study + explicit compensations. +- Temporal: keep deterministic discipline for a possible future replay/audit. -## Contrato Mental +## Mental Contract -Una orquestacion se define asi: +An orchestration is defined like this: ```ts Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { @@ -219,27 +219,27 @@ Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { }); ``` -`orca` solo ve: +`orca` only sees: -- evento recibido -- accion registrada +- event received +- registered action - stage -- tokens requeridos -- tokens emitidos -- resultado -- politica de error +- required tokens +- emitted tokens +- result +- error policy - timers -La accion es la que decide llamar a `App.cache`, `App.perm`, -`App.connections` o cualquier otro servicio. +The action is the one that decides to call `App.cache`, `App.perm`, +`App.connections` or any other service. -## Setup Tipado _(roadmap v2)_ +## Typed Setup _(roadmap v2)_ -> No implementado. Ver "Roadmap v2" más abajo. +> Not implemented. See "Roadmap v2" below. -La API actual es directa: `createEngineOrca()` o `createActiveOrca()` con -`onEvent()` y `configureEvent()`. Los eventos, tokens y action ids viven como -constantes exportadas por la app o por los presets: +The current API is direct: `createEngineOrca()` or `createActiveOrca()` with +`onEvent()` and `configureEvent()`. Events, tokens and action ids live as +constants exported by the app or by the presets: ```ts const Orca = createEngineOrca({ bus, timers, logger }); @@ -253,93 +253,93 @@ Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { }); ``` -Para v2 se valora un wrapper tipado tipo `setupOrca({ events, tokens, -actions })` que infiera payloads, valide tokens fantasma en gates, -restrinja `emits` a `provides` y emita un grafo navegable. Hoy esa -validación es estática y best-effort vía `Orca.validate()` (ver -sección "Validate"). +For v2 a typed wrapper such as `setupOrca({ events, tokens, actions })` is being +considered that would infer payloads, validate phantom tokens in gates, restrict +`emits` to `provides` and emit a navigable graph. Today that validation is static +and best-effort via `Orca.validate()` (see the "Validate" section). -## Eventos +## Events -`orca` se conecta a `bus` y escucha eventos declarados mediante constantes. -No se deben usar strings sueltos en acciones de aplicacion. +`orca` connects to `bus` and listens to events declared via constants. +Loose strings must not be used in application actions. -Ejemplo de evento de app: +Example of an app event: ```ts export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed' as const; ``` -Ejemplo de registro: +Example registration: ```ts Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, ACTION_RESET_PRIVATE_STATE); ``` -El payload del evento debe ser seguro: +The event payload must be safe: -- sin tokens -- sin passwords -- sin secretos -- sin headers de autorizacion -- sin raw webhook body -- sin datos privados innecesarios +- no tokens +- no passwords +- no secrets +- no authorization headers +- no raw webhook body +- no unnecessary private data -Si una accion necesita informacion sensible, debe resolverla desde el modulo -correspondiente en el momento de ejecutar la accion. +If an action needs sensitive information, it must resolve it from the +corresponding module at the moment of running the action. -## Acciones +## Actions -Una accion es una unidad de trabajo asociada a un evento. +An action is a unit of work associated with an event. ```ts interface OrcaAction { readonly id: OrcaActionId; readonly stage: OrcaStage; - readonly after?: readonly OrcaToken[]; // espera estos tokens antes de correr - readonly unless?: readonly OrcaToken[]; // idempotencia: se salta si alguno está presente - readonly abortOn?: readonly OrcaToken[]; // se BLOQUEA si alguno está presente - readonly fanIn?: OrcaFanInSpec; // quórum: corre con `min` de `tokens` presentes - readonly provides?: readonly OrcaToken[]; // tokens que declara emitir (guía a validate()) - readonly actionTimeoutMs?: number; // timeout por acción → OrcaTimeout + abort del signal - readonly transaction?: string; // tag de grupo atómico (compensación LIFO al fallar) - readonly parallel?: boolean; // wave concurrente con acciones consecutivas del mismo stage - readonly onError?: OrcaErrorPolicy; // reacción a OrcaError (CONTINUE por defecto / ABORT_RUN) - readonly compensate?: OrcaActionFn; // rollback si el run aborta tras su éxito + readonly after?: readonly OrcaToken[]; // wait for these tokens before running + readonly unless?: readonly OrcaToken[]; // idempotency: skipped if any is present + readonly abortOn?: readonly OrcaToken[]; // BLOCKED if any is present + readonly fanIn?: OrcaFanInSpec; // quorum: runs with `min` of `tokens` present + readonly provides?: readonly OrcaToken[]; // tokens it declares it emits (guides validate()) + readonly actionTimeoutMs?: number; // per-action timeout → OrcaTimeout + signal abort + readonly transaction?: string; // atomic-group tag (LIFO compensation on failure) + readonly parallel?: boolean; // concurrent wave with consecutive actions of the same stage + readonly onError?: OrcaErrorPolicy; // reaction to OrcaError (CONTINUE by default / ABORT_RUN) + readonly compensate?: OrcaActionFn; // rollback if the run aborts after its success readonly action: OrcaActionFn; } ``` -No hay `priority`, `tokenTimeoutMs`, `onFatal` ni `onTimeout`: el orden intra-stage -es el de registro, el único timeout es `actionTimeoutMs`, y un fallo irrecuperable se -señala **devolviendo** `OrcaFatal` desde la acción (no con una política aparte). +There is no `priority`, `tokenTimeoutMs`, `onFatal` or `onTimeout`: the +intra-stage order is registration order, the only timeout is `actionTimeoutMs`, +and an unrecoverable failure is signaled by **returning** `OrcaFatal` from the +action (not with a separate policy). -Una accion debe ser: +An action must be: -- nombrada con constante -- idempotente cuando sea posible -- testeable de forma aislada -- explicita en su politica de fallo -- explicita si necesita transaccion -- explicita si puede esperar tokens -- segura respecto a payloads con datos sensibles +- named with a constant +- idempotent when possible +- testable in isolation +- explicit about its failure policy +- explicit if it needs a transaction +- explicit if it can wait for tokens +- safe with respect to payloads carrying sensitive data ## Stages -Los stages dan estructura al pipeline. Dentro de un stage el orden es el de -**registro** (no hay prioridad numérica); para ordenar por dependencias usa -tokens (`after` / `provides`), y para concurrencia intra-stage, `parallel`. +Stages give structure to the pipeline. Within a stage the order is +**registration** order (there is no numeric priority); to order by dependencies +use tokens (`after` / `provides`), and for intra-stage concurrency, `parallel`. ```txt -guard valida precondiciones -pre prepara el entorno y cancela trabajo incompatible -main ejecuta el efecto principal -post reacciones derivadas tras main -cleanup limpieza de estado temporal -finally diagnostics, metrics y trazas finales +guard validates preconditions +pre prepares the environment and cancels incompatible work +main runs the main effect +post derived reactions after main +cleanup cleanup of temporary state +finally diagnostics, metrics and final traces ``` -Constantes previstas: +Intended constants: ```ts export const ORCA_STAGE_GUARD = 'guard' as const; @@ -350,38 +350,37 @@ export const ORCA_STAGE_CLEANUP = 'cleanup' as const; export const ORCA_STAGE_FINALLY = 'finally' as const; ``` -`finally` debe poder ejecutarse aunque el pipeline haya abortado, salvo que el -orquestador haya sido disposed. +`finally` must be able to run even if the pipeline aborted, unless the +orchestrator has been disposed. -## Concurrencia dentro del stage (`parallel`) +## Concurrency within the stage (`parallel`) -Dentro de un stage el orden de ejecución es el **orden de registro**. Una acción -puede declarar `parallel: true`: las acciones **consecutivas** con `parallel: true` -y el mismo stage forman una **wave** que corre en paralelo vía `Promise.all`. Una -acción secuencial (el default, `parallel: false`) rompe la wave y corre hasta -completarse antes de que arranque la siguiente. +Within a stage the execution order is the **registration order**. An action can +declare `parallel: true`: the **consecutive** actions with `parallel: true` and +the same stage form a **wave** that runs in parallel via `Promise.all`. A +sequential action (the default, `parallel: false`) breaks the wave and runs to +completion before the next one starts. -Los tokens emitidos dentro de una wave se fusionan en el set del run **después** de -que la wave se asiente; las puertas (`after` / `unless` / `abortOn` / `fanIn`) se -evalúan al inicio de la wave contra los tokens acumulados antes, así que los -hermanos de una misma wave nunca ven los tokens de los otros. Si una acción paralela -devuelve `OrcaFatal` o falla con `onError: 'abort-run'`, el run aborta solo tras -asentarse la wave — los hermanos no se cancelan a media ejecución. +The tokens emitted within a wave are merged into the run's set **after** the wave +settles; the gates (`after` / `unless` / `abortOn` / `fanIn`) are evaluated at +the start of the wave against the tokens accumulated before, so the siblings of +the same wave never see each other's tokens. If a parallel action returns +`OrcaFatal` or fails with `onError: 'abort-run'`, the run aborts only after the +wave settles — the siblings are not cancelled mid-execution. -No existen modos `sync` / `async` ni constantes `ORCA_EXEC_*`: la única palanca de -concurrencia **intra-stage** es `parallel`. La concurrencia **entre runs** del mismo -evento se configura aparte con `configureEvent(event, { queuePolicy })` (siguiente -sección). +There are no `sync` / `async` modes or `ORCA_EXEC_*` constants: the only +**intra-stage** concurrency lever is `parallel`. The concurrency **between runs** +of the same event is configured separately with +`configureEvent(event, { queuePolicy })` (next section). -## Concurrencia De Runs +## Run Concurrency -Un run es una ejecución concreta de un evento. El pipeline interno -de un run no resuelve por sí solo qué ocurre cuando el mismo evento -entra varias veces mientras una ejecución anterior sigue viva. Esa -decisión es parte del contrato del evento y se configura con -`Orca.configureEvent(event, { queuePolicy })`. +A run is a concrete execution of an event. A run's internal pipeline does not by +itself resolve what happens when the same event enters several times while a +previous execution is still alive. That decision is part of the event's contract +and is configured with `Orca.configureEvent(event, { queuePolicy })`. -Constantes vivas (`consts.ts`): +Live constants (`consts.ts`): ```ts ORCA_QUEUE_FIFO; // 'fifo' @@ -390,29 +389,28 @@ ORCA_QUEUE_DROP_LATEST; // 'drop-latest' ORCA_QUEUE_PARALLEL; // 'parallel' ``` -Semántica: +Semantics: -| Modo | Uso | -| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `fifo` (default) | Cada evento encola un run; los runs no-paralelos se serializan globalmente. Garantía simple. | -| `replace-queued` | Si llega un evento mientras hay otro encolado, el queued se descarta y se sustituye por el nuevo. **No aborta in-flight.** Última intención pendiente gana. | -| `drop-latest` | Si hay un run del mismo evento in-flight o encolado, el incoming se descarta. Ignora retriggers durante trabajo. | -| `parallel` | Lanza runs concurrentes; eventos paralelos no toman el lock global. Footgun: solapa side-effects. | +| Mode | Use | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fifo` (default) | Each event queues a run; non-parallel runs are serialized globally. Simple guarantee. | +| `replace-queued` | If an event arrives while another is queued, the queued one is discarded and replaced by the new one. **Does not abort in-flight.** Latest pending intent wins. | +| `drop-latest` | If there is a run of the same event in-flight or queued, the incoming one is discarded. Ignores retriggers during work. | +| `parallel` | Launches concurrent runs; parallel events do not take the global lock. Footgun: overlaps side-effects. | -`replace-queued` deja el run activo terminar (FINALLY incluido) — la -variante "fuerte" que aborta in-flight (`replace-current`, takeLatest) -queda en Roadmap v2. +`replace-queued` lets the active run finish (FINALLY included) — the "strong" +variant that aborts in-flight (`replace-current`, takeLatest) is in Roadmap v2. -`parallel` no es default por una razón: en orquestaciones destructivas -(cambio de identidad, logout, tenant switch, invalidación de permisos, -cache privada), solapar runs es una fuente directa de condiciones de -carrera. Opt-in explícito por evento y diagnostics visibles. +`parallel` is not the default for a reason: in destructive orchestrations +(identity change, logout, tenant switch, permission invalidation, private cache), +overlapping runs is a direct source of race conditions. Explicit opt-in per event +and visible diagnostics. ## Tokens -Los tokens son hechos semanticos producidos por acciones. +Tokens are semantic facts produced by actions. -Ejemplos: +Examples: ```ts export const ORCA_TOKEN_HTTP_PRIVATE_CANCELLED = 'http:private_cancelled' as const; @@ -422,64 +420,64 @@ export const ORCA_TOKEN_PERMISSIONS_INVALIDATED = 'perm:invalidated' as const; export const ORCA_TOKEN_CONNECTIONS_REAUTHENTICATED = 'connection:reauthenticated' as const; ``` -Una accion puede: +An action can: -- `after`: esperar tokens antes de ejecutarse. -- `unless`: saltarse si ya existe un token. -- `abortOn`: abortarse o bloquearse si aparece un token. -- `provides`: declarar los tokens que puede producir. +- `after`: wait for tokens before running. +- `unless`: skip if a token already exists. +- `abortOn`: abort or block if a token appears. +- `provides`: declare the tokens it can produce. -Los tokens evitan depender del nombre concreto de otra accion. La accion -`connection.reauth` no necesita saber si el cache se limpio por `clearCacheV1` o -`resetPrivateCache`; solo espera `cache:ok`. +Tokens avoid depending on the concrete name of another action. The action +`connection.reauth` does not need to know whether the cache was cleared by +`clearCacheV1` or `resetPrivateCache`; it only waits for `cache:ok`. -### Scope De Tokens +### Token Scope -Los tokens son **scoped al run**. Un token emitido durante un run de -`APP_EVENT_USER_IDENTITY_CHANGED` no existe para el siguiente run del mismo -evento ni para otro evento diferente. +Tokens are **scoped to the run**. A token emitted during a run of +`APP_EVENT_USER_IDENTITY_CHANGED` does not exist for the next run of the same +event nor for a different event. ```txt run A emits cache:ok -run B no ve cache:ok salvo que una accion de B lo vuelva a emitir +run B does not see cache:ok unless one of B's actions emits it again ``` -Esto evita fugas accidentales entre ejecuciones. Si la aplicacion necesita un -hecho persistente, ese hecho debe vivir en un modulo real (`cache`, `session`, -`perm`, `storage`) o publicarse como evento nuevo en `bus`, no como token global. +This avoids accidental leaks between executions. If the application needs a +persistent fact, that fact must live in a real module (`cache`, `session`, +`perm`, `storage`) or be published as a new event on `bus`, not as a global +token. -Casos permitidos: +Allowed cases: -- tokens emitidos por acciones del mismo run -- seed tokens explicitos al crear un run -- tokens derivados de configuracion del evento +- tokens emitted by actions of the same run +- explicit seed tokens when creating a run +- tokens derived from the event's configuration -Casos prohibidos en v0: +Cases forbidden in v0: -- token global compartido entre eventos -- token persistido en storage -- token reutilizado automaticamente entre runs -- token con TTL +- global token shared between events +- token persisted in storage +- token automatically reused between runs +- token with TTL - sticky token -Esta regla no debe ser configurable en v0. Si se necesita estado persistente, -la fuente de verdad es un artefacto (`session`, `cache`, `perm`, `storage`) y no +This rule must not be configurable in v0. If persistent state is needed, the +source of truth is an artifact (`session`, `cache`, `perm`, `storage`) and not `orca`. -### Tokens Flag Y Tokens Con Payload +### Flag Tokens and Payload Tokens -Los tokens son nombres semánticos (`string`) que un run acumula a medida -que las acciones los emiten. Forma básica — flag puro, sin datos: +Tokens are semantic names (`string`) that a run accumulates as actions emit them. +Basic form — pure flag, no data: ```ts return orcaSuccess({ emits: [ORCA_TOKEN_CACHE_OK] }); ``` -Una acción aguas abajo decide si actuar consultando `ctx.tokens.has(...)` -directamente o vía gates declarativos (`after` / `unless` / `abortOn` / -`fanIn`). Si necesita transportar datos (más allá de "esto pasó"), usa -la forma con payload — `emits` acepta entradas -`{ token, payload }` mezcladas con strings sueltos: +A downstream action decides whether to act by checking `ctx.tokens.has(...)` +directly or via declarative gates (`after` / `unless` / `abortOn` / `fanIn`). If +it needs to carry data (beyond "this happened"), use the payload form — `emits` +accepts `{ token, payload }` entries mixed with loose strings: ```ts return orcaSuccess({ @@ -490,50 +488,47 @@ return orcaSuccess({ }); ``` -El payload se lee aguas abajo con `ctx.tokenPayloads.get(token)` (un -`ReadonlyMap`). Tokens emitidos como string suelto -**no** aparecen en el mapa, así que `ctx.tokens.has('x')` y -`ctx.tokenPayloads.has('x')` pueden divergir (presencia vs. payload). -Los gates siguen comparando solo nombres — el payload es un canal -ortogonal para coordinación intra-run. +The payload is read downstream with `ctx.tokenPayloads.get(token)` (a +`ReadonlyMap`). Tokens emitted as a loose string do **not** +appear in the map, so `ctx.tokens.has('x')` and `ctx.tokenPayloads.has('x')` can +diverge (presence vs. payload). The gates still compare only names — the payload +is an orthogonal channel for intra-run coordination. -Limitación tipada: `payload` está tipado como `unknown`; los authors -hacen cast. La inferencia tipo `setupOrca({ tokens: { Token: -SchemaPayload } })` queda para v2. +Typed limitation: `payload` is typed as `unknown`; authors cast. Inference such +as `setupOrca({ tokens: { Token: SchemaPayload } })` is left for v2. -### Validacion Del Grafo +### Graph Validation -`orca` debe validar la configuracion antes de ejecutar en produccion. Como -minimo: +`orca` must validate the configuration before running in production. At a +minimum: -- detectar ciclos estaticos entre tokens -- detectar acciones que esperan tokens que ninguna accion puede producir -- detectar tokens declarados en `abortOn` o `unless` con typos evidentes -- detectar acciones duplicadas por `id` dentro del mismo evento -- detectar `provides` que nunca se emiten en ningun resultado posible si se - puede inferir +- detect static cycles between tokens +- detect actions that wait for tokens that no action can produce +- detect tokens declared in `abortOn` or `unless` with obvious typos +- detect actions duplicated by `id` within the same event +- detect `provides` that are never emitted in any possible result if it can be + inferred -La validacion no sustituye a tests, pero debe fallar pronto cuando el grafo sea -imposible. Un pipeline que puede quedarse bloqueado por configuracion debe -fallar al registrar acciones o durante `Orca.validate()`, no seis meses despues -en una sesion real. +Validation does not replace tests, but it must fail early when the graph is +impossible. A pipeline that can get blocked by configuration must fail when +registering actions or during `Orca.validate()`, not six months later in a real +session. -API prevista: +Intended API: ```ts Orca.validate(); Orca.commit(); ``` -`validate()` comprueba la configuracion sin cerrar el registro. `commit()` -valida y congela la configuracion para ejecucion. En desarrollo, `orca` puede -ejecutar `validate()` automaticamente antes del primer evento si el usuario no -lo hizo. +`validate()` checks the configuration without closing the registry. `commit()` +validates and freezes the configuration for execution. In development, `orca` can +run `validate()` automatically before the first event if the user did not do it. -## OrcaEnvelope y OrcaActionContext +## OrcaEnvelope and OrcaActionContext -Cada evento que el motor procesa se envuelve en un `OrcaEnvelope` -interno antes de llegar a la cola de runs: +Every event the engine processes is wrapped in an internal `OrcaEnvelope` before +reaching the run queue: ```ts interface OrcaEnvelope { @@ -555,11 +550,11 @@ interface OrcaEventMeta { } ``` -El `payload` permanece limpio (`{ userId, tenantId, ... }`). El `meta` -es propiedad del runtime de orca. +The `payload` stays clean (`{ userId, tenantId, ... }`). The `meta` is owned by +orca's runtime. -Cada `OrcaAction` recibe un `OrcaActionContext` con la metadata útil -para esa ejecución: +Each `OrcaAction` receives an `OrcaActionContext` with the metadata useful for +that execution: ```ts interface OrcaActionContext { @@ -577,43 +572,43 @@ interface OrcaActionContext { } ``` -## Regla módulo → bus, action → ctx.emit +## Rule: module → bus, action → ctx.emit -La **frontera de responsabilidad** queda nítida: +The **responsibility boundary** is clear: ```txt -Módulo → bus.publish() (nunca conoce orca) -Action → ctx.emit() (preserva traceId, parentEventId, depth) +Module → bus.publish() (never knows orca) +Action → ctx.emit() (preserves traceId, parentEventId, depth) ``` -Un módulo nunca recibe `ctx`. Si una `OrcaAction` quiere emitir un -evento derivado durante un run, usa `ctx.emit()` para que orca cree un -envelope hijo (mismo `traceId`, `parentEventId = ctx.eventId`, -`depth = ctx.depth + 1`, `emittedByAction` = id de la acción). +A module never receives `ctx`. If an `OrcaAction` wants to emit a derived event +during a run, it uses `ctx.emit()` so orca creates a child envelope (same +`traceId`, `parentEventId = ctx.eventId`, `depth = ctx.depth + 1`, +`emittedByAction` = the action's id). -Si en su lugar la acción llama a un módulo que internamente hace -`bus.publish(...)`, orca lo verá como evento durante el run pero como -**raíz** desde la perspectiva de orca: nuevo `traceId`, `depth = 0`. No -se mezcla con la traza del run actual. Esa es la **opción A** de -v0-kernel; la opción B (interceptar `bus.publish` durante un run para -atribuir `emittedByAction` perfecto) queda pospuesta a v1. +If instead the action calls a module that internally does `bus.publish(...)`, +orca will see it as an event during the run but as a **root** from orca's +perspective: new `traceId`, `depth = 0`. It does not mix with the current run's +trace. That is **option A** of the v0-kernel; option B (intercepting +`bus.publish` during a run to attribute a perfect `emittedByAction`) is now +implemented in v1 as a diagnostic bonus in environments with `AsyncLocalStorage` +— see "Bus interception" in the Roadmap v1 section. -## Reentrada Y Eventos Durante Un Run +## Reentrancy and Events During a Run -Cualquier evento que un módulo publique en el bus durante un run se -encola, no se ejecuta inline. Lo mismo aplica a los eventos derivados -vía `ctx.emit()`. El motor mantiene una cola FIFO por evento y nunca -ejecuta acciones recursivamente dentro del mismo stack. +Any event a module publishes on the bus during a run is queued, not run inline. +The same applies to derived events via `ctx.emit()`. The engine keeps a FIFO +queue per event and never runs actions recursively within the same stack. ```txt main(action A) - -> ctx.emit(B) -> child envelope B encolado - -> NO ejecuta pipeline B dentro de action A + -> ctx.emit(B) -> child envelope B queued + -> does NOT run pipeline B inside action A ``` ### Reentry guards -`createEngineOrca` acepta `reentry: OrcaReentryOptions`: +`createEngineOrca` accepts `reentry: OrcaReentryOptions`: ```ts interface OrcaReentryOptions { @@ -627,40 +622,39 @@ interface OrcaReentryOptions { } ``` -Cuando un envelope **derivado** (vía `ctx.emit()`) supera uno de los -límites, el motor aplica la política configurada: +When a **derived** envelope (via `ctx.emit()`) exceeds one of the limits, the +engine applies the configured policy: -- `skip` — el envelope no se encola; la traza continúa para otros - eventos. Diagnostic `orca.reentry.blocked` con la `reason` adecuada. -- `abort-trace` — la traza queda marcada como abortada; cualquier - evento ya encolado para esa traza se descarta cuando le toca turno; - los runs en vuelo de esa traza terminan, pero los siguientes - registran `ORCA_RUN_INTERRUPTED`. Diagnostic `orca.trace.aborted`. -- `error` — se lanza `OrcaReentryError` síncronamente desde - `ctx.emit()`. La acción puede capturarlo o dejarlo escalar. +- `skip` — the envelope is not queued; the trace continues for other events. + Diagnostic `orca.reentry.blocked` with the appropriate `reason`. +- `abort-trace` — the trace is marked as aborted; any event already queued for + that trace is discarded when its turn comes; the in-flight runs of that trace + finish, but the following ones record `ORCA_RUN_INTERRUPTED`. Diagnostic + `orca.trace.aborted`. +- `error` — `OrcaReentryError` is thrown synchronously from `ctx.emit()`. The + action can catch it or let it escalate. -Razones (`OrcaReentryReason`): +Reasons (`OrcaReentryReason`): - `max-depth` — `depth > maxDepth` - `max-events-per-trace` — `eventCount + 1 > maxEventsPerTrace` -- `repeated-event` — un mismo nombre supera `repeatedEventLimit` -- `deduped` — el `dedupeKey` ya existe en la traza -- `trace-aborted` — la traza fue marcada como abortada antes -- `run-aborted` — el run fue abortado por `ORCA_ON_ERROR_ABORT_RUN` -- `disposed` — el motor está disposed +- `repeated-event` — the same name exceeds `repeatedEventLimit` +- `deduped` — the `dedupeKey` already exists in the trace +- `trace-aborted` — the trace was marked as aborted before +- `run-aborted` — the run was aborted by `ORCA_ON_ERROR_ABORT_RUN` +- `disposed` — the engine is disposed -**Importante**: los publishes directos en el bus desde código de módulo -NO entran en estos contadores. Cada `bus.publish(event, payload)` -genera un envelope raíz con `traceId` fresco y `depth = 0`. Los -contadores nacen y mueren con cada traza. +**Important**: direct publishes on the bus from module code do NOT enter these +counters. Each `bus.publish(event, payload)` generates a root envelope with a +fresh `traceId` and `depth = 0`. The counters are born and die with each trace. -`bus` conserva la entrega local. `orca` decide cuando consume y arranca runs. -Si un evento nuevo requiere ejecucion inmediata, debe modelarse como token del -run actual, no como evento reentrante. +`bus` keeps local delivery. `orca` decides when it consumes and starts runs. If a +new event requires immediate execution, it must be modeled as a token of the +current run, not as a reentrant event. ## Result -El resultado de una accion debe ser explicito. +An action's result must be explicit. ```ts type OrcaResult = @@ -672,7 +666,7 @@ type OrcaResult = | OrcaFatal; ``` -Forma preliminar: +Preliminary form: ```ts interface OrcaSuccess { @@ -719,7 +713,7 @@ interface OrcaFatal { } ``` -Helpers previstos: +Intended helpers: ```ts orcaSuccess({ emits?: tokens, value?: data }); @@ -730,18 +724,18 @@ orcaTimeout(timeoutMs, { emits?: tokens }); orcaFatal(error, { emits?: tokens }); ``` -Las excepciones no deben ser el flujo normal. Si una accion lanza, `orca` la -captura y la convierte en `ORCA_RESULT_ERROR` o `ORCA_RESULT_FATAL` segun la -politica declarada. +Exceptions must not be the normal flow. If an action throws, `orca` catches it +and converts it into `ORCA_RESULT_ERROR` or `ORCA_RESULT_FATAL` according to the +declared policy. -`ORCA_RESULT_INTERRUPTED` representa cancelacion/interrupcion controlada, no un -fallo de negocio. Es importante para `replace`, `dispose` y futuras politicas -de cancelacion: abortar un run anterior porque llego una intencion nueva no -debe verse igual que un error de cache o una excepcion inesperada. +`ORCA_RESULT_INTERRUPTED` represents controlled cancellation/interruption, not a +business failure. It matters for `replace`, `dispose` and future cancellation +policies: aborting a previous run because a new intent arrived must not look the +same as a cache error or an unexpected exception. -## Politicas De Fallo +## Failure Policies -Una accion declara que ocurre si falla. +An action declares what happens if it fails. ```ts export const ORCA_ON_ERROR_CONTINUE = 'continue' as const; @@ -750,28 +744,28 @@ export const ORCA_ON_ERROR_ABORT_STAGE = 'abort-stage' as const; export const ORCA_ON_ERROR_ABORT_RUN = 'abort-run' as const; ``` -`ABORT_ACTION` y `ABORT_STAGE` se aceptan por compat pero hoy se comportan como -`CONTINUE`; solo `CONTINUE` (default) y `ABORT_RUN` son distintos. +`ABORT_ACTION` and `ABORT_STAGE` are accepted for compat but today behave like +`CONTINUE`; only `CONTINUE` (default) and `ABORT_RUN` are distinct. -Un fallo **irrecuperable** no tiene constante de política: la acción **devuelve** -`OrcaFatal` (`orcaFatal(error)`) y el motor **siempre** aborta el run — precede a -cualquier otro estado (timeout, aborted, partial) e ignora el `onError` de la -acción. No existen `ORCA_ON_FATAL_*`. +An **unrecoverable** failure has no policy constant: the action **returns** +`OrcaFatal` (`orcaFatal(error)`) and the engine **always** aborts the run — it +precedes any other state (timeout, aborted, partial) and ignores the action's +`onError`. There are no `ORCA_ON_FATAL_*`. -### Rollback Y Compensacion +### Rollback and Compensation -`rollback` no debe formar parte de v0 como promesa generica. En frontend, una -accion puede mutar cache, stores reactivos, IndexedDB, cookies o conexiones; no -hay rollback universal como en una base de datos. +`rollback` must not be part of v0 as a generic promise. In frontend, an action +can mutate cache, reactive stores, IndexedDB, cookies or connections; there is no +universal rollback like in a database. -Para v0, las politicas deben hablar de abortar, continuar, bloquear o limpiar. -La reparacion de estado se hace con: +For v0, policies must speak of aborting, continuing, blocking or cleaning up. +State repair is done with: -- stage `cleanup` -- acciones idempotentes -- acciones de compensacion explicitas +- the `cleanup` stage +- idempotent actions +- explicit compensation actions -Compensaciones (implementadas): +Compensations (implemented): ```ts interface OrcaAction { @@ -785,33 +779,32 @@ type OrcaActionFn = ( ) => OrcaResult | Promise>; ``` -El compensador recibe el mismo `OrcaActionContext` que la acción (su `ctx.emit()` -devuelve `null` — el rollback no es lugar para fan-out). -La compensacion no es rollback magico. Es una accion inversa o saneadora -declarada por la aplicacion. `orca` puede invocarla en orden inverso cuando un -run aborta, pero solo para acciones que la hayan declarado. +The compensator receives the same `OrcaActionContext` as the action (its +`ctx.emit()` returns `null` — rollback is not the place for fan-out). +Compensation is not magic rollback. It is an inverse or sanitizing action +declared by the application. `orca` can invoke it in reverse order when a run +aborts, but only for actions that declared it. ## Timeouts -El único timeout implementado es **`actionTimeoutMs`** por acción. El motor corre -la promesa de la acción contra un timer del `TimerScheduler` inyectado; si el timer -vence primero, produce `OrcaTimeout` (`{ ok: false, status: ORCA_RESULT_TIMEOUT, -timeoutMs }`) y aborta el `signal` de esa acción. Las acciones hermanas no se ven -afectadas — cada una tiene su propio controller. `actionTimeoutMs <= 0` (u omitido) -corre sin mediación. +The only implemented timeout is **`actionTimeoutMs`** per action. The engine runs +the action's promise against a timer of the injected `TimerScheduler`; if the +timer expires first, it produces `OrcaTimeout` (`{ ok: false, status: +ORCA_RESULT_TIMEOUT, timeoutMs }`) and aborts that action's `signal`. Sibling +actions are not affected — each has its own controller. `actionTimeoutMs <= 0` +(or omitted) runs without mediation. -Un timeout es un **resultado** (`OrcaTimeout`), no una política: no hay campo -`onTimeout` ni constantes `ORCA_ON_TIMEOUT_*`. Los timeouts de nivel superior -(`runTimeoutMs` / `stageTimeoutMs` / `idleTimeoutMs`) **no están implementados** — -son roadmap. +A timeout is a **result** (`OrcaTimeout`), not a policy: there is no `onTimeout` +field or `ORCA_ON_TIMEOUT_*` constants. The higher-level timeouts (`runTimeoutMs` +/ `stageTimeoutMs` / `idleTimeoutMs`) are **not implemented** — they are roadmap. -`orca` nunca usa `Date.now()` ni `setTimeout()` directamente: siempre el -`TimerScheduler` de `timer`. +`orca` never uses `Date.now()` or `setTimeout()` directly: always the +`TimerScheduler` of `timer`. ## Timers -`timer` coordina el tiempo. `orca` solo registra deadlines y cancela sus timers -al terminar el run o al hacer `dispose()`. +`timer` coordinates time. `orca` only registers deadlines and cancels its timers +when the run finishes or on `dispose()`. ```ts const Orca = createEngineOrca({ @@ -821,23 +814,23 @@ const Orca = createEngineOrca({ }); ``` -Internamente el motor mintea un timer por acción con `actionTimeoutMs` (atado a -`runId` / `stage` / `actionId`) y lo cancela al asentarse la acción, al terminar el -run o en `dispose()`. No hay un helper público de claves de timer: la gestión es -interna al motor. +Internally the engine mints a timer per action with `actionTimeoutMs` (tied to +`runId` / `stage` / `actionId`) and cancels it when the action settles, when the +run finishes or on `dispose()`. There is no public timer-key helper: management is +internal to the engine. -Si `orca` crea timers sobre un scheduler inyectado, no es propietario del -scheduler. `Orca.dispose()` cancela los timers registrados por `orca`, pero no -destruye `App.timers`. +If `orca` creates timers on an injected scheduler, it does not own the scheduler. +`Orca.dispose()` cancels the timers registered by `orca`, but does not destroy +`App.timers`. -## Transacciones +## Transactions -Una transacción es un **grupo atómico** de acciones que comparten el mismo tag -`transaction: string` sobre el mismo evento (pueden abarcar varios stages). Si -cualquier miembro termina en `ERROR` o `FATAL`, el motor **compensa** de inmediato -a los miembros que ya habían tenido éxito — en orden **LIFO** de finalización — y -aborta el run. El `onError` de un miembro se ignora dentro de una transacción: la -semántica transaccional siempre aborta. +A transaction is an **atomic group** of actions that share the same tag +`transaction: string` on the same event (they can span several stages). If any +member ends in `ERROR` or `FATAL`, the engine **compensates** immediately the +members that had already succeeded — in **LIFO** order of completion — and aborts +the run. A member's `onError` is ignored within a transaction: the transactional +semantics always abort. ```ts Orca.onEvent('checkout.submit', { @@ -845,24 +838,24 @@ Orca.onEvent('checkout.submit', { stage: ORCA_STAGE_MAIN, transaction: 'checkout', action: reserveStock, - compensate: releaseStock // se invoca en rollback si otro miembro falla + compensate: releaseStock // invoked in rollback if another member fails }); ``` -Etiquetar un miembro con `transaction` no obliga a declarar `compensate` — un -miembro sin compensador simplemente no tiene nada que deshacer, y `validate()` emite -un **warning** cuando ningún miembro de la transacción lo declara (no tendría efecto -de rollback). Las compensaciones de transacción se anexan a -`OrcaRunResult.compensations[]` igual que las estándar, y cada compensador corre -**como mucho una vez** por run. +Tagging a member with `transaction` does not force declaring `compensate` — a +member without a compensator simply has nothing to undo, and `validate()` emits a +**warning** when no member of the transaction declares it (it would have no +rollback effect). Transaction compensations are appended to +`OrcaRunResult.compensations[]` just like the standard ones, and each compensator +runs **at most once** per run. -No existen `OrcaTransactionPort` ni constantes `ORCA_TX_*`: la transacción es el tag -de agrupación + `compensate`, no un puerto externo ni modos `required` / -`requires-new`. +There is no `OrcaTransactionPort` or `ORCA_TX_*` constants: the transaction is the +grouping tag + `compensate`, not an external port or `required` / `requires-new` +modes. ## Run Result -Cada publicacion de evento puede producir un resumen completo: +Each event publication can produce a complete summary: ```ts interface OrcaRunResult { @@ -882,7 +875,7 @@ interface OrcaRunResult { } ``` -Cada accion ejecutada debe quedar trazada: +Each executed action must be traced: ```ts interface OrcaActionRun { @@ -904,7 +897,7 @@ interface OrcaActionRun { } ``` -Esto es clave para tests compuestos: +This is key for composite tests: ```ts expect(run.status).toBe(ORCA_RUN_ABORTED); @@ -916,13 +909,13 @@ expect(run.tokens).toContain(ORCA_TOKEN_CACHE_ERROR); ## Active Layer -`createActiveOrca()` existiria solo para inspeccion reactiva y paginas -de test/devtools. +`createActiveOrca()` would exist only for reactive inspection and test/devtools +pages. -Debe tener un limite de historial. Una sesion larga no puede acumular todos los -runs en memoria. +It must have a history limit. A long session cannot accumulate all the runs in +memory. -Propiedades posibles: +Possible properties: ```ts ActiveOrca.running; @@ -934,7 +927,7 @@ ActiveOrca.failedRuns; ActiveOrca.dispose(); ``` -Opcion prevista: +Intended option: ```ts const ActiveOrca = createActiveOrca({ @@ -942,50 +935,49 @@ const ActiveOrca = createActiveOrca({ }); ``` -La logica de ejecucion debe vivir en `createEngineOrca()`. La capa -activa no debe ser necesaria para tests de core ni para futuro server runtime. +The execution logic must live in `createEngineOrca()`. The active layer must not +be necessary for core tests or for a future server runtime. -## Integracion Con App +## App Integration -`orca` debe estar presente en `active-app`, pero inerte hasta que la aplicacion -registre acciones. +`orca` must be present in `active-app`, but inert until the application registers +actions. -Nombre recomendado en `App`: +Recommended name in `App`: ```ts App.Orchestration; ``` -`orca` queda como nombre del artefacto, alias/import y prefijo de constantes -(`$orca`, `ORCA_*`). El campo de aplicacion usa nombre semantico porque se lee -en codigo de producto: +`orca` remains the name of the artifact, alias/import and constant prefix +(`$orca`, `ORCA_*`). The application field uses a semantic name because it is read +in product code: ```ts App.Orchestration.onEvent(...); ``` -`active-app` puede crear `Bus`, `Timers`, `Logger` y `Orchestration`, pero no debe -esconder la politica de orquestacion. +`active-app` can create `Bus`, `Timers`, `Logger` and `Orchestration`, but must +not hide the orchestration policy. -Inercia esperada: +Expected inertia: -- sin acciones registradas, no hay runs -- sin acciones registradas para un evento, el evento es O(1) no-op -- `orca` se suscribe a un evento del bus solo al registrar la primera accion - para ese evento -- si se elimina la ultima accion de un evento, `orca` se desuscribe de ese - evento -- `dispose()` es no-op si nunca se uso -- el historial activo no se reserva de forma costosa hasta el primer run +- with no registered actions, there are no runs +- with no registered actions for an event, the event is an O(1) no-op +- `orca` subscribes to a bus event only when registering the first action for + that event +- if the last action of an event is removed, `orca` unsubscribes from that event +- `dispose()` is a no-op if it was never used +- the active history is not allocated expensively until the first run -Decision de bundle v0: `App.Orchestration` puede ser un engine real incluido en -el bundle base. No se usara dynamic import para el nucleo v0; la complejidad de -un proxy async no compensa si el engine inerte es pequeno. +v0 bundle decision: `App.Orchestration` can be a real engine included in the base +bundle. Dynamic import will not be used for the v0 core; the complexity of an +async proxy is not worth it if the inert engine is small. -`Bus` tambien debe ser un recurso siempre presente e inerte. Si una app tiene -`App.Orchestration`, debe tener `App.bus`. +`Bus` must also be an always-present, inert resource. If an app has +`App.Orchestration`, it must have `App.bus`. -Uso explicito: +Explicit use: ```ts App.Orchestration.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { @@ -1001,64 +993,65 @@ App.Orchestration.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { }); ``` -Un preset futuro puede existir, pero debe ser una funcion que registra acciones -visibles: +A future preset may exist, but it must be a function that registers visible +actions: ```ts registerStandardAppOrchestration(App, App.Orchestration); ``` -No debe haber magia implicita donde `createActiveApp()` active side-effects -destructivos sin que el desarrollador pueda ver que acciones se registraron. +There must be no implicit magic where `createActiveApp()` activates destructive +side-effects without the developer being able to see which actions were +registered. -### Registro Dinamico +### Dynamic Registration -`orca` debe permitir registrar acciones dinamicamente, por ejemplo cuando una -feature lazy-loaded se activa. +`orca` must allow registering actions dynamically, for example when a +lazy-loaded feature is activated. -Regla: +Rule: ```txt -acciones registradas durante un run no participan en ese run +actions registered during a run do not participate in that run ``` -Cada run usa un snapshot de acciones tomado al inicio. Las acciones nuevas solo -participan en runs futuros. Esto evita que el set de trabajo cambie a mitad de +Each run uses a snapshot of actions taken at the start. New actions only +participate in future runs. This prevents the working set from changing mid- pipeline. -## Fan-In Entre Eventos +## Fan-In Between Events -v0 mantiene `orca` como motor **single-event-trigger**: un evento dispara un -pipeline de acciones. No existe `onEvents([A, B])` en v0. +v0 keeps `orca` as a **single-event-trigger** engine: one event fires a pipeline +of actions. There is no `onEvents([A, B])` in v0. -Si una aplicacion necesita fan-in como: +If an application needs fan-in such as: ```txt -ejecutar cuando auth.ready y perm.loaded hayan ocurrido +run when auth.ready and perm.loaded have both occurred ``` -debe sintetizar un evento compuesto fuera de `orca`: +it must synthesize a composite event outside `orca`: ```txt APP_EVENT_AUTH_READY APP_EVENT_PERMISSIONS_LOADED - -> modulo/combinador publica APP_EVENT_SECURITY_CONTEXT_READY - -> orca escucha APP_EVENT_SECURITY_CONTEXT_READY + -> module/combiner publishes APP_EVENT_SECURITY_CONTEXT_READY + -> orca listens to APP_EVENT_SECURITY_CONTEXT_READY ``` -Esto mantiene el core simple y evita abrir en v0 preguntas grandes: +This keeps the core simple and avoids opening big questions in v0: -- que ocurre si A llega dos veces antes que B -- cuanto vive el join -- si el join reinicia timeout -- como se cancelan joins al cambiar usuario -- si los payloads se combinan o se reemplazan +- what happens if A arrives twice before B +- how long the join lives +- whether the join resets the timeout +- how joins are cancelled when the user changes +- whether payloads are combined or replaced -`onEvents()` puede ser una extension futura, pero no debe bloquear el core. +`onEvents()` can be a future extension, but it must not block the core. -## Ejemplo Completo +## Complete Example -Cambio de identidad: +Identity change: ```ts Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { @@ -1112,7 +1105,7 @@ Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, { }); ``` -## Algoritmo Preliminar +## Preliminary Algorithm ```txt on bus event: @@ -1145,24 +1138,23 @@ on bus event: emit diagnostics ``` -Protecciones obligatorias: +Mandatory protections: -- detectar ciclos de tokens/configuracion -- detectar acciones bloqueadas por tokens que nadie produce -- aislar tokens por run -- aplicar politica de concurrencia por evento -- no ejecutar eventos publicados durante un run inline -- no ejecutar acciones tras `dispose()` -- cancelar timers propios al abortar o finalizar -- capturar excepciones de acciones -- no publicar secretos en diagnostics -- no dejar promesas colgadas sin status final +- detect token/configuration cycles +- detect actions blocked by tokens nobody produces +- isolate tokens per run +- apply the per-event concurrency policy +- do not run events published during a run inline +- do not run actions after `dispose()` +- cancel own timers on abort or finish +- catch action exceptions +- do not publish secrets in diagnostics +- do not leave dangling promises without a final status ## Diagnostics -`orca` usa el `Logger` común de `libs/logger` con una capa de -diagnostics catalogada. Las claves vivas (`ORCA_DIAGNOSTIC_EVENTS` en -`consts.ts`): +`orca` uses the common `Logger` from `libs/logger` with a cataloged diagnostics +layer. The live keys (`ORCA_DIAGNOSTIC_EVENTS` in `consts.ts`): ```ts 'orca.run.started'; @@ -1186,272 +1178,268 @@ diagnostics catalogada. Las claves vivas (`ORCA_DIAGNOSTIC_EVENTS` en 'orca.configuration.invalid'; ``` -Cada evento carga meta con `runId` / `eventId` / `traceId` / `depth` -cuando aplica, más campos específicos (`durationMs`, `error`, `reason`, -etc). Los mensajes y niveles viven en `diagnostics.ts` — nunca -hardcodeados en el runtime. +Each event carries meta with `runId` / `eventId` / `traceId` / `depth` when +applicable, plus specific fields (`durationMs`, `error`, `reason`, etc). The +messages and levels live in `diagnostics.ts` — never hardcoded in the runtime. ## Tests -Cobertura unitaria del motor en -`src/arts/orca/test/engine-orca.test.ts` (registro, stages, error -policies, run trace, dispose, envelope/contexto, reentry guards, -diagnostics, `actionTimeoutMs`, `OrcaFatal`, gates `after`/`unless`/ -`abortOn`/`fanIn`, `validate()`, `compensate`, `commit()`, `parallel` -waves, `transaction`, tokens con payload, queue policies, -bus interception). Cobertura del wrapper reactivo en -`src/arts/orca/test/active-orca.svelte.test.ts` (snapshots -reactivos vía `engine.onChange`). - -Test compuesto del ecosistema vivo en -`src/arts/active-app/test/ecosystem-orca.test.ts`: cambio de -usuario A → B que valida en una sola trace orca limpieza de -cache + invalidación de perm + reauth de connections; sobre -revoke ejecuta cache-clear-on-revoke + connections-close-on-revoke; -verifica que `onError: continue` no bloquea presets hermanos -cuando uno falla; y comprueba que el detach de -`applyStandardOrca` desregistra todo. - -## Invariantes - -- `orca` no importa artefactos concretos salvo contratos comunes. -- `orca` no conoce módulos de negocio. -- Los artefactos no consumen `orca`; solo publican eventos en `bus`. -- La aplicación registra acciones en `orca`. -- `App.orca` existe siempre, pero no ejecuta nada sin acciones. -- `App.bus` debe existir si existe `App.orca`. -- Todas las strings públicas viven en constantes. -- Los eventos son constantes, no strings inline. -- Los tokens son constantes, no strings inline. -- Los logs usan `Logger` común. -- Los diagnostics son catalogados. -- Los timers los ejecuta `timer`. -- El resultado de una acción siempre queda representado. -- Los payloads de eventos no contienen credenciales. -- Las acciones destructivas son explícitas. -- Los módulos nunca reciben `ctx`. Sólo las `OrcaAction` lo reciben, y - `ctx.emit()` es el camino que preserva trazabilidad. +Unit coverage of the engine in `src/arts/orca/test/engine-orca.test.ts` +(registration, stages, error policies, run trace, dispose, envelope/context, +reentry guards, diagnostics, `actionTimeoutMs`, `OrcaFatal`, gates +`after`/`unless`/`abortOn`/`fanIn`, `validate()`, `compensate`, `commit()`, +`parallel` waves, `transaction`, tokens with payload, queue policies, bus +interception). Coverage of the reactive wrapper in +`src/arts/orca/test/active-orca.svelte.test.ts` (reactive snapshots via +`engine.onChange`). + +Composite test of the live ecosystem in +`src/arts/active-app/test/ecosystem-orca.test.ts`: a user change A → B that +validates in a single orca trace the cache cleanup + perm invalidation + +connections reauth; on revoke it runs cache-clear-on-revoke + +connections-close-on-revoke; verifies that `onError: continue` does not block +sibling presets when one fails; and checks that detaching `applyStandardOrca` +unregisters everything. + +## Invariants + +- `orca` does not import concrete artifacts except common contracts. +- `orca` does not know business modules. +- Artifacts do not consume `orca`; they only publish events on `bus`. +- The application registers actions in `orca`. +- `App.orca` always exists, but runs nothing without actions. +- `App.bus` must exist if `App.orca` exists. +- All public strings live in constants. +- Events are constants, not inline strings. +- Tokens are constants, not inline strings. +- Logs use the common `Logger`. +- Diagnostics are cataloged. +- Timers are run by `timer`. +- An action's result is always represented. +- Event payloads do not contain credentials. +- Destructive actions are explicit. +- Modules never receive `ctx`. Only the `OrcaAction`s receive it, and + `ctx.emit()` is the path that preserves traceability. ## Roadmap v1 -Aceptado en el contrato público y honrado por el motor — v1 cerrado al 100%. - -### Ya en el motor (de v1) - -- ✅ **`actionTimeoutMs`** — el motor corre la acción contra un timer - del scheduler inyectado; al expirar aborta el `signal` y produce - `ORCA_RESULT_TIMEOUT`. Las acciones hermanas siguen su curso. -- ✅ **`OrcaFatal`** — `orcaFatal(error)` produce - `ORCA_ACTION_STATUS_FATAL` y aborta el run inmediatamente sin - consultar `onError`. El stage `FINALLY` se ejecuta de todas formas. - Run status `ORCA_RUN_FATAL` con precedencia sobre cualquier otro. -- ✅ **Gates `after` / `unless` / `abortOn`** — el motor evalúa los - tokens declarados en cada acción contra los emitidos hasta ese - punto del run. `unless` se prueba primero (idempotencia: skip si - alguno está presente), luego `abortOn` (halt: blocked si alguno - está presente), luego `after` (dependencia: skip si alguno falta). - La razón se serializa en `OrcaActionRun.reason` como +Accepted in the public contract and honored by the engine — v1 100% closed. + +### Already in the engine (from v1) + +- ✅ **`actionTimeoutMs`** — the engine runs the action against a timer + of the injected scheduler; on expiry it aborts the `signal` and produces + `ORCA_RESULT_TIMEOUT`. Sibling actions carry on. +- ✅ **`OrcaFatal`** — `orcaFatal(error)` produces + `ORCA_ACTION_STATUS_FATAL` and aborts the run immediately without + consulting `onError`. The `FINALLY` stage runs anyway. + Run status `ORCA_RUN_FATAL` with precedence over any other. +- ✅ **Gates `after` / `unless` / `abortOn`** — the engine evaluates the + tokens declared in each action against those emitted up to that + point in the run. `unless` is tested first (idempotency: skip if + any is present), then `abortOn` (halt: blocked if any is + present), then `after` (dependency: skip if any is missing). + The reason is serialized in `OrcaActionRun.reason` as `unless-triggered:` / `abort-on-triggered:` / - `after-not-met:,,…`. El stage `FINALLY` los bypasa - siempre. -- ✅ **`validate()`** — análisis estático que recorre las acciones - registradas por evento, en orden canónico `(stage, registeredAt)`, - acumulando los `provides` upstream. Para cada acción reporta: - `unsatisfiable-after` (error: token no producido por nadie ni en - un stage anterior ni más arriba en el mismo stage), - `orphan-unless` / `orphan-abort-on` (warnings: gate inútil), - `dependency-cycle` (error: A.after necesita lo que B.provides y - viceversa). Devuelve `{ ok, issues }`; nunca lanza. La aplicación - decide si tratar las issues como bloqueantes — el motor no congela - registros ni runs en función del resultado. -- ✅ **`commit()`** — congela el grafo de acciones. Tras - `commit()`, cualquier `onEvent()` lanza `OrcaFrozenError`. Es - idempotente y no ejecuta `validate()` implícitamente: el caller - decide si valida primero y trata las issues como bloqueantes. - Las funciones `detach` devueltas antes del freeze siguen - funcionando — `commit()` cierra registros nuevos, no los - existentes. `dispose()` opera con normalidad sobre un motor - ya commiteado y tiene precedencia sobre el guardia frozen. -- ✅ **`parallel`** — flag opt-in por acción. Acciones consecutivas - con `parallel: true` y mismo stage forman una **wave** que corre - con `Promise.all`; las secuenciales rompen la wave (cada una es - una wave de uno). Los gates (`after`/`unless`/`abortOn`) se - evalúan al **inicio** de la wave contra los tokens acumulados - por waves anteriores; los emitidos durante la wave se mergean - cuando la wave **liquida**, así dos paralelas hermanas nunca se - ven entre sí. Errores y `OrcaFatal` se evalúan tras settle: la - wave se completa, luego el run se aborta si alguien lanzó FATAL - o ERROR+ABORT_RUN. Las acciones en vuelo no se cancelan a media - ejecución (cada una conserva su propio `actionTimeoutMs`). Las - paralelas que tuvieron éxito y declaran `compensate` entran a - la pila LIFO y se compensan en orden inverso a su registro. - `validate()` retiene `provides` hasta el final de la wave: dos - paralelas hermanas con `after`/`provides` cruzados se reportan - como `unsatisfiable-after`. -- ✅ **`transaction`** — grupos atómicos por evento (cualquier - stage). Si un miembro del tx termina en `ERROR` o `FATAL`, el - motor compensa **inmediatamente** los miembros del mismo tx que - ya tuvieron éxito en orden LIFO de finalización, marca al run - como abortado, y deja al flujo estándar pre-FINALLY que compense - el resto. Las semánticas tx **anulan** el `onError` del miembro - que falla (el tx siempre aborta el run; no hace falta declarar - `abort-run`). Los compensadores se invocan **a lo más una vez**: - el tx marca a sus miembros como `compensated` y la pasada global - los salta. Cada `OrcaActionRun` carga `transactionId?` para - navegación de trace. `validate()` emite warning - `transaction-without-compensate` cuando todos los miembros de un - tx no declaran `compensate` (rollback no-op). -- ✅ **Tokens con payload** — el array `emits` acepta entradas - `{ token, payload }` además de strings sueltos; ambos formatos - se mezclan en el mismo array. `ctx.tokenPayloads` (mapa - `ReadonlyMap`) expone los payloads por - nombre — los tokens emitidos como string suelto **no** aparecen - en el mapa, así `ctx.tokens.has('x')` y - `ctx.tokenPayloads.has('x')` pueden divergir (presencia vs. - payload). Los gates (`after`/`unless`/`abortOn`/`provides`) - siguen comparando solo nombres. La instantánea de `tokenPayloads` - por wave es independiente: paralelas hermanas nunca ven los - payloads emitidos por sus pares mid-flight. Last-write-wins en - colisiones de nombre. `OrcaActionRun.emittedPayloads?` carga el - mapa por acción (ausente si la acción solo emitió strings). - `OrcaRunResult.tokenPayloads` es la unión final del run (Map - vacío cuando ningún token llevó payload). -- ✅ **`fan-in`** — gate de quórum. La acción declara - `fanIn: { tokens, min }` y dispara cuando al menos `min` de los - tokens listados están presentes en la instantánea de tokens del - wave. `min` por defecto es `tokens.length` (semántica AND - equivalente a `after`); con `min: 1` se obtiene - "first-to-finish wins". Orden de gates: `unless` → `abortOn` → - `fanIn` → `after`. Si el quórum no se cumple, la acción se - marca SKIPPED con `reason = fan-in-not-met:/:`. - `fanIn` y `after` pueden coexistir; ambos deben pasar. - `validate()` reporta error `unsatisfiable-fan-in` cuando menos - de `min` tokens del set tienen proveedor upstream — el gate - jamás dispararía. + `after-not-met:,,…`. The `FINALLY` stage bypasses them + always. +- ✅ **`validate()`** — static analysis that walks the actions + registered per event, in canonical order `(stage, registeredAt)`, + accumulating the upstream `provides`. For each action it reports: + `unsatisfiable-after` (error: token produced by nobody in an + earlier stage nor higher up in the same stage), + `orphan-unless` / `orphan-abort-on` (warnings: useless gate), + `dependency-cycle` (error: A.after needs what B.provides and + vice versa). Returns `{ ok, issues }`; never throws. The application + decides whether to treat the issues as blocking — the engine does not + freeze registrations or runs based on the result. +- ✅ **`commit()`** — freezes the action graph. After + `commit()`, any `onEvent()` throws `OrcaFrozenError`. It is + idempotent and does not run `validate()` implicitly: the caller + decides whether to validate first and treat the issues as blocking. + The `detach` functions returned before the freeze keep + working — `commit()` closes new registrations, not the + existing ones. `dispose()` operates normally on an + already-committed engine and takes precedence over the frozen guard. +- ✅ **`parallel`** — opt-in flag per action. Consecutive actions + with `parallel: true` and the same stage form a **wave** that runs + with `Promise.all`; the sequential ones break the wave (each is + a wave of one). The gates (`after`/`unless`/`abortOn`) are + evaluated at the **start** of the wave against the tokens accumulated + by previous waves; those emitted during the wave are merged + when the wave **settles**, so two sibling parallels never see + each other. Errors and `OrcaFatal` are evaluated after settle: the + wave completes, then the run aborts if someone threw FATAL + or ERROR+ABORT_RUN. In-flight actions are not cancelled mid- + execution (each keeps its own `actionTimeoutMs`). The parallels + that succeeded and declare `compensate` enter the LIFO stack and are + compensated in reverse order of their registration. + `validate()` retains `provides` until the end of the wave: two + sibling parallels with crossed `after`/`provides` are reported + as `unsatisfiable-after`. +- ✅ **`transaction`** — atomic groups per event (any + stage). If a tx member ends in `ERROR` or `FATAL`, the + engine compensates **immediately** the members of the same tx that + already succeeded in LIFO order of completion, marks the run + as aborted, and lets the standard pre-FINALLY flow compensate + the rest. The tx semantics **override** the `onError` of the failing + member (the tx always aborts the run; there is no need to declare + `abort-run`). Compensators are invoked **at most once**: + the tx marks its members as `compensated` and the global pass + skips them. Each `OrcaActionRun` carries `transactionId?` for + trace navigation. `validate()` emits warning + `transaction-without-compensate` when all members of a + tx do not declare `compensate` (no-op rollback). +- ✅ **Tokens with payload** — the `emits` array accepts entries + `{ token, payload }` besides loose strings; both formats + mix in the same array. `ctx.tokenPayloads` (map + `ReadonlyMap`) exposes the payloads by + name — the tokens emitted as a loose string do **not** appear + in the map, so `ctx.tokens.has('x')` and + `ctx.tokenPayloads.has('x')` can diverge (presence vs. + payload). The gates (`after`/`unless`/`abortOn`/`provides`) + still compare only names. The `tokenPayloads` snapshot + per wave is independent: sibling parallels never see the + payloads emitted by their peers mid-flight. Last-write-wins on + name collisions. `OrcaActionRun.emittedPayloads?` carries the + map per action (absent if the action only emitted strings). + `OrcaRunResult.tokenPayloads` is the run's final union (empty + Map when no token carried a payload). +- ✅ **`fan-in`** — quorum gate. The action declares + `fanIn: { tokens, min }` and fires when at least `min` of the + listed tokens are present in the wave's token snapshot. + `min` defaults to `tokens.length` (AND semantics + equivalent to `after`); with `min: 1` you get + "first-to-finish wins". Gate order: `unless` → `abortOn` → + `fanIn` → `after`. If the quorum is not met, the action is + marked SKIPPED with `reason = fan-in-not-met:/:`. + `fanIn` and `after` can coexist; both must pass. + `validate()` reports error `unsatisfiable-fan-in` when fewer + than `min` tokens of the set have an upstream provider — the gate + would never fire. - ✅ **Queue policies** — `orca.configureEvent(event, { queuePolicy })` - selecciona cómo se gestionan los runs concurrentes / pendientes - por evento. **Nota importante**: el motor implementa - **single global lane** para todos los eventos no-paralelos — - como mucho un run `fifo` / `replace-queued` / `drop-latest` - está en vuelo a la vez, sin importar a qué evento pertenezca. - Esto preserva la invariante v0 y hace predecible la - composición cross-event (cache-invalidate-then-refresh-perm, - cambio-de-identidad, etc). La concurrencia per-event para - políticas no-paralelas queda en Roadmap v2. - - `'fifo'` (default): cada evento encola un run; los runs se - serializan en la lane global no-paralela. - - `'replace-queued'`: cuando llega un nuevo evento del mismo - nombre y ya hay uno encolado, el queued se descarta (se emite - `orca.queue.dropped` con `reason: 'replaced'`). El in-flight - NO se aborta; máximo "1 in-flight + 1 queued" por evento. La - variante fuerte takeLatest (abortar in-flight + encolar nuevo) - queda reservada como `'replace-current'` para v2. - - `'drop-latest'`: si hay un run del mismo evento in-flight o - encolado, el incoming se descarta (`reason: 'drop-latest'`). - - `'parallel'`: los runs se lanzan concurrentemente vía - `Promise`; eventos paralelos no toman el lock global, así - que pueden correr a la vez con eventos no-paralelos. - `dispose()` aborta todos los `AbortController` in-flight de - una vez. `configureEvent` es idempotente con la misma - política y throw si se intenta cambiarla; throws - `OrcaFrozenError` después de `commit()`. -- ✅ **`compensate`** — cada acción puede declarar una función - compensatoria. Cuando el run aborta (FATAL / ABORT_RUN onError / - trace-aborted), el motor recorre la pila LIFO de acciones que ya - completaron `success` con compensador y las invoca en orden - inverso. Cada compensación corre con un `AbortController` propio - (el del run ya está abortado) y un `ctx` cuyo `emit()` devuelve - `null` — las compensaciones no fan-out eventos, son rollback puro. - Best-effort: si una compensación lanza, se registra y la cadena - continúa con la siguiente. Las compensaciones aparecen en - `OrcaRunResult.compensations[]` separadas de `actions[]`. Los - stages `FINALLY` no son compensables (finally es la limpieza - misma) y se ejecutan **después** de las compensaciones. -- ✅ **Bus interception (bonus diagnóstico, no contrato)** — cuando - un módulo llama `bus.publish('e', payload)` desde dentro del - cuerpo de una acción, **si el entorno expone** - `AsyncLocalStorage`, el motor atribuye el evento al run/acción - activos: el envelope resultante es un **child** (depth+1, mismo - `traceId`, `parentEventId` apuntando al run en curso, - `emittedByAction` igual al id de la acción). Implementación: sync - detect de `globalThis.AsyncLocalStorage`, fallback a - `node:async_hooks` vía dynamic import en Node/Bun. En navegadores - sin `AsyncContext` el campo queda en `null` y la semántica vuelve - a root-event. - > **Regla dura**: las acciones deben usar `ctx.emit()` siempre - > que necesiten atribución / encadenamiento de trace. La - > interception es un bonus diagnóstico que mejora el trace en - > entornos con ALS, no un contrato cross-env. Una acción que - > emita vía `bus.publish` obtiene resultados distintos en - > Node/SSR vs. browser sin AsyncContext, y los reentry guards no - > aplican igual si el evento entra como root. Para fan-out - > determinista usa `ctx.emit()` y deja `bus.publish` para - > eventos genuinamente raíz (input UI, mensaje server, etc). -- ✅ **`createActiveOrca()`** — wrapper Svelte 5 reactivo sobre - `EngineOrca`. Expone los mismos métodos del engine y añade - snapshots respaldados por `$state`: `runningSnapshot`, + selects how the concurrent / pending runs are managed + per event. **Important note**: the engine implements a + **single global lane** for all non-parallel events — + at most one `fifo` / `replace-queued` / `drop-latest` run + is in flight at a time, regardless of which event it belongs to. + This preserves the v0 invariant and makes cross-event + composition predictable (cache-invalidate-then-refresh-perm, + identity-change, etc). Per-event concurrency for + non-parallel policies is in Roadmap v2. + - `'fifo'` (default): each event queues a run; the runs are + serialized in the non-parallel global lane. + - `'replace-queued'`: when a new event of the same + name arrives and one is already queued, the queued one is discarded (it + emits `orca.queue.dropped` with `reason: 'replaced'`). The in-flight one + is NOT aborted; at most "1 in-flight + 1 queued" per event. The + strong takeLatest variant (abort in-flight + queue new) + is reserved as `'replace-current'` for v2. + - `'drop-latest'`: if there is a run of the same event in-flight or + queued, the incoming one is discarded (`reason: 'drop-latest'`). + - `'parallel'`: the runs are launched concurrently via + `Promise`; parallel events do not take the global lock, so + they can run at the same time as non-parallel events. + `dispose()` aborts all in-flight `AbortController`s at + once. `configureEvent` is idempotent with the same + policy and throws if you try to change it; throws + `OrcaFrozenError` after `commit()`. +- ✅ **`compensate`** — each action can declare a + compensating function. When the run aborts (FATAL / ABORT_RUN onError / + trace-aborted), the engine walks the LIFO stack of actions that already + completed `success` with a compensator and invokes them in reverse + order. Each compensation runs with its own `AbortController` + (the run's is already aborted) and a `ctx` whose `emit()` returns + `null` — compensations do not fan-out events, they are pure rollback. + Best-effort: if a compensation throws, it is recorded and the chain + continues with the next one. Compensations appear in + `OrcaRunResult.compensations[]` separate from `actions[]`. The + `FINALLY` stages are not compensable (finally is the cleanup + itself) and run **after** the compensations. +- ✅ **Bus interception (diagnostic bonus, not contract)** — when + a module calls `bus.publish('e', payload)` from within the + body of an action, **if the environment exposes** + `AsyncLocalStorage`, the engine attributes the event to the active + run/action: the resulting envelope is a **child** (depth+1, same + `traceId`, `parentEventId` pointing to the current run, + `emittedByAction` equal to the action's id). Implementation: sync + detection of `globalThis.AsyncLocalStorage`, fallback to + `node:async_hooks` via dynamic import in Node/Bun. In browsers + without `AsyncContext` the field stays `null` and the semantics revert + to root-event. + > **Hard rule**: actions must use `ctx.emit()` whenever + > they need trace attribution / chaining. The + > interception is a diagnostic bonus that improves the trace in + > environments with ALS, not a cross-env contract. An action that + > emits via `bus.publish` gets different results in + > Node/SSR vs. browser without AsyncContext, and the reentry guards do + > not apply the same way when the event enters as root. For + > deterministic fan-out use `ctx.emit()` and leave `bus.publish` for + > genuinely root events (UI input, server message, etc). +- ✅ **`createActiveOrca()`** — reactive Svelte 5 wrapper over + `EngineOrca`. Exposes the same engine methods and adds + `$state`-backed snapshots: `runningSnapshot`, `recentRunsSnapshot`, `latestRun`, `committedSnapshot`, - `disposedSnapshot`. Los cells se actualizan en el listener de - `engine.onChange()` (notifica en register/detach, run-start, - run-complete, `commit()`, `dispose()`) — sin `$effect` para - evitar `effect_update_depth_exceeded`. El wrapper no tiene - lógica de orquestación propia; sólo capa reactiva. Las API no - reactivas (`running`, `committed`, `disposed`, `recentRuns()`) - siguen disponibles para readers programáticos sync. + `disposedSnapshot`. The cells are updated in the listener of + `engine.onChange()` (which notifies on register/detach, run-start, + run-complete, `commit()`, `dispose()`) — without `$effect` to + avoid `effect_update_depth_exceeded`. The wrapper has no + orchestration logic of its own; only the reactive layer. The non- + reactive APIs (`running`, `committed`, `disposed`, `recentRuns()`) + remain available for sync programmatic readers. ## Roadmap v2 -Lista de mejoras y features pendientes, ordenadas por categoría. -No están aceptadas en el contrato público de v1 — pueden cambiar de -forma libre antes de aterrizar. - -### Diferidas explícitamente desde v1 - -- **`'replace-current'` policy** — variante "fuerte" de - `'replace-queued'` (estilo `takeLatest`) que aborta el run en - vuelo además de descartar los queued. Hoy `'replace-queued'` solo - afecta a la cola. -- **Per-event concurrency lane para políticas no-paralelas** — hoy - todos los eventos `fifo` / `replace-queued` / `drop-latest` - comparten una única "lane" global serializada (al menos un run - no-paralelo a la vez en todo el motor). Para apps con muchos - flujos independientes que no quieren parallel-policy en cada - uno, una lane por evento sería un upgrade. Requiere ampliar - `canStartRun` con tracking per-event y revisar invariantes de - sequencing cross-event. -- **Transactions cross-event** — un `transaction` hoy vive en un - solo evento; spanning entre eventos necesita reconciliar tracing - y orden de compensación. -- **Nested transactions** — txs anidados con compensación - jerárquica (rollback parcial vs. propagación al tx padre). - -### Roadmap original (`README` v0/v1, no atacado) - -- **Retry policies** por acción (con backoff: lineal, exponencial, - jitter; `retryCount` máximo). -- **Concurrency limits** — máximo N runs simultáneos a nivel de - motor o por evento (acota `parallel` y trabaja con +List of pending improvements and features, ordered by category. They are not +accepted in the v1 public contract — they may change freely before landing. + +### Explicitly deferred from v1 + +- **`'replace-current'` policy** — the "strong" variant of + `'replace-queued'` (`takeLatest` style) that aborts the in-flight + run in addition to discarding the queued ones. Today `'replace-queued'` only + affects the queue. +- **Per-event concurrency lane for non-parallel policies** — today + all `fifo` / `replace-queued` / `drop-latest` events + share a single serialized global "lane" (at least one non-parallel + run at a time in the whole engine). For apps with many independent + flows that do not want a parallel-policy on each + one, a lane per event would be an upgrade. It requires extending + `canStartRun` with per-event tracking and reviewing the cross-event + sequencing invariants. +- **Cross-event transactions** — a `transaction` today lives in a + single event; spanning across events needs to reconcile tracing + and compensation order. +- **Nested transactions** — nested txs with hierarchical + compensation (partial rollback vs. propagation to the parent tx). + +### Original roadmap (`README` v0/v1, untouched) + +- **Retry policies** per action (with backoff: linear, exponential, + jitter; max `retryCount`). +- **Concurrency limits** — a maximum of N simultaneous runs at engine + level or per event (bounds `parallel` and works with `actionTimeoutMs`). -- **Tokens con payload tipado** — el campo `payload` hoy es - `unknown`; los authors lo castean. v2 podría introducir un - registry tipado `OrcaTokenSchema` para inferencia - end-to-end. -- **Visualización del grafo por evento** — herramienta que pinta el - grafo `(stages, actions, after, provides, fanIn, transaction)` y - destaca issues de `validate()`. -- **Active inspector** — devtools/UI en runtime (lista de runs, - trace de tokens, timeline de waves, estado de queues). -- **Presets de App visibles** — un catálogo de orquestaciones - predefinidas (cache-clear-on-revoke, etc.) descubrible desde la - app; hoy son helpers sueltos. -- **Tests compuestos** con `session`, `perm`, `cache`, `connection`, - `http` — escenarios end-to-end que validan la composición de - arts vía orca. - -### Futuro `active-server` - -- Mismo modelo conceptual portado a Go/Rust en servidor. -- Acciones server con DB tx / outbox. -- Bridge de eventos server → client. -- Replay/resume por `runId` / `eventId`. -- Audit durable. +- **Tokens with typed payload** — the `payload` field today is + `unknown`; authors cast it. v2 could introduce a + typed registry `OrcaTokenSchema` for end-to-end + inference. +- **Per-event graph visualization** — a tool that draws the + graph `(stages, actions, after, provides, fanIn, transaction)` and + highlights `validate()` issues. +- **Active inspector** — devtools/UI at runtime (list of runs, + token trace, wave timeline, queue state). +- **Visible App presets** — a catalog of predefined + orchestrations (cache-clear-on-revoke, etc.) discoverable from the + app; today they are loose helpers. +- **Composite tests** with `session`, `perm`, `cache`, `connection`, + `http` — end-to-end scenarios that validate the composition of + arts via orca. + +### Future `active-server` + +- The same conceptual model ported to Go/Rust on the server. +- Server actions with DB tx / outbox. +- A bridge of server → client events. +- Replay/resume by `runId` / `eventId`. +- Durable audit.