@ -48,7 +48,7 @@ orca entre engine y wrapper reactivo). El motor expone:
- `OrcaActionContext` : `eventId` , `traceId` , `depth` , `signal` ,
`emit()` , `tokens` , `tokenPayloads` , `logger`
- `OrcaResult` : `success / skipped / error / interrupted / timeout /
fatal`
fatal`
- `OrcaRunResult` con `tokens` , `tokenPayloads` , `actions[]` y
`compensations[]`
- Reentry guards: `maxDepth` , `maxEventsPerTrace` ,
@ -57,7 +57,7 @@ orca entre engine y wrapper reactivo). El motor expone:
- `actionTimeoutMs` por acción — race contra timer + abort
per-acción, aislado de hermanas
- `OrcaFatal` con precedencia `FATAL > TIMEOUT > ABORTED >
INTERRUPTED > PARTIAL > SUCCESS`
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`
@ -92,15 +92,15 @@ Las decisiones arquitectónicas que sostienen el diseño:
## Naming
| Concepto | Nombre |
| -------- | ------ |
| Artefacto | `orca` |
| Raiz imperativa | `createEngineOrca()` / `EngineOrca` |
| Raiz reactiva | `createActiveOrca()` / `ActiveOrca` |
| Accion | `OrcaAction` |
| Resultado | `OrcaResult` |
| Ejecucion de evento | `OrcaRun ` |
| Token semantico | `OrcaToken` |
| Concepto | Nombre |
| ------------------- | ----------------------------- ------ |
| Artefacto | `orca` |
| Raiz imperativa | `createEngineOrca()` / `EngineOrca` |
| Raiz reactiva | `createActiveOrca()` / `ActiveOrca` |
| Accion | `OrcaAction` |
| Resultado | `OrcaResult` |
| Ejecucion de evento | `OrcaRun Result ` |
| Token semantico | `OrcaToken` |
El alias publico previsto seria:
@ -170,15 +170,15 @@ coordinar varios artefactos cuando ocurre algo, registra acciones en `orca`.
`orca` toma ideas de varios ecosistemas, pero no copia ninguno:
| 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 |
| 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 |
Ideas concretas que conviene robar:
@ -233,7 +233,7 @@ Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
La accion es la que decide llamar a `App.cache` , `App.perm` ,
`App.connections` o cualquier otro servicio.
## Setup Tipado *(roadmap v2)*
## Setup Tipado _(roadmap v2)_
> No implementado. Ver "Roadmap v2" más abajo.
@ -296,25 +296,24 @@ Una accion es una unidad de trabajo asociada a un evento.
interface OrcaAction< TPayload = unknown , TValue = unknown > {
readonly id: OrcaActionId;
readonly stage: OrcaStage;
readonly priority?: number;
readonly execution?: OrcaExecutionMode;
readonly after?: readonly OrcaToken[];
readonly unless?: readonly OrcaToken[];
readonly abortOn?: readonly OrcaToken[];
readonly provides?: readonly OrcaToken[];
readonly transaction?: OrcaTransactionMode;
readonly actionTimeoutMs?: number;
readonly tokenTimeoutMs?: number;
readonly onError?: OrcaErrorPolicy;
readonly onFatal?: OrcaFatalPolicy;
readonly onTimeout?: OrcaTimeoutPolicy;
action(
payload: TPayload,
context: OrcaActionContext
): OrcaMaybePromise< OrcaResult < TValue > >;
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< TPayload , void > ; // rollback si el run aborta tras su éxito
readonly action: OrcaActionFn< TPayload , TValue > ;
}
```
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).
Una accion debe ser:
- nombrada con constante
@ -327,8 +326,9 @@ Una accion debe ser:
## Stages
Los stages dan estructura al pipeline. La prioridad solo ordena dentro del
stage; no debe sustituir a los 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` .
```txt
guard valida precondiciones
@ -353,32 +353,25 @@ export const ORCA_STAGE_FINALLY = 'finally' as const;
`finally` debe poder ejecutarse aunque el pipeline haya abortado, salvo que el
orquestador haya sido disposed.
## Execution Modes
Una accion puede declarar como se integra en el stage:
```ts
export const ORCA_EXEC_SYNC = 'sync' as const;
export const ORCA_EXEC_ASYNC = 'async' as const;
export const ORCA_EXEC_PARALLEL = 'parallel' as const;
```
## Concurrencia dentro del stage (`parallel`)
Semantica propuesta:
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.
| Modo | Semantica |
| ---- | --------- |
| `sync` | Ejecuta y resuelve antes de continuar. |
| `async` | Puede esperar promesas, pero bloquea su grupo/stage. |
| `parallel` | Puede ejecutarse junto a otras acciones listas del mismo stage. |
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.
El motor debe decidir grupos de ejecucion usando:
- stage
- priority
- tokens disponibles
- dependencias pendientes
- modo de ejecucion
- limites de concurrencia futuros
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).
## Concurrencia De Runs
@ -391,20 +384,20 @@ decisión es parte del contrato del evento y se configura con
Constantes vivas (`consts.ts`):
```ts
ORCA_QUEUE_FIFO // 'fifo'
ORCA_QUEUE_REPLACE_QUEUED // 'replace-queued'
ORCA_QUEUE_DROP_LATEST // 'drop-latest'
ORCA_QUEUE_PARALLEL // 'parallel'
ORCA_QUEUE_FIFO; // 'fifo'
ORCA_QUEUE_REPLACE_QUEUED; // 'replace-queued'
ORCA_QUEUE_DROP_LATEST; // 'drop-latest'
ORCA_QUEUE_PARALLEL; // 'parallel'
```
Semántica:
| Modo | Uso |
| ---- | --- |
| `fifo` (default) | Cada evento encola un run; los runs no-paralelos se serializan globalmente. Garantía simple. |
| 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. |
| `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. |
`replace-queued` deja el run activo terminar (FINALLY incluido) — la
variante "fuerte" que aborta in-flight (`replace-current`, takeLatest)
@ -544,21 +537,21 @@ interno antes de llegar a la cola de runs:
```ts
interface OrcaEnvelope< TPayload > {
readonly event: string;
readonly payload: TPayload;
readonly meta: OrcaEventMeta;
readonly event: string;
readonly payload: TPayload;
readonly meta: OrcaEventMeta;
}
interface OrcaEventMeta {
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly parentRunId?: OrcaRunId;
readonly emittedByAction?: OrcaActionId;
readonly depth: number;
readonly stack: readonly string[];
readonly publishedAt: number;
readonly dedupeKey?: string;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly parentRunId?: OrcaRunId;
readonly emittedByAction?: OrcaActionId;
readonly depth: number;
readonly stack: readonly string[];
readonly publishedAt: number;
readonly dedupeKey?: string;
}
```
@ -570,21 +563,17 @@ para esa ejecución:
```ts
interface OrcaActionContext {
readonly runId: OrcaRunId;
readonly event: string;
readonly stage: OrcaStage;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly depth: number;
readonly tokens: ReadonlySet< OrcaToken > ;
readonly signal: AbortSignal;
readonly logger: Logger;
emit< TPayload > (
event: string,
payload: TPayload,
options?: OrcaEmitOptions
): OrcaEventId | null;
readonly runId: OrcaRunId;
readonly event: string;
readonly stage: OrcaStage;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly depth: number;
readonly tokens: ReadonlySet< OrcaToken > ;
readonly signal: AbortSignal;
readonly logger: Logger;
emit< TPayload > (event: string, payload: TPayload, options?: OrcaEmitOptions): OrcaEventId | null;
}
```
@ -628,13 +617,13 @@ main(action A)
```ts
interface OrcaReentryOptions {
readonly maxDepth?: number; // default 16
readonly maxEventsPerTrace?: number; // default 128
readonly repeatedEventLimit?: number; // default 2
readonly repeatedEventPolicy?:
| typeof ORCA_REENTRY_SKIP // default
| typeof ORCA_REENTRY_ABORT_TRACE
| typeof ORCA_REENTRY_ERROR;
readonly maxDepth?: number; // default 16
readonly maxEventsPerTrace?: number; // default 128
readonly repeatedEventLimit?: number; // default 2
readonly repeatedEventPolicy?:
| typeof ORCA_REENTRY_SKIP // default
| typeof ORCA_REENTRY_ABORT_TRACE
| typeof ORCA_REENTRY_ERROR;
}
```
@ -759,13 +748,15 @@ export const ORCA_ON_ERROR_CONTINUE = 'continue' as const;
export const ORCA_ON_ERROR_ABORT_ACTION = 'abort-action' as const;
export const ORCA_ON_ERROR_ABORT_STAGE = 'abort-stage' as const;
export const ORCA_ON_ERROR_ABORT_RUN = 'abort-run' as const;
export const ORCA_ON_FATAL_ABORT_RUN = 'abort-run' as const;
export const ORCA_ON_FATAL_DISPOSE = 'dispose' as const;
```
`fatal` debe tener semantica fuerte: por defecto aborta el run. Seguir tras un
fatal solo debe permitirse con politica explicita y muy justificada.
`ABORT_ACTION` y `ABORT_STAGE` se aceptan por compat pero hoy se comportan como
`CONTINUE` ; solo `CONTINUE` (default) y `ABORT_RUN` son distintos.
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_*` .
### Rollback Y Compensacion
@ -780,50 +771,41 @@ La reparacion de estado se hace con:
- acciones idempotentes
- acciones de compensacion explicitas
Contrato futuro para co mpensaciones:
Compensaciones (implementadas) :
```ts
interface OrcaAction< TPayload = unknown , TValue = unknown > {
action(
payload: TPayload,
context: OrcaActionContext
): OrcaMaybePromise< OrcaResult < TValue > >;
compensate?(
payload: TPayload,
context: OrcaCompensationContext
): OrcaMaybePromise< OrcaResult > ;
readonly action: OrcaActionFn< TPayload , TValue > ;
readonly compensate?: OrcaActionFn< TPayload , void > ;
}
type OrcaActionFn< TPayload , TValue > = (
payload: TPayload,
context: OrcaActionContext
) => OrcaResult< TValue > | Promise< OrcaResult < TValue > >;
```
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.
## Politicas De Timeout
## Timeouts
Los timeouts se declaran en `orca` , pero los ejecuta `timer` .
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.
Niveles previstos:
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.
| Timeout | Descripcion |
| ------- | ----------- |
| `runTimeoutMs` | Tiempo maximo para todo el pipeline del evento. |
| `stageTimeoutMs` | Tiempo maximo para un stage. |
| `actionTimeoutMs` | Tiempo maximo de una accion. |
| `tokenTimeoutMs` | Tiempo maximo esperando tokens. |
| `idleTimeoutMs` | Tiempo maximo sin progreso del pipeline. |
Politicas:
```ts
export const ORCA_ON_TIMEOUT_CONTINUE = 'continue' as const;
export const ORCA_ON_TIMEOUT_ABORT_ACTION = 'abort-action' as const;
export const ORCA_ON_TIMEOUT_ABORT_STAGE = 'abort-stage' as const;
export const ORCA_ON_TIMEOUT_ABORT_RUN = 'abort-run' as const;
export const ORCA_ON_TIMEOUT_FATAL = 'fatal' as const;
```
`orca` nunca debe usar `Date.now()` ni `setTimeout()` directamente. Debe usar
`orca` nunca usa `Date.now()` ni `setTimeout()` directamente: siempre el
`TimerScheduler` de `timer` .
## Timers
@ -839,12 +821,10 @@ const Orca = createEngineOrca({
});
```
Las keys de timer deben generarse con helpers constantes, no con strings
dispersos:
```ts
orcaTimerKey(runId, stage, actionId, ORCA_TIMER_ACTION_TIMEOUT);
```
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.
Si `orca` crea timers sobre un scheduler inyectado, no es propietario del
scheduler. `Orca.dispose()` cancela los timers registrados por `orca` , pero no
@ -852,36 +832,33 @@ destruye `App.timers`.
## Transacciones
`orca` no implementa una transaccion concreta. Acepta un puerto:
```ts
interface OrcaTransactionPort {
run< T > (work: () => Promise< T > ): Promise< T > ;
}
```
Modos:
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.
```ts
export const ORCA_TX_NONE = 'none' as const;
export const ORCA_TX_OPTIONAL = 'optional' as const;
export const ORCA_TX_REQUIRED = 'required' as const;
export const ORCA_TX_REQUIRES_NEW = 'requires-new' as const;
Orca.onEvent('checkout.submit', {
id: ORCA_ACTION_RESERVE_STOCK,
stage: ORCA_STAGE_MAIN,
transaction: 'checkout',
action: reserveStock,
compensate: releaseStock // se invoca en rollback si otro miembro falla
});
```
En cliente, una transaccion puede ser un batch de UI/cache/overlays. En futuro
`active-server` , puede mapear a una transaccion real de DB/outbox.
Regla:
```txt
orca coordina la frontera transaccional; el adapter ejecuta la transaccion.
```
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.
En v0, `transaction` puede existir como contrato conceptual, pero no debe
prometer rollback universal. Si no hay `OrcaTransactionPort` , una
accion con `ORCA_TX_REQUIRED` debe fallar al registrarse o al validar la
configuracion, no degradar silenciosamente a `none` .
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` .
## Run Result
@ -976,7 +953,7 @@ registre acciones.
Nombre recomendado en `App` :
```ts
App.Orchestration
App.Orchestration;
```
`orca` queda como nombre del artefacto, alias/import y prefijo de constantes
@ -1153,7 +1130,7 @@ on bus event:
find actions whose after tokens are satisfied
skip actions whose unless tokens are present
block/abort actions whose abortOn tokens are present
run ready actions according to priority and execution mode
run ready actions in registration order (parallel waves via Promise.all)
collect Result
add emitted tokens
apply error/fatal/timeout policies
@ -1188,25 +1165,25 @@ diagnostics catalogada. Las claves vivas (`ORCA_DIAGNOSTIC_EVENTS` en
`consts.ts` ):
```ts
'orca.run.started'
'orca.run.completed'
'orca.run.aborted'
'orca.action.started'
'orca.action.completed'
'orca.action.failed'
'orca.action.fatal'
'orca.action.timeout'
'orca.action.skipped'
'orca.action.blocked'
'orca.action.interrupted'
'orca.compensation.started'
'orca.compensation.completed'
'orca.compensation.failed'
'orca.event.emitted'
'orca.reentry.blocked'
'orca.trace.aborted'
'orca.queue.dropped'
'orca.configuration.invalid'
'orca.run.started';
'orca.run.completed';
'orca.run.aborted';
'orca.action.started';
'orca.action.completed';
'orca.action.failed';
'orca.action.fatal';
'orca.action.timeout';
'orca.action.skipped';
'orca.action.blocked';
'orca.action.interrupted';
'orca.compensation.started';
'orca.compensation.completed';
'orca.compensation.failed';
'orca.event.emitted';
'orca.reentry.blocked';
'orca.trace.aborted';
'orca.queue.dropped';
'orca.configuration.invalid';
```
Cada evento carga meta con `runId` / `eventId` / `traceId` / `depth`