Implements the v0-kernel design captured in docs/orca_minds.txt: the
engine now carries every event through an internal envelope, exposes
trace identity to actions, and bounds derived-event chains through
configurable reentry guards. The previous engine was effectively a
callback runner with stages; this commit turns it into a runtime that
can answer "where does this event come from, how deep is it, and when
should I stop?" without polluting user-defined payloads with runtime
metadata.
New public surface:
- OrcaEnvelope<TPayload> + OrcaEventMeta (eventId, traceId,
parentEventId, parentRunId, emittedByAction, depth, stack,
publishedAt, dedupeKey)
- OrcaActionContext gains eventId / traceId / parentEventId / depth
plus emit(event, payload, options?) -> OrcaEventId | null
- OrcaResult adds OrcaInterrupted (with orcaInterrupted() helper)
- OrcaActionRun.status and OrcaRunResult.status add 'interrupted'
- OrcaRunResult exposes eventId / traceId / parentEventId / depth
- OrcaReentryOptions on EngineOrcaOptions: maxDepth (16),
maxEventsPerTrace (128), repeatedEventLimit (2),
repeatedEventPolicy (skip / abort-trace / error)
- Constants for reentry policies and reasons; OrcaReentryError class
Engine semantics:
- Bus publishes are roots: fresh traceId, depth=0, no parent. They
never enter the reentry counters.
- ctx.emit() builds a child envelope inheriting the parent's traceId
and incrementing depth. The child is enqueued, never executed
inline.
- Reentry guards apply only to derived envelopes. Crossing maxDepth,
maxEventsPerTrace, repeatedEventLimit, or matching a previous
dedupeKey triggers the configured policy. Skip blocks just that
envelope; abort-trace marks the trace and skips every queued event
that belongs to it; error throws OrcaReentryError synchronously.
- dispose() marks every live trace aborted with reason 'disposed' so
late ctx.emit() calls (e.g. from compensating cleanup) get a clean
rejection instead of an exception.
Diagnostics gain four new event types
(action.interrupted, event.emitted, reentry.blocked, trace.aborted)
and every existing one carries the envelope identifiers
(runId, eventId, traceId, parentEventId, depth) where applicable, so a
log sink can correlate runs without parsing variant tags.
Tests: 10 new tests covering envelope identity (root depth=0, child
depth+1), ctx.emit() trace inheritance, run-trace correlation, orphan
emit, all four reentry guards (maxDepth with disjoint event names so
the same-name limit doesn't interfere, repeatedEventLimit, error
policy throwing OrcaReentryError, abort-trace, dedupeKey), and the
invariant that bus publishes start fresh traces.
Active-app presets keep working unchanged (they don't call ctx.emit
yet); the API extension is additive on the OrcaActionContext side
(presets still type-check against the wider context shape).
Total: 1357/1357 vitest tests pass (47 in orca, +10 from this commit).
Roadmap v1 documented at the foot of the orca README and parked under
@v1+ in the source: actionTimeoutMs runtime, compensate invocation,
after/unless/abortOn gating, OrcaFatal distinction, queue policies
(commit/replace/parallel), transaction/atomic groups, payload-bearing
tokens, fan-in, validate()/commit() static graph validation,
createActiveOrca() reactive wrapper, and the bus.publish interception
(Option B) that would let modules' direct publishes attach
emittedByAction perfectly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
parent
1515df1be3
commit
feacd46c62
@ -0,0 +1,691 @@
|
||||
Clippy-pet verdict: `orca` es una ballena preciosa, pero si intentamos montarla entera en v0 nos hunde la barca.
|
||||
|
||||
Lo correcto:
|
||||
|
||||
```txt
|
||||
v0 = kernel
|
||||
v1 = enterprise orchestration
|
||||
```
|
||||
|
||||
`orca` está bien pensado porque tiene tres aciertos enormes:
|
||||
|
||||
- Desacopla módulos: `orca` no sabe qué es `cach`, `perm`, `sess` o `connection`.
|
||||
- Convierte flujos críticos en artefactos trazables: `OrcaRunResult`.
|
||||
- Usa tokens como hechos semánticos per-run, que es la parte diferencial.
|
||||
|
||||
Pero ahora mismo el README mezcla kernel y futuro. Para que sea implementable, v0 debería recortarse así:
|
||||
|
||||
```txt
|
||||
entra en v0:
|
||||
setupOrca
|
||||
createEngineOrca
|
||||
App.Orchestration inerte
|
||||
onEvent
|
||||
stages
|
||||
queue/drop
|
||||
tokens per-run flag
|
||||
after/provides
|
||||
OrcaResult success/skipped/error/fatal/interrupted
|
||||
OrcaRunResult
|
||||
validate()
|
||||
diagnostics
|
||||
|
||||
sale de v0:
|
||||
commit()
|
||||
replace
|
||||
timeouts
|
||||
transaction
|
||||
compensate
|
||||
parallel
|
||||
abortOn/unless avanzados si complican
|
||||
tokens con payload
|
||||
fan-in
|
||||
```
|
||||
|
||||
La pregunta decisiva no es “¿está bien diseñado?”. Sí, lo está.
|
||||
|
||||
La pregunta correcta es: **¿cuál es el núcleo mínimo que demuestra que `orca` merece existir?**
|
||||
|
||||
Respuesta: `event -> staged actions -> tokens -> result trace`.
|
||||
|
||||
Si eso funciona, lo demás se puede crecer con calma.
|
||||
|
||||
|
||||
como evitamos la reentrada en orca , si no establecemos en el payload del evento los eventos que le preceden en el stack ? al disparar el componente verifica si esta gestionando ya un evento de ese stack y lo omitiria , como lo suelen hacer el evitar loops infinitos ?
|
||||
|
||||
|
||||
La forma sana no es meter “eventos precedentes” en el payload de negocio. Eso contaminaría todos los eventos con metadata de orquestación y acabaría mezclando dominio con runtime.
|
||||
|
||||
Lo correcto es que `orca` envuelva cada evento en un **OrcaEnvelope** interno:
|
||||
|
||||
```ts
|
||||
interface OrcaEnvelope<TPayload = unknown> {
|
||||
readonly event: string;
|
||||
readonly payload: TPayload;
|
||||
readonly meta: {
|
||||
readonly eventId: string;
|
||||
readonly parentEventId?: string;
|
||||
readonly runId?: string;
|
||||
readonly traceId: string;
|
||||
readonly depth: number;
|
||||
readonly stack: readonly string[];
|
||||
readonly emittedBy?: OrcaActionId;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
El payload sigue limpio:
|
||||
|
||||
```ts
|
||||
{ previousActorId, nextActorId }
|
||||
```
|
||||
|
||||
Y `orca` conserva el contexto:
|
||||
|
||||
```ts
|
||||
{
|
||||
event: APP_EVENT_CACHE_CLEARED,
|
||||
meta: {
|
||||
parentEventId: 'evt-1',
|
||||
traceId: 'trace-7',
|
||||
depth: 2,
|
||||
stack: [
|
||||
APP_EVENT_USER_IDENTITY_CHANGED,
|
||||
APP_EVENT_CACHE_CLEARED
|
||||
],
|
||||
emittedBy: ORCA_ACTION_CLEAR_PRIVATE_CACHE
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Entonces la prevención de loops vive en `orca`, no en los módulos.
|
||||
|
||||
Reglas típicas para evitar loops infinitos:
|
||||
|
||||
```txt
|
||||
1. No ejecutar inline eventos emitidos durante un run.
|
||||
2. Encolarlos con traceId y parentEventId.
|
||||
3. Mantener stack interno por trace.
|
||||
4. Rechazar o saltar eventos si superan maxDepth.
|
||||
5. Rechazar o saltar si el mismo evento aparece demasiado en el mismo trace.
|
||||
6. Aplicar dedupeKey opcional.
|
||||
7. Tener maxEventsPerTrace.
|
||||
8. Tener diagnostics cuando se corta el loop.
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```ts
|
||||
const Orca = createEngineOrca({
|
||||
bus,
|
||||
timers,
|
||||
logger,
|
||||
reentry: {
|
||||
maxDepth: 16,
|
||||
maxEventsPerTrace: 128,
|
||||
repeatedEventPolicy: ORCA_REENTRY_SKIP,
|
||||
repeatedEventLimit: 2
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Si ocurre esto:
|
||||
|
||||
```txt
|
||||
A -> action publica B
|
||||
B -> action publica A
|
||||
A -> action publica B
|
||||
...
|
||||
```
|
||||
|
||||
`orca` ve el trace:
|
||||
|
||||
```txt
|
||||
A, B, A, B, A
|
||||
```
|
||||
|
||||
Y puede cortar:
|
||||
|
||||
```ts
|
||||
ORCA_RESULT_INTERRUPTED
|
||||
ORCA_REENTRY_REASON_REPEATED_EVENT
|
||||
```
|
||||
|
||||
o marcar el run como:
|
||||
|
||||
```ts
|
||||
ORCA_RUN_ABORTED
|
||||
```
|
||||
|
||||
Los patrones habituales en otros sistemas:
|
||||
|
||||
- Redux middleware suele evitar loops por convención y con guards manuales.
|
||||
- Redux-Saga usa `takeLatest`, cancelación y control de tasks.
|
||||
- EventEmitter no te protege: si emites recursivo, te comes el loop.
|
||||
- State machines evitan loops por transición/estado: si ya estás en cierto estado, el evento no transiciona.
|
||||
- Sistemas workflow usan `trace/correlationId`, depth limits, idempotency keys y dedupe.
|
||||
|
||||
Para `orca`, yo fijaría esto:
|
||||
|
||||
```txt
|
||||
payload = dominio, limpio
|
||||
envelope/meta = runtime, trace, stack, depth
|
||||
tokens = coordinación intra-run
|
||||
bus events = comunicación inter-run
|
||||
```
|
||||
|
||||
Y añadiría al contrato v0:
|
||||
|
||||
```ts
|
||||
interface OrcaReentryOptions {
|
||||
readonly maxDepth?: number; // default 16
|
||||
readonly maxEventsPerTrace?: number; // default 128
|
||||
readonly repeatedEventLimit?: number; // default 1 o 2
|
||||
readonly repeatedEventPolicy?:
|
||||
| typeof ORCA_REENTRY_SKIP
|
||||
| typeof ORCA_REENTRY_ABORT_TRACE
|
||||
| typeof ORCA_REENTRY_ERROR;
|
||||
}
|
||||
```
|
||||
|
||||
Así evitas reentrada sin ensuciar los payloads y sin obligar a cada módulo/componente a recordar “quién vino antes”.
|
||||
|
||||
|
||||
cual estrategia es mas arquitectonicamente mejor ?
|
||||
|
||||
|
||||
Arquitectónicamente, la mejor estrategia es:
|
||||
|
||||
```txt
|
||||
payload limpio + envelope runtime interno + cola de orca + límites de reentrada
|
||||
```
|
||||
|
||||
No metería el stack en el payload del evento.
|
||||
|
||||
Por qué:
|
||||
|
||||
- El payload pertenece al dominio: `userId`, `tenantId`, `orderId`, etc.
|
||||
- El stack pertenece al runtime de orquestación.
|
||||
- Si mezclas ambos, todos los módulos empiezan a conocer detalles de `orca`.
|
||||
- Si mañana cambias cómo trazas reentrada, rompes contratos de eventos.
|
||||
- Los eventos pueden venir de `connection`, `auth`, `sess`, `http`, etc.; no todos deberían cargar metadata de runtime.
|
||||
|
||||
La arquitectura correcta sería:
|
||||
|
||||
```txt
|
||||
buss event:
|
||||
type + payload limpio
|
||||
|
||||
orca internal envelope:
|
||||
eventId
|
||||
traceId
|
||||
parentEventId
|
||||
runId
|
||||
depth
|
||||
stack
|
||||
emittedByAction
|
||||
dedupeKey
|
||||
```
|
||||
|
||||
Flujo:
|
||||
|
||||
```txt
|
||||
1. buss publica EVENT_A(payload)
|
||||
2. orca crea envelope A con traceId
|
||||
3. action de A publica EVENT_B
|
||||
4. orca no ejecuta B inline
|
||||
5. orca crea envelope B con parent=A, mismo traceId, depth+1
|
||||
6. orca comprueba stack/repeated/depth
|
||||
7. si es seguro, encola B
|
||||
8. si detecta loop, corta con diagnostic
|
||||
```
|
||||
|
||||
Esto es más limpio que “cada componente mira si ya está gestionando un evento de ese stack”, porque esa opción reparte lógica de orquestación por toda la app. Es justo lo que `orca` intenta evitar.
|
||||
|
||||
Estrategia recomendada para v0:
|
||||
|
||||
```ts
|
||||
createEngineOrca({
|
||||
bus,
|
||||
logger,
|
||||
reentry: {
|
||||
maxDepth: 16,
|
||||
maxEventsPerTrace: 128,
|
||||
repeatedEventLimit: 2,
|
||||
repeatedEventPolicy: ORCA_REENTRY_SKIP
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Reglas:
|
||||
|
||||
- Eventos publicados durante un run se encolan, no se ejecutan inline.
|
||||
- El stack es interno de `orca`.
|
||||
- Si el mismo evento aparece demasiadas veces en el mismo trace, se salta o aborta.
|
||||
- Si depth supera límite, aborta el trace.
|
||||
- Si eventsPerTrace supera límite, aborta el trace.
|
||||
- Se emite diagnostic estructurado.
|
||||
- El `OrcaRunResult` registra `traceId`, `parentEventId`, `depth`, `interruptedReason`.
|
||||
|
||||
Mi voto: **envelope interno + queue + trace guards**. Es la opción más limpia, menos acoplada y más mantenible.
|
||||
|
||||
|
||||
|
||||
me refiero a los otros sistemas de referencia
|
||||
|
||||
|
||||
|
||||
En los sistemas de referencia, la estrategia arquitectónicamente mejor **no es meter el stack en el payload**. Casi todos los sistemas buenos separan:
|
||||
|
||||
```txt
|
||||
payload = datos de negocio
|
||||
runtime context = control, cancelación, stack, tracing, concurrencia
|
||||
```
|
||||
|
||||
**Comparativa**
|
||||
| Sistema | Cómo evita loops/reentrada | Lectura para `orca` |
|
||||
|---|---|---|
|
||||
| RTK Listener Middleware | Usa runtime API: `signal`, `cancelActiveListeners()`, `cancel()`, `take`, `condition`, `delay`, `pause`, `fork`. No mete stack en la action. | Muy buen referente. `orca` debería pasar `signal`, `traceId`, `runId` y control de cancelación en `OrcaActionContext`. Fuente: [RTK listener middleware](https://redux-toolkit.js.org/api/createListenerMiddleware). |
|
||||
| Redux-Saga | Usa tareas y cancelación: `takeLatest` cancela la tarea anterior; `takeEvery` permite concurrencia; `race`, `cancel`, `fork`, `join`. | Bueno para políticas `queue/drop/replace/parallel`, pero no copiaría generators. Fuente: [Redux-Saga concurrency](https://redux-saga.js.org/docs/advanced/Concurrency/). |
|
||||
| XState | Evita loops mediante estado, guards y transiciones. Si ya estás en un estado, un evento puede no transicionar. El control vive en la máquina, no en el payload. | Útil para flows cerrados; para `orca`, copiaría `setup()` tipado y la idea de runtime/state como dueño del control. Fuente: [XState setup](https://stately.ai/docs/setup). |
|
||||
| Effect Workflow | Distingue error/cause/interruption y tiene compensación estructurada. El runtime sabe qué falló y cómo compensar; no lo mete en payload. | Muy buen referente para `OrcaInterrupted`, compensaciones futuras y `Cause`. Fuente: [Effect Workflow compensation](https://effect-ts.github.io/effect/workflow/Workflow.ts.html). |
|
||||
| EventEmitter simple | No evita loops. Si un listener emite el mismo evento recursivamente, problema del usuario. | Esto es justo lo que `orca` no debe ser. |
|
||||
|
||||
**Veredicto**
|
||||
La mejor estrategia para `orca` es una mezcla de RTK Listener + Saga + XState:
|
||||
|
||||
```txt
|
||||
payload limpio
|
||||
OrcaEnvelope interno
|
||||
cola propia de orca
|
||||
traceId / parentEventId / runId
|
||||
AbortSignal en OrcaActionContext
|
||||
política de concurrencia por evento
|
||||
límites maxDepth / maxEventsPerTrace
|
||||
diagnostics cuando corta loops
|
||||
```
|
||||
|
||||
No pondría el stack en el payload. Eso es arquitectura más débil porque obliga a todos los módulos a conocer el runtime de `orca`.
|
||||
|
||||
La estrategia superior es:
|
||||
|
||||
```ts
|
||||
interface OrcaActionContext {
|
||||
readonly runId: OrcaRunId;
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly parentEventId?: OrcaEventId;
|
||||
readonly depth: number;
|
||||
readonly signal: AbortSignal;
|
||||
}
|
||||
```
|
||||
|
||||
Y el envelope interno:
|
||||
|
||||
```ts
|
||||
interface OrcaEnvelope<TPayload> {
|
||||
readonly event: string;
|
||||
readonly payload: TPayload;
|
||||
readonly meta: OrcaEventMeta;
|
||||
}
|
||||
```
|
||||
|
||||
Así `orca` se parece a los sistemas buenos: el **runtime** controla reentrada, cancelación y concurrencia; el **payload** sigue siendo dominio limpio.
|
||||
|
||||
|
||||
ahora mismo no se parece a eso en nada ?
|
||||
|
||||
|
||||
|
||||
|
||||
Correcto: **el diseño actual apunta en esa dirección, pero todavía no se parece lo suficiente en contrato concreto**.
|
||||
|
||||
Ahora mismo el README ya dice cosas buenas:
|
||||
|
||||
```txt
|
||||
payload limpio
|
||||
no ejecutar inline
|
||||
queue/drop/replace
|
||||
tokens per-run
|
||||
RunResult
|
||||
App.Orchestration inerte
|
||||
```
|
||||
|
||||
Pero faltan las piezas que lo harían parecerse de verdad a RTK/Saga/XState/Effect en lo importante:
|
||||
|
||||
```txt
|
||||
OrcaEnvelope interno
|
||||
OrcaActionContext con traceId/runId/eventId/depth
|
||||
AbortSignal en acciones
|
||||
cola interna explícita de orca
|
||||
políticas de reentrada: maxDepth, maxEventsPerTrace
|
||||
reason/result para reentry-skip / interrupted
|
||||
relación formal buss -> orca listener -> queue
|
||||
```
|
||||
|
||||
Así que mi diagnóstico es:
|
||||
|
||||
```txt
|
||||
Conceptualmente: sí está en el camino correcto.
|
||||
Contractualmente: todavía no está cerrado.
|
||||
Implementablemente: aún tendría ambigüedad peligrosa.
|
||||
```
|
||||
|
||||
La sección que habría que añadir al README antes de implementar es algo como:
|
||||
|
||||
```ts
|
||||
interface OrcaEnvelope<TPayload = unknown> {
|
||||
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[];
|
||||
}
|
||||
|
||||
interface OrcaActionContext {
|
||||
readonly runId: OrcaRunId;
|
||||
readonly event: string;
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly depth: number;
|
||||
readonly signal: AbortSignal;
|
||||
readonly emit: <TPayload>(event: string, payload: TPayload) => void;
|
||||
}
|
||||
```
|
||||
|
||||
Y reglas:
|
||||
|
||||
```txt
|
||||
1. Las acciones no llaman bus.publish directamente si quieren preservar trace.
|
||||
2. Usan ctx.emit() para eventos derivados.
|
||||
3. ctx.emit() crea envelope hijo.
|
||||
4. Eventos derivados se encolan, no inline.
|
||||
5. Orca corta loops con maxDepth/maxEventsPerTrace/repeatedEventLimit.
|
||||
6. Si una acción llama App.Bus directamente, orca lo trata como evento raíz nuevo.
|
||||
```
|
||||
|
||||
Esto último es importante: **si queremos trazabilidad real, las acciones deben emitir eventos derivados vía `ctx.emit()`**, no vía `App.Bus.publish()` directamente. Ahí está la diferencia entre “tenemos bus” y “tenemos runtime de orquestación”.
|
||||
|
||||
Así que sí: ahora mismo aún no está del todo. Tiene el esqueleto filosófico, pero le falta el contrato de runtime/envelope/context para ser una pieza robusta.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ctx deberiamos definirlo, seria otra capa mas ?
|
||||
|
||||
|
||||
|
||||
|
||||
Sí, `ctx` hay que definirlo. Pero no lo vería como “otra capa” al nivel de `buss`/`orca`/`timr`; lo vería como **el runtime context de una ejecución de `orca`**.
|
||||
|
||||
No es un artefacto nuevo. Es parte del contrato de `orca`.
|
||||
|
||||
```txt
|
||||
orca = engine
|
||||
run = ejecución de un evento
|
||||
ctx = contexto que orca entrega a cada action dentro de ese run
|
||||
```
|
||||
|
||||
El `ctx` es donde ponemos todo lo que no debe ir en el payload:
|
||||
|
||||
```ts
|
||||
interface OrcaActionContext {
|
||||
readonly runId: OrcaRunId;
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly parentEventId?: OrcaEventId;
|
||||
readonly depth: number;
|
||||
|
||||
readonly signal: AbortSignal;
|
||||
|
||||
emit<TPayload>(
|
||||
event: string,
|
||||
payload: TPayload,
|
||||
options?: OrcaEmitOptions
|
||||
): void;
|
||||
|
||||
token(token: OrcaToken): void;
|
||||
|
||||
hasToken(token: OrcaToken): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
La separación queda:
|
||||
|
||||
```txt
|
||||
payload
|
||||
Datos del evento.
|
||||
Pertenece al dominio.
|
||||
|
||||
ctx
|
||||
Datos/control de ejecución.
|
||||
Pertenece a orca.
|
||||
|
||||
App modules
|
||||
Servicios reales.
|
||||
La action los cierra por closure.
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```ts
|
||||
App.Orchestration.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
|
||||
id: ORCA_ACTION_CLEAR_PRIVATE_CACHE,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
provides: [ORCA_TOKEN_CACHE_OK, ORCA_TOKEN_CACHE_ERROR],
|
||||
|
||||
action: async (payload, ctx) => {
|
||||
if (ctx.signal.aborted) {
|
||||
return orcaInterrupted('run aborted');
|
||||
}
|
||||
|
||||
const result = await App.Cache.clearActorScope(payload.previousActorId);
|
||||
|
||||
if (!result.ok) {
|
||||
return orcaError(result.error, {
|
||||
emits: [ORCA_TOKEN_CACHE_ERROR]
|
||||
});
|
||||
}
|
||||
|
||||
ctx.emit(APP_EVENT_PRIVATE_CACHE_CLEARED, {
|
||||
actorId: payload.previousActorId
|
||||
});
|
||||
|
||||
return orcaSuccess({
|
||||
emits: [ORCA_TOKEN_CACHE_OK]
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`ctx.emit()` es importante porque preserva traza:
|
||||
|
||||
```txt
|
||||
run A
|
||||
action clear-cache
|
||||
ctx.emit(B)
|
||||
|
||||
orca crea envelope B:
|
||||
traceId = A.traceId
|
||||
parentEventId = A.eventId
|
||||
emittedByAction = clear-cache
|
||||
depth = A.depth + 1
|
||||
```
|
||||
|
||||
Si la action llama directamente:
|
||||
|
||||
```ts
|
||||
App.Bus.publish(B, payload)
|
||||
```
|
||||
|
||||
entonces `orca` no sabe que B viene de A salvo que intercepte todo, lo cual es más frágil. Por eso yo pondría esta regla:
|
||||
|
||||
```txt
|
||||
Dentro de acciones orca, usa ctx.emit() para eventos derivados.
|
||||
Usa App.Bus.publish() solo para eventos raíz externos al run.
|
||||
```
|
||||
|
||||
Entonces no es una capa adicional. Es el equivalente a:
|
||||
|
||||
- `listenerApi` en RTK listener middleware.
|
||||
- `saga effects` en redux-saga.
|
||||
- `context/event` en XState.
|
||||
- `workflow context/cause` en Effect/Temporal.
|
||||
|
||||
Mi recomendación: sí, definir `OrcaActionContext` como contrato central de v0. Sin `ctx`, `orca` será solo un callback runner. Con `ctx`, empieza a ser runtime de orquestación.
|
||||
|
||||
|
||||
|
||||
y como sabe una accion de un modulo a la hora de emitir un evento donde lo debe hacer, por ejemplo
|
||||
|
||||
auth.onChangeUser (newUser) {
|
||||
bus.publish(AUTH_CHANGE_EVENT,newUser)
|
||||
o
|
||||
ctx.publish(AUTH_CHANGE_EVENT,newUser)
|
||||
}
|
||||
|
||||
|
||||
|
||||
La regla tiene que ser muy clara:
|
||||
|
||||
```txt
|
||||
Un módulo nunca usa ctx.
|
||||
Una acción de orca sí puede usar ctx.
|
||||
```
|
||||
|
||||
Tu ejemplo:
|
||||
|
||||
```ts
|
||||
auth.onChangeUser((newUser) => {
|
||||
bus.publish(AUTH_CHANGE_EVENT, newUser);
|
||||
});
|
||||
```
|
||||
|
||||
Eso está bien si ocurre **dentro del módulo auth**. `auth` no debe saber que existe `orca`, por tanto no puede recibir ni usar `ctx`.
|
||||
|
||||
En cambio esto:
|
||||
|
||||
```ts
|
||||
auth.onChangeUser((newUser) => {
|
||||
ctx.publish(AUTH_CHANGE_EVENT, newUser);
|
||||
});
|
||||
```
|
||||
|
||||
Eso estaría mal si `auth.onChangeUser` es código del módulo. Meterías `orca` dentro de `auth`.
|
||||
|
||||
La separación correcta:
|
||||
|
||||
```txt
|
||||
Módulo -> Bus
|
||||
Orca action -> Ctx
|
||||
```
|
||||
|
||||
Ejemplo completo:
|
||||
|
||||
```ts
|
||||
// Dentro de auth
|
||||
function setUser(nextUser: User) {
|
||||
currentUser = nextUser;
|
||||
|
||||
bus.publish(AUTH_EVENT_USER_CHANGED, {
|
||||
userId: nextUser.id
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`auth` publica en `bus`. Punto. No conoce `orca`.
|
||||
|
||||
Ahora una acción de `orca`:
|
||||
|
||||
```ts
|
||||
App.Orchestration.onEvent(AUTH_EVENT_USER_CHANGED, {
|
||||
id: ORCA_ACTION_CLEAR_PRIVATE_STATE,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
|
||||
action: async (payload, ctx) => {
|
||||
await App.Cache.clearActorScope(payload.userId);
|
||||
|
||||
ctx.emit(APP_EVENT_PRIVATE_STATE_CLEARED, {
|
||||
userId: payload.userId
|
||||
});
|
||||
|
||||
return orcaSuccess({
|
||||
emits: [ORCA_TOKEN_CACHE_OK]
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Aquí sí usamos `ctx.emit()` porque estamos dentro de una action y queremos preservar trace.
|
||||
|
||||
Entonces hay dos tipos de emisión:
|
||||
|
||||
```txt
|
||||
bus.publish()
|
||||
Para módulos, componentes, servidor, código externo a orca.
|
||||
Crea evento raíz desde la perspectiva de orca.
|
||||
|
||||
ctx.emit()
|
||||
Solo dentro de una OrcaAction.
|
||||
Crea evento hijo dentro del trace actual.
|
||||
```
|
||||
|
||||
Si un módulo emite durante una acción, por ejemplo:
|
||||
|
||||
```ts
|
||||
action: async () => {
|
||||
await App.Auth.changeUser(nextUser); // Auth internamente hace bus.publish
|
||||
}
|
||||
```
|
||||
|
||||
`orca` lo verá como evento publicado durante un run, pero no sabrá con tanta precisión qué acción lo emitió salvo que tenga un contexto activo global interno. Hay dos opciones:
|
||||
|
||||
**Opción A, simple y limpia v0:**
|
||||
|
||||
```txt
|
||||
Módulo publica por bus.
|
||||
Orca detecta que hay run activo y lo encola como child event genérico.
|
||||
parentRunId sí, emittedByAction quizá no.
|
||||
```
|
||||
|
||||
**Opción B, más precisa:**
|
||||
|
||||
`orca` ejecuta cada action dentro de un “current action scope” interno. Si durante esa ejecución algún módulo hace `bus.publish`, `orca` intercepta el publish y lo marca como:
|
||||
|
||||
```ts
|
||||
emittedByAction: ORCA_ACTION_CLEAR_PRIVATE_STATE
|
||||
```
|
||||
|
||||
Eso requiere integración fuerte entre `buss` y `orca`.
|
||||
|
||||
Mi recomendación:
|
||||
|
||||
```txt
|
||||
v0:
|
||||
módulos siempre bus.publish
|
||||
actions pueden ctx.emit
|
||||
si un módulo publica durante un run, orca lo trata como evento derivado del run si puede detectarlo, pero no exigimos emittedByAction perfecto
|
||||
|
||||
v1:
|
||||
integración bus/orca con current action scope para trazabilidad completa
|
||||
```
|
||||
|
||||
Regla para desarrolladores:
|
||||
|
||||
```txt
|
||||
Si estás escribiendo código de módulo: usa bus.publish.
|
||||
Si estás escribiendo una OrcaAction: usa ctx.emit para eventos derivados del flujo.
|
||||
```
|
||||
|
||||
Así evitamos acoplar módulos a `orca`, pero damos a las acciones una vía correcta para preservar trazabilidad.
|
||||
|
||||
|
||||
Loading…
Reference in new issue