F1 — `ConnectionReconnectOptions.random?: () => number` lets callers inject a deterministic source for backoff jitter, threaded through `computeBackoffDelay`. Default remains `Math.random` so existing apps are unaffected. Test covers maxAttempts, the new random injection (jitter +max and -max clamped to minDelay), the disabled case and the disposed/intentional-close gate. F2 — `runAuth()` singleflight in the connection request runtime: when an auth round is in flight, every concurrent caller awaits the same promise, so only one auth frame goes on the wire. Closes the gap where `session.changed` + `session.external_changed` could land back-to-back and produce two auth frames. Tested at the request-runtime level with fake ack registry + sender (integration-level testing of this through the mock transport is timing-flaky and adds no extra coverage). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>master
parent
22aab00b7d
commit
15737e0f37
@ -0,0 +1,549 @@
|
||||
# Auditoria Codex - ecosistema Active + Orca
|
||||
|
||||
Fecha: 2026-05-04
|
||||
Workspace: `G:\dev\svelte\active`
|
||||
|
||||
## Alcance
|
||||
|
||||
Se audito el estado actual del ecosistema con foco en el nuevo modulo
|
||||
`src/arts/orca` y sus puntos de acoplamiento con:
|
||||
|
||||
- `src/arts/active-app`
|
||||
- `src/arts/bus`
|
||||
- `src/arts/session`
|
||||
- `src/arts/cache`
|
||||
- `src/arts/perm`
|
||||
- `src/arts/connection`
|
||||
- documentacion publica en `src/arts/*/README.md` y pagina activa generada.
|
||||
|
||||
No se modifico codigo existente. El arbol ya tenia cambios previos en:
|
||||
|
||||
- `src/arts/orca/consts.ts`
|
||||
- `src/arts/orca/result.ts`
|
||||
- `src/arts/orca/types.ts`
|
||||
|
||||
Esos cambios parecen corregir parte de la deriva de comentarios `v0.1+` respecto a
|
||||
timeout/fatal/gates/compensation, pero no cierran todos los problemas.
|
||||
|
||||
## Veredicto
|
||||
|
||||
`orca` esta bien orientado arquitectonicamente. La decision fuerte, y la que
|
||||
realmente eleva el ecosistema, es mover las reacciones inter-modulo a una capa de
|
||||
orquestacion declarativa situada en `active-app`, no dentro de `cache`, `perm`,
|
||||
`session` o `connection`. Eso reduce acoplamiento y permite testear flujos
|
||||
compuestos como objetos de ejecucion (`OrcaRunResult`).
|
||||
|
||||
El modulo, sin embargo, ya ha crecido mas rapido que su contrato. El motor actual
|
||||
implementa gates, timeouts de accion, compensacion, transacciones, payloads en
|
||||
tokens, fan-in y politicas de cola. La documentacion mezcla tres versiones a la
|
||||
vez: boceto conceptual, v0 inicial y estado real. Antes de seguir expandiendo
|
||||
`orca`, conviene congelar el contrato real y eliminar ambiguedades.
|
||||
|
||||
Estado de pruebas focalizadas:
|
||||
|
||||
```txt
|
||||
npx vitest run src/arts/orca/test src/arts/active-app/test/presets.test.ts
|
||||
|
||||
Test Files 4 passed
|
||||
Tests 169 passed
|
||||
```
|
||||
|
||||
## Hallazgos P1
|
||||
|
||||
### 1. Liberacion de trace no es segura con runs paralelos
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:928`
|
||||
- `src/arts/orca/engine-orca.ts:931`
|
||||
- `src/arts/orca/engine-orca.ts:935`
|
||||
|
||||
`maybeReleaseTrace()` borra `traceStates` si no quedan eventos en cola. El propio
|
||||
comentario reconoce que no sabe si hay runs en vuelo del mismo trace. Eso era
|
||||
defendible con ejecucion estrictamente secuencial, pero ya existe
|
||||
`ORCA_QUEUE_PARALLEL`. Si dos eventos derivados del mismo trace corren en
|
||||
paralelo, el primero que termine puede borrar el estado de trace mientras el otro
|
||||
sigue vivo.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Los contadores `maxDepth`, `maxEventsPerTrace`, `repeatedEventLimit` y
|
||||
`dedupeKey` pueden resetearse antes de tiempo.
|
||||
- Un loop por eventos derivados puede saltarse la proteccion si el trace se borra
|
||||
mientras quedan runs paralelos activos.
|
||||
- La trazabilidad de un flujo enterprise deja de ser confiable justo en el modo
|
||||
mas peligroso: paralelo.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Mantener `traceInFlightCount` por `traceId`.
|
||||
- Incrementarlo en `spawnRun()`.
|
||||
- Decrementarlo en el `finally` de `executeRun()`.
|
||||
- Liberar el trace solo cuando `queuedCount(traceId) === 0` e
|
||||
`inFlightCount(traceId) === 0`.
|
||||
- Anadir test: dos eventos derivados con `ORCA_QUEUE_PARALLEL` en el mismo trace;
|
||||
el primero termina, el segundo emite otro evento repetido y el guard sigue
|
||||
aplicando.
|
||||
|
||||
### 2. `provides` no limita `emits`, asi que la validacion puede mentir
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/types.ts:421`
|
||||
- `src/arts/orca/types.ts:424`
|
||||
- `src/arts/orca/types.ts:425`
|
||||
- `src/arts/orca/engine-orca.ts:1117`
|
||||
- `src/arts/orca/engine-orca.ts:1124`
|
||||
|
||||
El contrato dice que `provides` es lo que una accion puede producir, y
|
||||
`validate()` usa `provides` para detectar tokens imposibles. Pero el runtime
|
||||
acepta cualquier token que venga en `result.emits`, aunque no este declarado en
|
||||
`provides`.
|
||||
|
||||
Impacto:
|
||||
|
||||
- `validate()` no es una garantia fuerte; es una estimacion basada en intencion.
|
||||
- Una accion puede desbloquear otra con un token que el grafo no declaraba.
|
||||
- Un typo en `emits` puede cambiar el pipeline sin que `validate()` lo vea.
|
||||
- El futuro `setupOrca()` tipado pierde parte de su sentido si el runtime permite
|
||||
emisiones fuera de esquema.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- En modo dev, lanzar o registrar `CONFIGURATION_INVALID` cuando una accion emite
|
||||
un token no declarado en `provides`.
|
||||
- En modo prod, al menos registrar diagnostic `orca.configuration.invalid` o
|
||||
descartar el token no declarado segun opcion.
|
||||
- Documentar excepcion solo si se decide permitir tokens dinamicos, pero entonces
|
||||
`validate()` debe llamarse "best effort", no "static graph validation".
|
||||
|
||||
### 3. `ORCA_QUEUE_REPLACE` no es `replace-current`, solo reemplaza queued
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/consts.ts:73`
|
||||
- `src/arts/orca/engine-orca.ts:420`
|
||||
- `src/arts/orca/engine-orca.ts:421`
|
||||
- `src/arts/orca/engine-orca.ts:422`
|
||||
- `src/arts/orca/README.md:439`
|
||||
- `src/arts/orca/README.md:1444`
|
||||
|
||||
Hay dos semanticas distintas usando el mismo nombre mental:
|
||||
|
||||
- Una seccion del README describe `replace` como "aborta el run activo y empieza
|
||||
uno nuevo", estilo `takeLatest`.
|
||||
- El motor implementa "reemplaza solo entradas queued; no aborta in-flight".
|
||||
- Otra seccion posterior del README ya reconoce esta realidad.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Para formularios, busquedas, navegacion o cambio de identidad, un usuario puede
|
||||
esperar "ultima intencion gana", pero el run anterior seguira ejecutandose.
|
||||
- Si la accion vieja muta cache/conexiones despues de la nueva, puede pisar estado.
|
||||
- El nombre `replace` es ambiguo para usuarios que vienen de saga/Rx/RTK.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Renombrar la politica actual a algo explicito: `ORCA_QUEUE_REPLACE_QUEUED` o
|
||||
`ORCA_QUEUE_LATEST_QUEUED`.
|
||||
- Reservar `ORCA_QUEUE_REPLACE_CURRENT` / `ORCA_QUEUE_TAKE_LATEST` para la
|
||||
variante fuerte que aborta in-flight.
|
||||
- Si se mantiene el nombre actual, borrar del README cualquier promesa de abortar
|
||||
el run activo.
|
||||
|
||||
### 4. IDs de run/event/trace usan `Date.now()` y `Math.random()` fuera de `timr`
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:1513`
|
||||
- `src/arts/orca/engine-orca.ts:1514`
|
||||
- `src/arts/orca/engine-orca.ts:1517`
|
||||
- `src/arts/orca/engine-orca.ts:1518`
|
||||
- `src/arts/orca/engine-orca.ts:1521`
|
||||
- `src/arts/orca/engine-orca.ts:1522`
|
||||
- `src/arts/orca/README.md:841`
|
||||
|
||||
El README establece que `orca` no debe usar `Date.now()` ni `setTimeout()`
|
||||
directamente. Los timestamps del envelope ya pasan por `timers.clock.now()`, pero
|
||||
los IDs de run/event/trace no.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Tests y replay no son plenamente deterministas.
|
||||
- En SSR/hydration o tests con clocks controlados, los IDs no siguen el tiempo
|
||||
simulado.
|
||||
- La disciplina "todo tiempo via timr" queda rota justo en el modulo que quiere
|
||||
ser trazable.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Anadir `idFactory` a `EngineOrcaOptions` o tres factories separadas:
|
||||
`runIdFactory`, `eventIdFactory`, `traceIdFactory`.
|
||||
- Default: contador monotono con prefijo, alimentado por `timers.clock.now()` si
|
||||
se quiere incluir tiempo.
|
||||
- Tests: factory determinista para snapshots.
|
||||
|
||||
### 5. `setupOrca()` esta documentado como API recomendada pero no existe
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/README.md:235`
|
||||
- `src/arts/orca/README.md:241`
|
||||
- `src/arts/orca/README.md:271`
|
||||
- `src/arts/orca/README.md:284`
|
||||
- `src/arts/orca/README.md:1271`
|
||||
- `src/arts/orca/index.ts`
|
||||
|
||||
El README recomienda `setupOrca()` para apps grandes y muestra ejemplos, pero no
|
||||
hay export ni implementacion en `src/arts/orca`.
|
||||
|
||||
Impacto:
|
||||
|
||||
- La documentacion invita a usar una API inexistente.
|
||||
- El usuario no sabe si debe usar `App.Orca.onEvent()` directo, presets o un
|
||||
setup tipado futuro.
|
||||
- La promesa de validacion fuerte de tokens/eventos queda sin soporte.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- O implementar un `setupOrca()` minimo, aunque solo envuelva `onEvent` con
|
||||
declaracion de eventos/tokens/actions.
|
||||
- O mover esa seccion a "Futuro / v0.1" y dejar claro que v0 real es
|
||||
`createEngineOrca()` + `App.Orca.onEvent()`.
|
||||
|
||||
### 6. La integracion con `connection` no esta cerrada y la documentacion promete mas de lo que existe
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/active-app/presets/standard.ts:28`
|
||||
- `src/arts/active-app/presets/index.ts:5`
|
||||
- `src/arts/active-app/service-factories/connections.ts:9`
|
||||
- `src/arts/connection/README.md:83`
|
||||
- `src/arts/connection/README.md:93`
|
||||
- `src/arts/connection/README.md:100`
|
||||
- `src/arts/connection/session-wiring.ts:27`
|
||||
|
||||
`applyStandardOrca()` solo registra presets de cache y perm. No hay preset para
|
||||
`connections.reauthenticateAll()` ni para `connections.closeAll()` en cambios de
|
||||
identidad/revoke/expire. A la vez, la documentacion de `connection` habla de
|
||||
`autoReauthOn: 'standard'` y de escuchar `SESSION_EVENT_IDENTITY_CHANGED`.
|
||||
|
||||
Ademas, `defineActiveConnections()` ya no inyecta `bus` ni `session`; el comentario
|
||||
dice que identidad debe venir por orca preset o por una `session` manual, pero ese
|
||||
preset no existe.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Una app que siga la documentacion puede creer que las conexiones se reautentican
|
||||
al cambiar de usuario, pero con `applyStandardOrca(App)` no ocurre.
|
||||
- El caso critico que motivo `orca` ("chat conectado con credenciales del usuario
|
||||
anterior") sigue sin preset estandar.
|
||||
- `connection` conserva un mecanismo interno de `session-wiring` que compite
|
||||
conceptualmente con la nueva regla: los artefactos publican/reciben eventos; la
|
||||
app orquesta reacciones.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Crear preset en `active-app/presets`:
|
||||
`applyConnectionsReauthOnIdentityChange(App)`.
|
||||
- Definir si el estandar hace `reauthenticateAll()` o `closeAll()` cuando no hay
|
||||
credencial nueva.
|
||||
- Actualizar `applyStandardOrca()` para incluir conexiones cuando `App.connections`
|
||||
exista.
|
||||
- Decidir si `connection.session-wiring` queda como modo local/manual o se depreca
|
||||
a favor del preset de `orca`.
|
||||
|
||||
## Hallazgos P2
|
||||
|
||||
### 7. `runActionWithTimeout()` aborta de forma cooperativa, pero la accion puede seguir mutando estado
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:1042`
|
||||
- `src/arts/orca/engine-orca.ts:1052`
|
||||
- `src/arts/orca/engine-orca.ts:1056`
|
||||
- `src/arts/orca/engine-orca.ts:1058`
|
||||
- `src/arts/orca/engine-orca.ts:1070`
|
||||
|
||||
El timeout hace `actionController.abort()` y resuelve `orcaTimeout(timeoutMs)`, pero
|
||||
la promesa original de la accion sigue viva si la accion no respeta `ctx.signal`.
|
||||
Esto es normal en JS, pero debe tratarse como contrato de seguridad.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Una accion timeout puede mutar cache, conexiones o permisos despues de que el run
|
||||
ya haya tomado otra decision.
|
||||
- Los tests pueden pasar porque el resultado es `timeout`, pero el side-effect
|
||||
tardio queda fuera del trace.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Documentar que toda accion async debe comprobar `ctx.signal.aborted` antes y
|
||||
despues de awaits relevantes.
|
||||
- Anadir helper `ctx.throwIfAborted()` o `orcaAbortIfSignaled(ctx)`.
|
||||
- Anadir test: accion con timeout que intenta mutar despues; demostrar que el
|
||||
patron recomendado lo evita.
|
||||
|
||||
### 8. La cola es global para eventos no-parallel, aunque el contrato se lee como per-event
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:526`
|
||||
- `src/arts/orca/engine-orca.ts:527`
|
||||
- `src/arts/orca/engine-orca.ts:529`
|
||||
- `src/arts/orca/engine-orca.ts:531`
|
||||
|
||||
`canStartRun()` serializa globalmente todos los eventos no-parallel:
|
||||
`nonParallelInFlight === 0`. Eso significa que un evento lento de `cache` puede
|
||||
bloquear un evento no relacionado de `perm`, `connection` o cualquier feature.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Semantica mas conservadora y lenta de lo que sugiere "queue policy per event".
|
||||
- Posible cuello de botella en apps grandes con flujos independientes.
|
||||
- Si esta decision es intencional, falta nombrarla como "single lane".
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Decidir explicitamente:
|
||||
- Modelo A: serializacion global por defecto, documentada como garantia simple.
|
||||
- Modelo B: serializacion por evento, con concurrencia entre eventos distintos.
|
||||
- Si se mantiene A, renombrar/comentar como `globalSerialLane`.
|
||||
- Si se pasa a B, usar `inFlightByEvent` para bloquear solo el mismo evento.
|
||||
|
||||
### 9. `commit()` congela el grafo; eso choca con features lazy si no se documenta como modo prod
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/types.ts:711`
|
||||
- `src/arts/orca/types.ts:723`
|
||||
- `src/arts/orca/engine-orca.ts:1275`
|
||||
- `src/arts/orca/test/engine-orca.test.ts:4013`
|
||||
|
||||
El comportamiento actual esta claro en codigo y tests: despues de `commit()`,
|
||||
`onEvent()` y `configureEvent()` lanzan `OrcaFrozenError`. Esto no es un bug por
|
||||
si mismo, pero tensiona el objetivo de registrar acciones desde features
|
||||
lazy-loaded.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Si una app llama `App.Orca.commit()` durante bootstrap, una ruta lazy ya no puede
|
||||
registrar sus acciones al cargar.
|
||||
- HMR/devtools pueden quedar bloqueados si no se separa "validar" de "sellar".
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Documentar `commit()` como opcion production/seal, no como paso obligatorio.
|
||||
- Considerar `validate()` para dev y `commit()` solo para builds donde el grafo es
|
||||
completo al arrancar.
|
||||
- Si se quiere lazy + freeze, introducir versionado por evento: cada run usa un
|
||||
snapshot, pero el registro global puede crecer entre runs.
|
||||
|
||||
### 10. `ctx.emit()` es correcto, pero la atribucion via `bus.publish()` depende de AsyncLocalStorage
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:133`
|
||||
- `src/arts/orca/engine-orca.ts:204`
|
||||
- `src/arts/orca/engine-orca.ts:864`
|
||||
- `src/arts/orca/als.ts`
|
||||
- `src/arts/orca/README.md:606`
|
||||
- `src/arts/orca/README.md:621`
|
||||
- `src/arts/orca/README.md:1470`
|
||||
|
||||
El diseno correcto es claro: modulo -> `bus.publish()`, accion -> `ctx.emit()`.
|
||||
El motor intenta interceptar `bus.publish()` dentro de acciones mediante
|
||||
AsyncLocalStorage cuando esta disponible. En navegador puede no estar disponible,
|
||||
por lo que el mismo `bus.publish()` dentro de una accion puede ser hijo en Node y
|
||||
root event en browser.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Trazas distintas entre SSR/tests Node y browser.
|
||||
- Reentry guards no aplican igual si un evento derivado sale como root.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Mantener la regla dura: las acciones usan `ctx.emit()` siempre.
|
||||
- Tratar la interceptacion ALS como bonus diagnostico, no como contrato.
|
||||
- Anadir lint/documentacion: no usar `App.Bus.publish()` dentro de una accion de
|
||||
`orca` salvo que se quiera crear root event explicito.
|
||||
|
||||
### 11. `engine-orca.ts` ya es un monolito de responsabilidades
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts` (~58 KB)
|
||||
|
||||
El engine concentra registro, cola, trace state, reentry, ejecucion de waves,
|
||||
timeouts, compensation, validation, ID generation, diagnostics y public API.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Dificulta revisar invariantes delicadas como "trace cleanup + parallel".
|
||||
- Hace mas probable que futuras features (`runTimeoutMs`, `setupOrca`, inspector)
|
||||
entren sin frontera clara.
|
||||
- Ya contradice la direccion del ecosistema de reducir monolitos (`connection.ts`
|
||||
se venia refactorizando por el mismo motivo).
|
||||
|
||||
Recomendacion de split:
|
||||
|
||||
- `queue.ts`: enqueue, policies, drain, in-flight counters.
|
||||
- `trace.ts`: envelope, trace state, reentry guards, release.
|
||||
- `runner.ts`: stages, waves, action execution, status precedence.
|
||||
- `timeouts.ts`: action timeout helpers y timer keys.
|
||||
- `compensation.ts`: LIFO compensation / transaction compensation.
|
||||
- `validation.ts`: validate/commit helpers.
|
||||
- `ids.ts`: factories deterministas.
|
||||
- `engine-orca.ts`: composicion publica.
|
||||
|
||||
### 12. Los presets usan action ids y tokens inline/locales
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/active-app/presets/cache-clear-on-identity-change.ts:6`
|
||||
- `src/arts/active-app/presets/cache-clear-on-identity-change.ts:7`
|
||||
- `src/arts/active-app/presets/cache-clear-on-revoke.ts:6`
|
||||
- `src/arts/active-app/presets/cache-clear-on-revoke.ts:7`
|
||||
- `src/arts/active-app/presets/perm-invalidate-on-identity-change.ts:6`
|
||||
- `src/arts/active-app/presets/perm-invalidate-on-identity-change.ts:7`
|
||||
|
||||
Los eventos del ecosistema ya estan centralizados como constantes, pero los
|
||||
`ACTION_ID` y `TOKEN_*` de presets quedan como strings locales no exportados.
|
||||
|
||||
Impacto:
|
||||
|
||||
- No se pueden reutilizar en tests de integracion, docs, inspector `/test/orca`
|
||||
o setup tipado.
|
||||
- Rompe la regla emergente: "eventos, tokens y action ids como constantes".
|
||||
- El inspector no puede mostrar nombres canonicos importables.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Crear `src/arts/active-app/presets/consts.ts` o exportar desde cada preset:
|
||||
`APP_ORCA_ACTION_CACHE_CLEAR_ON_IDENTITY_CHANGE`,
|
||||
`APP_ORCA_TOKEN_CACHE_CLEARED_ON_IDENTITY_CHANGE`, etc.
|
||||
- Usar nombres de constantes en README y pagina docs.
|
||||
|
||||
## Hallazgos P3
|
||||
|
||||
### 13. Header de `engine-orca.ts` esta obsoleto
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:24`
|
||||
- `src/arts/orca/engine-orca.ts:25`
|
||||
- `src/arts/orca/engine-orca.ts:26`
|
||||
|
||||
El comentario inicial dice que `after`, `unless`, `abortOn`,
|
||||
`actionTimeoutMs` y `compensate` se aceptan pero se ignoran. El motor ya los
|
||||
implementa.
|
||||
|
||||
Impacto:
|
||||
|
||||
- La siguiente persona que lea el archivo empieza con un mapa mental falso.
|
||||
- Ya se corrigieron comentarios en `types.ts`, `consts.ts` y `result.ts`, pero el
|
||||
comentario mas importante del motor sigue atrasado.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Actualizar el bloque de cabecera para describir el estado real.
|
||||
- Evitar version tags contradictorios dentro del codigo; mover roadmap al README.
|
||||
|
||||
### 14. El README de `orca` mezcla contrato actual, boceto y roadmap en un solo flujo
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/README.md:511`
|
||||
- `src/arts/orca/README.md:1411`
|
||||
- `src/arts/orca/README.md:1506`
|
||||
- `src/arts/orca/README.md:1522`
|
||||
|
||||
Ejemplo claro: una seccion dice que en v0 los tokens son flags sin payload; otra
|
||||
seccion posterior dice que los tokens con payload ya estan implementados. Esto no
|
||||
es solo estetico: afecta al modo en que un desarrollador modela acciones.
|
||||
|
||||
Impacto:
|
||||
|
||||
- La documentacion no sirve como contrato normativo.
|
||||
- Los lectores no saben si estan viendo la version deseada o la version real.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Reestructurar README en tres bloques cerrados:
|
||||
- "Contrato actual implementado"
|
||||
- "Patrones recomendados"
|
||||
- "Roadmap / no implementado"
|
||||
- Eliminar del contrato actual cualquier API no exportada (`setupOrca`) o moverla
|
||||
a roadmap.
|
||||
- Mantener una tabla "Feature -> estado -> archivo/test".
|
||||
|
||||
### 15. Falta una prueba compuesta del caso que motivo `orca`: cambio de usuario con cache/perm/connection
|
||||
|
||||
Referencias:
|
||||
|
||||
- `src/arts/active-app/test/presets.test.ts`
|
||||
- `src/arts/orca/test/engine-orca.test.ts`
|
||||
|
||||
Hay buena cobertura unitaria de `orca` y presets basicos. Falta el escenario
|
||||
ecosistema que debe probar el valor real:
|
||||
|
||||
1. Usuario A abre sesion.
|
||||
2. Chat/conexion usa credencial A.
|
||||
3. Cache actor-scoped guarda datos de A.
|
||||
4. Perm calcula snapshot de A.
|
||||
5. Cambia a usuario B.
|
||||
6. Orca ejecuta clear cache, invalidate perm y reauth/close connection.
|
||||
7. Ningun dato/credencial de A queda observable.
|
||||
|
||||
Impacto:
|
||||
|
||||
- El sistema puede estar correcto por modulo pero fallar justo en composicion.
|
||||
- El caso "chat con credenciales antiguas" sigue sin prueba de regresion.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Crear test de integracion en `src/arts/active-app/test/ecosystem-orca.test.ts`
|
||||
o equivalente.
|
||||
- Usar fakes de cache/perm/connection con counters y credenciales capturadas.
|
||||
- Verificar orden por `OrcaRunResult`: cache -> perm -> connection si se decide
|
||||
dependencia via tokens.
|
||||
|
||||
## Observaciones positivas
|
||||
|
||||
- `App.Bus`, `App.Timers` y `App.Orca` ya son core siempre presentes en
|
||||
`createActiveApp()`, y `Orca` es inerte hasta registrar acciones. Esa decision
|
||||
encaja con el diseno enterprise sin obligar a cada app a crear singletons
|
||||
manuales.
|
||||
- Los presets viven en `active-app/presets`, no dentro de los artefactos. Esto es
|
||||
correcto: `cache` no debe conocer `session`; `perm` no debe conocer `auth`;
|
||||
`orca` no debe conocer modulos.
|
||||
- `ActiveOrca` es un wrapper fino y razonable: estado reactivo, sin meter logica
|
||||
del engine en Svelte.
|
||||
- La suite focalizada de `orca` ya es grande: 169 tests entre engine/active/result
|
||||
y presets. Para un modulo recien nacido, eso es muy buena base.
|
||||
- `ctx.signal` existe en `OrcaActionContext`; eso habilita cancelacion cooperativa
|
||||
y es la direccion correcta.
|
||||
|
||||
## Recomendacion de orden de trabajo
|
||||
|
||||
1. Corregir P1.1: trace cleanup con runs paralelos.
|
||||
2. Cerrar P1.2: `provides` debe ser contrato real o `validate()` debe declararse
|
||||
best-effort.
|
||||
3. Resolver P1.3: renombrar/split de `replace` para evitar semantica ambigua.
|
||||
4. Sustituir `Date.now()`/`Math.random()` por factories deterministas.
|
||||
5. Decidir `setupOrca()`: implementarlo minimo o moverlo fuera de v0.
|
||||
6. Crear preset de `connection` y test compuesto usuario A -> usuario B.
|
||||
7. Reordenar README para separar implementado/recomendado/roadmap.
|
||||
8. Refactorizar `engine-orca.ts` por responsabilidades antes de anadir run/stage
|
||||
timeout, inspector visual o setup tipado.
|
||||
|
||||
## Conclusion
|
||||
|
||||
`orca` merece seguir. No es "otro bus": es el sitio correcto para convertir
|
||||
flujos inter-modulo en artefactos trazables y testeables. Pero precisamente por
|
||||
eso no puede permitirse ambiguedad en nombres, timeouts, tokens y concurrencia.
|
||||
|
||||
El ecosistema esta cruzando una frontera importante: de librerias coherentes a
|
||||
runtime de aplicacion. La prioridad ahora no es meter mas features, sino hacer
|
||||
que el contrato de `orca` sea tan fiable como su idea.
|
||||
@ -0,0 +1,566 @@
|
||||
# Auditoria Codex del ecosistema hacia 1.0
|
||||
|
||||
Fecha: 2026-05-04
|
||||
Alcance: `lang`, `cache`, `session`, `perm`, `auth`, `http`.
|
||||
Excluido: `orca`, ya auditado por separado.
|
||||
|
||||
## Veredicto ejecutivo
|
||||
|
||||
El ecosistema esta bastante por encima de una 0.x normal: hay contratos tipados, separacion real entre `libs`, `arts` y `svrs`, factories declarativas en `createActiveApp()`, y una base de tests que hoy pasa para los modulos auditados.
|
||||
|
||||
La brecha hacia 1.0 no esta tanto en "reescribir" modulos, sino en cerrar tres frentes:
|
||||
|
||||
1. **Contratos publicos exactos**: hay documentacion y ejemplos que todavia prometen aliases, nombres o inyecciones que el codigo ya no hace.
|
||||
2. **Determinismo operativo**: `timr`/`Timers` ya existe, pero `http`, `session`, `cache` y `perm` aun conservan defaults con `Date.now()`, `Math.random()` o timers nativos cuando se usan fuera del wiring ideal.
|
||||
3. **Pruebas compuestas**: los tests unitarios estan verdes, pero faltan historias de ecosistema donde `auth`, `session`, `perm`, `cache` y `http` fallan o se invalidan juntos.
|
||||
|
||||
Verificacion ejecutada:
|
||||
|
||||
```txt
|
||||
npx vitest run src/arts/lang/test src/arts/cache/test src/libs/cache/test src/svrs/cache/test src/arts/session/test src/arts/perm/test src/svrs/perm/test src/arts/auth/test src/svrs/auth/test src/arts/http/test
|
||||
|
||||
35 test files passed
|
||||
426 tests passed
|
||||
```
|
||||
|
||||
## Hallazgos transversales
|
||||
|
||||
### P1 - La documentacion debe dejar de prometer APIs antiguas
|
||||
|
||||
Hay restos de la etapa de aliases de 4 letras y de nombres capitalizados: `$cach`, `$sess`, `App.Cache`, `App.Sess`, ejemplos sin `services: { ... }`, y textos que dicen que `cache` esta siempre presente. El codigo actual va por `createActiveApp({ services })` y expone servicios lazy en minuscula (`App.cache`, `App.session`, `App.perm`, `App.auth`, `App.http`).
|
||||
|
||||
Impacto: un desarrollador nuevo no distingue que es API real y que es historia del framework. Para 1.0, esto no puede quedar en "se entiende mirando codigo"; la documentacion es parte del contrato.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Hacer una pasada de docs con una regla mecanica: ningun README ni pagina `src/web/routes/active/docs/**` puede usar alias o propiedades que no existan en `svelte.config.js` y en `src/arts/active-app/service-factories/**`.
|
||||
- Crear tests de snippets o al menos un script que busque `$cach`, `$sess`, `App.Cache`, `App.Sess`, `App.Http`, `App.Perm`, etc.
|
||||
|
||||
### P1 - La inyeccion entre servicios no esta igual de clara que el discurso
|
||||
|
||||
Las factories actuales inyectan principalmente `logger`, y solo `session` recibe tambien `bus`:
|
||||
|
||||
- `defineActiveAuth(...)` recibe `logger`; el `http` debe venir en `options`.
|
||||
- `defineActivePerm(...)` recibe `logger`; aunque `PermClientOptions` acepta `http?: EngineHttp`, la factory no declara dependencia de `http`.
|
||||
- `defineActiveCache(...)` recibe `logger`; no recibe `timers` por defecto.
|
||||
- `defineActiveSession(...)` recibe `logger` y `bus`; no recibe `timers` para auto-refresh.
|
||||
- `defineEngineHttp(...)` recibe `logger`; no recibe `timers`.
|
||||
|
||||
Esto es coherente si el principio es "pasivo por defecto, opt-in explicito". Pero varias docs ya hablan como si `Http`, `Cache`, `Bus` y `Timers` se cablearan automaticamente entre servicios.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Decidir para 1.0 una regla unica: o las factories solo reciben core deps minimas, o pueden declarar `serviceDependencies`.
|
||||
- Si se mantiene el modo minimalista, documentarlo sin ambiguedad: `auth` necesita `http` explicito, `perm` necesita `endpoint` o `http` explicito, `cache` no se invalida sola, `session` no refresca con Timers salvo que se le pase.
|
||||
- Si se quiere ergonomia enterprise, evolucionar factories para dependencias opcionales: `defineActivePerm` puede consumir `http` si existe; `defineActiveCache` y `defineActiveSession` pueden consumir `timers`.
|
||||
|
||||
### P1 - Falta una suite compuesta de identidad, permisos y cache
|
||||
|
||||
La base verde actual no prueba suficientemente los casos que mas preocupan en una aplicacion real:
|
||||
|
||||
- Usuario A abre sesion, cachea datos privados, cambia a usuario B, y B no ve cache ni permisos de A.
|
||||
- Backend comunica cambio de permisos y el cliente invalida decision cacheada antes de permitir acciones.
|
||||
- `http` recibe 401/403, dispara refresh/auth state, `session` adopta o revoca, `perm` y `cache` reaccionan.
|
||||
- Logout global revoca sesion, limpia cache actor-scoped y deja `perm` sin actor.
|
||||
- Password reset revoca sesiones y el cliente queda en `anonymous` sin datos privados residuales.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Crear `src/routes/test/ecosystem` como harness de estas historias o moverlas a tests headless en `src/arts/active-app/test/ecosystem-*.test.ts`.
|
||||
- Estos tests deben usar `App.Bus`/presets/orquestacion cuando toque, pero el criterio es observable: snapshots finales y ausencia de datos cruzados.
|
||||
|
||||
### P2 - Tiempo y aleatoriedad aun no son uniformes
|
||||
|
||||
Puntos concretos:
|
||||
|
||||
- `src/arts/http/retry.ts:48`, `:52`, `:64` usa `Date.now()`, `Math.random()` y `setTimeout`.
|
||||
- `src/arts/http/timeout.ts:43`, `:63` usa `setTimeout`.
|
||||
- `src/arts/cache/active-cache.svelte.ts:195` usa `Date.now()` si no se pasa clock.
|
||||
- `src/arts/session/auto-refresh.ts:44`, `:45`, `:72` usa `Date.now()`, `Math.random()` y `setInterval` si no se pasa `timers`.
|
||||
- `src/arts/perm/client.ts:291` usa `Date.now()` si no se pasa clock.
|
||||
|
||||
No es necesariamente un bug para uso aislado, pero para 1.0 la composicion `createActiveApp()` deberia poder inyectar `Timers.clock` y scheduler por defecto en los servicios que lo aceptan. Esa es una de las diferencias entre "librerias utiles" y "framework determinista".
|
||||
|
||||
## Modulo `lang`
|
||||
|
||||
### Estado
|
||||
|
||||
`lang` esta entre los modulos mas maduros. Tiene engine puro, wrapper active, resolucion de referencias `#?path|fallback`, extension de schema, pluralizacion, JSON helpers y una suite de tests amplia.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Cambiar `SvelteSet` por `Set` en listeners.** En `src/arts/lang/active-lang.svelte.ts:34` `localeListeners` no alimenta templates ni estado derivado; solo se itera manualmente. Igual que se hizo en otros modulos, `Set` plano reduce reactividad innecesaria.
|
||||
2. **Separar mutacion y composicion de schemas con nombres mas explicitos.** Hoy `extend(namespace, module)` muta el engine activo y `register(namespace, module)` devuelve un engine hijo. Es potente, pero el naming puede confundir. Para 1.0 documentaria una tabla estricta: `extend` muta, `register` compone hijo.
|
||||
3. **Hacer `SupportedLocale` menos cerrado.** `src/libs/lang/types.ts` limita locales a una lista base. Para producto 1.0, conviene permitir cualquier BCP47 tipado como branded string o una registry generica por app.
|
||||
4. **Modo estricto de interpolacion.** `interpolateTemplate()` resuelve placeholders, pero no hay modo que falle si falta un parametro. Para 1.0 deberia existir `strictInterpolation` con diagnostico/log.
|
||||
5. **Unificar docs de inyeccion.** La factory `defineActiveLang` si inyecta logger via `setLogger(core.logger)`. La documentacion debe mostrar claramente `services: { lang: defineActiveLang({ schema }) }` y no dar a entender que `createActiveApp()` siempre trae un schema real.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Loader asincrono de packs de idioma: `loadLocale(locale)` con cache y fallback.
|
||||
- `Lang.tCode(code)` si se adopta una capa comun de errores/codigos.
|
||||
- Integracion con formatos: resolver `{{price | currency}}`, `{{date | datetime}}` usando `format`.
|
||||
- Dev inspector: namespace registrados, fallback usado, claves faltantes, referencias circulares.
|
||||
- Script de validacion de schema: claves faltantes por locale, claves muertas y paths duplicados.
|
||||
|
||||
## Modulo `cache`
|
||||
|
||||
### Estado
|
||||
|
||||
`cache` tiene un runtime solido en `libs/cache`, wrapper `svrs/cache` y `arts/cache` reactivo. Hay politicas, scopes, tags, epochs, singleflight, stale-if-error y adapters memory/storage. La arquitectura esta bien orientada.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Actualizar docs antiguas.** Hay ejemplos que aun usan `$cach` o `App.Cache`; el alias real es `$cache` y la app expone `App.cache` solo si se declara `services.cache`.
|
||||
2. **Inyectar clock desde App.Timers cuando se usa como servicio.** `createActiveCache()` acepta `clock`, pero `defineActiveCache()` solo inyecta `logger`. Para 1.0, el default app-wired deberia usar `core.timers.clock`.
|
||||
3. **Evitar `console.warn` directo en memory adapter.** `src/libs/cache/adapters/memory.ts` usa `console.warn` si se crea en produccion sin `onProductionWarning`. En 1.0, la advertencia deberia pasar por diagnostics/logger o exigir handler explicito.
|
||||
4. **Revisar clonacion de valores.** `memoryCacheAdapter` usa `structuredClone` si existe y fallback JSON. El fallback rompe `Date`, `Map`, `Set`, `BigInt`, clases y valores no serializables. Para 1.0: o se documenta "valores serializables" o se expone `clone?: (value) => value`.
|
||||
5. **Purgado escalable.** La memoria purga expirados escaneando entradas. Es razonable para v0, pero para 1.0 conviene un sweeper opcional con `Timers` o un indice por expiracion si se esperan caches grandes.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Adapter L2 remoto opcional: Redis/HTTP/IndexedDB, manteniendo L1 memory.
|
||||
- Invalidacion por evento app: identity changed, tenant switched, permission changed.
|
||||
- `cache.queryHttp()` o helper de integracion con `http` para cachear respuestas con schema.
|
||||
- Metricas: hit rate, stale served, refresh failures, singleflight joins, evictions.
|
||||
- Politicas de privacy: impedir persistir scopes actor/session en adapters no seguros salvo opt-in.
|
||||
- Tests de no fuga cross-actor y cross-tenant.
|
||||
|
||||
## Modulo `session`
|
||||
|
||||
### Estado
|
||||
|
||||
`session` es de los modulos mejor testeados. Tiene engine puro, wrapper active, generacion/versionado para evitar carreras, broadcast/storage sync, eventos de bus y auto-refresh opcional. La direccion es buena.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Actualizar naming en docs.** Debe desaparecer `$sess` y `App.Sess`; el alias real es `$session` y el servicio es `App.session`.
|
||||
2. **Cablear auto-refresh con Timers desde App.** `auto-refresh.ts` ya acepta `timers`, pero `defineActiveSession()` no los inyecta. Para 1.0, si una app usa `createActiveApp()`, el refresh deberia poder ser determinista sin boilerplate manual.
|
||||
3. **Limitar defaults nativos en modo app-wired.** `Date.now`, `Math.random` y `setInterval` son aceptables como fallback aislado, pero no como camino principal del ecosistema.
|
||||
4. **Documentar ownership frente a auth.** `session` no debe saber de credenciales ni permisos; solo continuidad, refresh/revoke, snapshot y bus events. `auth` prueba identidad; `perm` decide permisos.
|
||||
5. **Opciones explicitas para sync multi-tab.** Si ya existen, deben documentarse mejor; si no, conviene `broadcast: false | { channel }` para entornos con privacidad estricta o tests.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Preset `defineActiveSession({ autoRefresh: { standard: true } })` que use `Timers`.
|
||||
- Integracion `http` para refresh por 401 sin acoplar `http` a `session`: hook reusable de aplicacion.
|
||||
- Eventos canonicos para identity changed, credential refreshed, revoked, expired.
|
||||
- Tests compuestos con auth/cache/perm.
|
||||
- Modo SSR documentado: adoptar snapshot server sin doble refresh ni flicker.
|
||||
|
||||
## Modulo `perm`
|
||||
|
||||
### Estado
|
||||
|
||||
`perm` tiene mas base de servidor de la que parecia al inicio: `libs/perm` incluye DSL/evaluator/compiler, `svrs/perm` aporta engine, repository y SQL de referencia, y `arts/perm` aporta cliente activo. Es una buena base para 1.0, pero hay dos puntos que conviene cerrar pronto.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Corregir compilador SQL para paths anidados.** El runtime usa `getPath`, pero `src/libs/perm/compilers/sql.ts:105` y `:110` leen `input.actor[expr.path]` y `input.context?.[expr.path]`. Un path como `risk.mfa` o `profile.department` se evaluara distinto en runtime y en SQL. Para 1.0 esto debe ser P1: usar `getPath()` tambien en actor/context o declarar que SQL solo soporta paths planos.
|
||||
2. **Preordenar policies una vez.** `src/libs/perm/runtime.ts` ordena por prioridad en cada decision. Para volumen real, ordenar al construir runtime y mantener indices por action/resource reduce coste.
|
||||
3. **Memoizar providers por decision.** `DefaultPermEvaluator` puede llamar varias veces a relation/attribute providers con la misma key dentro de una decision. Un cache por request reduce latencia y evita multiples consultas a DB.
|
||||
4. **Eliminar IDs auto-generados no estables en produccion.** `definePolicies`/builders pueden producir IDs por contador. Para 1.0, los policies persistidos deberian exigir `id` estable o generar checksum determinista.
|
||||
5. **Alinear factory y docs.** `PermClientOptions` acepta `http?: EngineHttp`, pero `defineActivePerm()` no inyecta `App.http`. O se documenta que `fetcher`/`http` son manuales, o se declara dependencia opcional de `http`.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Persistencia oficial: migraciones SQL versionadas, repository contract tests y ejemplos Kysely/Drizzle.
|
||||
- Webhook/evento de cambio de permisos: invalidar cliente y cache de decisiones por actor/tenant.
|
||||
- `what()` y `explain()` cacheados con invalidacion por version de policies.
|
||||
- Obligations/advice con enforcement helpers, no solo datos.
|
||||
- Auditoria de decisiones: escribir `permission_decision_audit` desde server engine con redaccion.
|
||||
- Tests de cross-actor race: login A -> decision allow -> login B -> misma accion no reutiliza decision.
|
||||
|
||||
## Modulo `auth`
|
||||
|
||||
### Estado
|
||||
|
||||
`auth` ha avanzado mucho: hay `libs/auth` como lenguaje comun, `svrs/auth` con engine server-authoritative, SQL de referencia, password signup/signin, CSRF, recovery, device records, refresh rotation y OAuth base. `arts/auth` es un cliente seguro que refleja `AuthCurrentView` y no intenta ser autoridad.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Completar handlers para rutas ya publicadas.** `src/libs/auth/consts.ts:21-30` declara rutas OAuth, MFA, devices y WebAuthn. `src/arts/auth/active-auth.svelte.ts:154-166` ya llama `DEVICES` y `DEVICE_REVOKE`. Pero `src/svrs/auth/handlers.ts` solo enruta current, csrf, password, logout, email verification y password reset. Resultado: `ActiveAuth.listDevices()` y `revokeDevice()` apuntan a endpoints que el handler generico no sirve. Para 1.0, o se agregan handlers, o se retiran del cliente hasta estar soportados.
|
||||
2. **No guardar secretos OAuth en metadata de flow.** `oauth-flow.ts` guarda `state` y `verifier` en `metadata` ademas de hashes. Aunque la store sea server-side, el contrato ideal es persistir solo hash/verifier cifrado o recuperar verifier por canal seguro. Para 1.0 debe revisarse porque el documento original era estricto con secretos.
|
||||
3. **PKCE challenge no debe usar hash token raw si no es base64url SHA-256 estandar.** Si `hashAuthToken()` no produce exactamente `base64url(SHA256(verifier))`, el flujo OAuth no sera interoperable. El test `oauth-pkce.test.ts` existe, pero conviene comprobarlo contra el RFC shape.
|
||||
4. **Rate-limit esta bien cableado en password/recovery/oauth, pero falta matriz.** `enforceAuthRateLimit()` existe y se usa en flujos principales; para 1.0 hace falta tabla por metodo, key usada y politica recomendada.
|
||||
5. **`createDbAuthAdapter` es demasiado generic para produccion.** El adapter `db.ts` acepta repositorios con `where` generico basado en records TS, mientras el SQL aplana `actorRef` a `tenant_id/actor_id`. Es valido como referencia, pero 1.0 necesita un adapter/repository probado contra el SQL real, no solo un contrato abstracto.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Handlers completos para devices y OAuth; MFA/WebAuthn marcados como experimental si no se implementan.
|
||||
- Contract tests del SQL auth: credentials, flows, linked accounts, session bindings, refresh families.
|
||||
- Anti-enumeration tests para recovery/email verification.
|
||||
- Refresh rotation integrada end-to-end con sesion real, no solo helper unitario.
|
||||
- Device/session management completo: listar, revocar actual, revocar otro, global logout.
|
||||
- Security events hacia logger/audit con redaccion obligatoria.
|
||||
- Integracion con `session`, `cache` y `perm`: signin/logout/password reset invalidan lo necesario sin acoplar modulos directamente.
|
||||
|
||||
## Modulo `http`
|
||||
|
||||
### Estado
|
||||
|
||||
`http` esta muy bien codificado: engine puro, resultados tipados, hooks, retry, timeout, schema de request/response, fetch inyectable y tests amplios. Es una pieza importante para que el ecosistema no dependa de `fetch` crudo.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Port de timers/retry.** `retry.ts` y `timeout.ts` usan timers nativos. Para 1.0 deberia existir `HttpTimerPort` o integracion directa con `Timers`, al menos cuando se crea via `defineEngineHttp()`.
|
||||
2. **Jitter inyectable.** `computeRetryDelay()` usa `Math.random()` si `policy.jitter` esta activo. Para tests deterministas y produccion controlada, aceptar `random?: () => number`.
|
||||
3. **Cerrar lifecycle de timeouts.** `attemptTimeoutSignal()` y `totalTimeoutSignal()` crean `setTimeout`; si la request termina antes, el timeout queda pendiente hasta disparar. No siempre es grave, pero en alto volumen deberia poder cancelarse.
|
||||
4. **Helpers de autenticacion sin acoplar a auth.** La pieza deberia ofrecer patrones genericos para `beforeRequest`/`beforeRetry`/`beforeError` que permitan refresh por 401, pero sin conocer `session` ni `auth`.
|
||||
5. **Observabilidad de hooks.** Hoy se emiten diagnosticos de retry y errores; para 1.0 conviene medir tiempo por intento, delay real, abort reason y hooks que rescatan respuestas.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `http.with({ fetch: event.fetch })` documentado con snippets SSR reales.
|
||||
- Preset de retry empresarial: idempotentes por defecto, 429/503 con Retry-After, jitter determinista opcional.
|
||||
- Adapter/cache bridge: convertir `Response` en envelope cacheable con schema.
|
||||
- Circuit breaker opcional o al menos hooks para implementarlo con `cache/session`.
|
||||
- Tests de 401 -> refresh -> replay request; 403 -> perm invalidation; offline -> stale cache.
|
||||
|
||||
## Roadmap recomendado hacia 1.0
|
||||
|
||||
### Sprint 1 - Contrato publico y docs reales
|
||||
|
||||
- Corregir aliases y nombres de servicios en READMEs y paginas `active/docs`.
|
||||
- Documentar factories reales: que inyecta cada una y que debe pasar el desarrollador.
|
||||
- Crear un test/script de docs que detecte aliases muertos y propiedades antiguas.
|
||||
- Publicar tabla "core siempre presente vs services declarados".
|
||||
|
||||
### Sprint 2 - Determinismo e inyeccion
|
||||
|
||||
- Hacer que `defineActiveCache`, `defineActiveSession`, `defineActivePerm` y `defineEngineHttp` puedan consumir `core.timers` cuando aplique.
|
||||
- Inyectar `random` en retry/session auto-refresh.
|
||||
- Mantener fallbacks nativos para uso aislado, pero no para el camino App.
|
||||
|
||||
### Sprint 3 - Bugs de contrato
|
||||
|
||||
- Completar handlers de `auth` para devices/OAuth o retirar esas APIs del cliente hasta estar soportadas.
|
||||
- Corregir path anidado en compilador SQL de `perm`.
|
||||
- Revisar OAuth PKCE y persistencia de verifier/state.
|
||||
- Alinear `defineActivePerm` con `http` real.
|
||||
|
||||
### Sprint 4 - Tests compuestos
|
||||
|
||||
Escenarios minimos:
|
||||
|
||||
1. Login A -> cache privado -> logout -> login B -> B no ve cache/perm de A.
|
||||
2. Permission webhook -> `perm.invalidate()` -> decision antigua no se reutiliza.
|
||||
3. HTTP 401 -> refresh sesion -> replay -> cache conserva solo datos validos.
|
||||
4. Password reset -> revoca sesiones -> active auth anonimo -> cache actor-scope limpia.
|
||||
5. Logout global -> session revoked -> perm sin actor -> cache limpia -> http protegido falla seguro.
|
||||
6. Tenant switch -> cache/perm invalidados por tenant.
|
||||
|
||||
### Sprint 5 - Server readiness
|
||||
|
||||
- `auth` y `perm` ya tienen SQL de referencia; convertirlo en migraciones versionadas o al menos en contract tests ejecutables.
|
||||
- `cache` necesita historia clara de adapter server: memory solo test/dev, storage/browser, y adapter remoto recomendado.
|
||||
- `http` no necesita `svrs/http`, pero si necesita ejemplos SSR y edge/runtime.
|
||||
|
||||
## Prioridad resumida
|
||||
|
||||
| Prioridad | Tema | Modulos | Motivo |
|
||||
|---|---|---|---|
|
||||
| P1 | Handlers `auth` incompletos para APIs publicas | auth | Cliente llama endpoints que el handler generico no enruta. |
|
||||
| P1 | SQL compiler no resuelve paths anidados igual que runtime | perm | Riesgo de decisiones distintas entre filtrado DB y evaluacion memory. |
|
||||
| P1 | Docs/API antiguas tras rename y service schema | todos | Bloquea adopcion y genera mal uso del framework. |
|
||||
| P1 | Tests compuestos cross-actor/cross-tenant | auth/session/perm/cache/http | Es donde aparecen fugas reales. |
|
||||
| P2 | Timers/random no unificados | cache/session/perm/http | Rompe determinismo en tests y trazabilidad. |
|
||||
| P2 | Persistencia/adapters DB contract-tested | auth/perm/cache | Necesario para apps reales. |
|
||||
| P2 | Metrics/diagnostics de runtime | cache/http/perm/auth | Necesario para operacion 1.0. |
|
||||
| P3 | Limpieza micro-reactiva (`SvelteSet` listeners) | lang | Pulido, bajo riesgo. |
|
||||
|
||||
## Criterio de cierre para 1.0
|
||||
|
||||
Yo no marcaria estos modulos como 1.0 hasta que se cumplan estas condiciones:
|
||||
|
||||
- La documentacion publica compila mentalmente y con snippets: ningun alias muerto, ningun servicio inventado.
|
||||
- El camino `createActiveApp({ services })` inyecta logger, bus, timers y servicios dependientes de forma explicita o documenta que no lo hace.
|
||||
- `auth`, `session`, `perm`, `cache` y `http` tienen al menos una suite compuesta de identidad completa.
|
||||
- `auth` no publica rutas/cliente que el server handler no soporte.
|
||||
- `perm` produce la misma decision en runtime y SQL compiler para paths soportados.
|
||||
- `http` y auto-refresh son testeables sin timers nativos.
|
||||
- Los adapters server de `auth` y `perm` tienen contract tests contra el modelo SQL de referencia.
|
||||
|
||||
Conclusion: la arquitectura es buena y la base esta verde. Lo que falta para 1.0 es menos glamour y mas cierre contractual: documentacion verdadera, wiring determinista y pruebas de historias completas. Esa es la parte que convierte el ecosistema en plataforma.
|
||||
|
||||
---
|
||||
|
||||
# Segunda tanda: sium, storage, timer, logger, frontend, format, connection
|
||||
|
||||
Fecha: 2026-05-04
|
||||
Alcance adicional: `sium`, `storage`, `timer`, `logger`, `frontend`, `format`, `connection`.
|
||||
Finalidad: misma que la primera tanda, buscar refactorizaciones, ampliaciones y criterios de cierre hacia version 1.0.
|
||||
|
||||
Verificacion ejecutada:
|
||||
|
||||
```txt
|
||||
npx vitest run src/arts/sium/test src/arts/storage/test src/arts/timer/test src/arts/logger/test src/arts/logger/adapters src/arts/frontend/test src/arts/format/test src/arts/format/currency/test src/arts/format/dates/test src/arts/format/numbers/test src/arts/format/units/test src/arts/connection/test
|
||||
|
||||
52 test files passed
|
||||
686 tests passed
|
||||
```
|
||||
|
||||
## Hallazgos transversales de la segunda tanda
|
||||
|
||||
### P1 - Documentacion con aliases y nombres de App obsoletos
|
||||
|
||||
La misma deuda aparece con fuerza en esta tanda. El codigo actual usa aliases semanticos:
|
||||
|
||||
```txt
|
||||
$storage, $timer, $logger, $format, $connection
|
||||
```
|
||||
|
||||
Pero varios README siguen usando:
|
||||
|
||||
```txt
|
||||
$stor, $timr, $logr, $fmts, $conn
|
||||
```
|
||||
|
||||
Tambien aparecen ejemplos con `App.Timers`, `App.Format`, `App.Frontend`, `App.Storage`, `App.Sess`, `App.Lang`, `App.createActiveConnections()` y `App.setLocale(...)`. El contrato actual de `createActiveApp({ services })` expone servicios en minuscula (`App.format`, `App.frontend`, `App.storage`, `App.session`, `App.lang`, `App.connections`) y las factories viven en `$active-app/services`.
|
||||
|
||||
Esto es P1 de documentacion contractual. Aunque el runtime este verde, una API 1.0 no puede tener docs que ensenan a importar desde aliases que ya no existen.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Pasada mecanica por README y paginas docs para reemplazar aliases viejos.
|
||||
- Script de CI que falle si aparecen `$stor`, `$timr`, `$logr`, `$fmts`, `$conn`, `App.Sess`, `App.Storage`, `App.Format`, `App.Frontend`, `App.Timers` en docs publicas salvo en secciones de migracion.
|
||||
- Tabla unica por modulo: alias real, factory real, propiedad real de `App`.
|
||||
|
||||
### P1 - Los servicios no comparten todavia un contrato de lifecycle uniforme
|
||||
|
||||
Algunos modulos siguen el contrato `ActiveEngine` o equivalente (`snapshot`, `lastError`, `disposed`, `dispose` idempotente). Otros son utilitarios activos sin `disposed` ni guardas post-dispose.
|
||||
|
||||
Casos relevantes:
|
||||
|
||||
- `storage` no impide `entry()` ni `clear()` despues de `dispose()`.
|
||||
- `frontend` mantiene setters operativos tras `dispose()`.
|
||||
- `format` root llama dispose de submodulos sin flag propio idempotente.
|
||||
|
||||
Para 1.0, todo servicio declarado en `createActiveApp({ services })` deberia tener una semantica uniforme:
|
||||
|
||||
- `dispose()` idempotente.
|
||||
- Metodos mutadores despues de dispose: o no-op documentado, o error tipado.
|
||||
- Snapshot/introspection si el servicio expone estado.
|
||||
|
||||
### P2 - El patron `CodeError` esta bien adoptado, pero quedan restos de nomenclatura antigua
|
||||
|
||||
`sium`, `storage`, `timer`, `logger` y `connection` ya extienden `CodeError` desde `$libs/errs`, que era una buena direccion. Pero hay comentarios y mensajes que todavia hablan de `stor`, `timr`, `logr` o `conn`. No rompe runtime, pero ensucia la identidad del ecosistema justo ahora que se abandono la regla de 4 letras.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Mantener `CodeError` como raiz.
|
||||
- Renombrar comentarios, mensajes y catalogos internos que digan `stor/timr/logr/conn` si el modulo ya se llama `storage/timer/logger/connection`.
|
||||
- Si `ErrCode` usa seeds de 4 letras por decision historica, documentarlo. Si no, migrarlo antes de 1.0.
|
||||
|
||||
## Modulo `sium`
|
||||
|
||||
### Estado
|
||||
|
||||
`sium` es probablemente el modulo mas completo de esta segunda tanda. Tiene core DSL, Standard Schema, introspection, codecs, lazy, domain types de color/date/time, resolucion de issues, integracion con `lang`, `CodeError` y una suite de tests amplia.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Resolver locale activo, no solo default inicial.** `createEngineSium()` captura `defaultLocale = options.locale ?? lang?.getDefaultLocale() ?? 'es'`. Si se inyecta `ActiveLang` mediante `defineEngineSium` y luego cambia el locale de `App.lang`, `resolveIssue()` sin locale explicito seguira usando el default capturado. Para 1.0, si el `lang` inyectado tiene `getLocale()`, Sium deberia usar el locale actual, o no pasar locale a `lang.t()` para dejar que Lang resuelva su estado actual.
|
||||
2. **Cerrar la migracion de errores legacy.** `src/arts/sium/errors.ts` mantiene `SIUM_ERRORS` como catalogo legacy para strings que aun no son `CodeError`. La propia nota dice que quedan sitios por migrar. Para 1.0, todos los errores de construccion/encode/decode deberian tener `ErrCode`.
|
||||
3. **Reducir fragilidad de facade manual.** `EngineSium` lista manualmente decenas de funciones. Hay tests de barrel, pero para 1.0 conviene un snapshot de surface o generacion controlada para evitar que `core` gane funciones que el engine no expone.
|
||||
4. **Separar issues de errores de programador en docs.** La distincion existe en codigo: `validate` devuelve `Result`, `decode` lanza. La documentacion debe insistir en cuando usar cada una.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `setupSium()` o presets de dominio para formularios complejos.
|
||||
- Bridge oficial con `lang`: `sium.resolveIssue(issue)` siguiendo locale activo.
|
||||
- Emision opcional de diagnostics por schema path para errores frecuentes.
|
||||
- Serializacion estable de schema para devtools y documentacion automatica.
|
||||
- Contract tests con `standard-schema` frente a Zod/Valibot/ArkType adapters.
|
||||
|
||||
## Modulo `storage`
|
||||
|
||||
### Estado
|
||||
|
||||
`storage` esta muy bien planteado: engine sync, active wrapper con `$state`, adapters memory/local/session/cookie/broadcast, envelopes con version/TTL/migration, serializers y tests suficientes. Es una pieza clave para preferencias no secretas y persistencia local.
|
||||
|
||||
### Hallazgos y refactorizaciones
|
||||
|
||||
1. **P1 - `dispose()` no cierra realmente la superficie publica.** `createEngineStorage()` marca `disposed = true`, pero `entry()`, `clear()` y `entries()` no verifican ese estado. Despues de `dispose()` se puede crear un entry nuevo sobre un bus ya limpiado y registries ya dispuestos. `ActiveStorage.dispose()` hereda el mismo problema porque delega al engine y no guarda flag propio. Para 1.0 debe haber `StorageDisposedError` o no-op documentado.
|
||||
2. **P2 - TTL usa `Date.now()` sin clock inyectable.** `decodeEnvelope()` y `encodeEnvelope()` aceptan `now`, pero `entry-runtime.ts` llama sin pasar reloj. `EngineStorageOptions` no tiene `clock`. Para tests deterministas y App wiring, conviene `clock?: { now(): number }`, inyectado desde `App.Timers.clock`.
|
||||
3. **P2 - `dynamicEntry()` depende de `$effect`, pero no hay defensa si se usa fuera de scope.** El comentario lo advierte, pero para 1.0 conviene test y error claro si Svelte lanza fuera de componente/effect root.
|
||||
4. **P2 - Top-level serializer auto-selection puede sorprender.** Esta documentado: objetos con `Date` anidada caen a JSON y no restauran Date. Para 1.0, anadir recipes de serializer por schema o integracion con Sium.
|
||||
5. **P2 - Cookies cliente no endurecidas por defecto.** `cookieAdapter()` default `secure: false`, `sameSite: 'lax'`. Es razonable para preferencias no secretas, pero docs deben repetir que no es para secretos y que auth/session cookies no pasan por `storage`.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `StorageDisposedError` y guardas post-dispose.
|
||||
- Clock inyectable desde App.
|
||||
- Adapter IndexedDB async separado o modulo nuevo, porque el contrato actual es sync.
|
||||
- Encryption/redaction adapter opt-in para preferencias sensibles, sin prometer seguridad para secretos.
|
||||
- Schema serializer: `entry('profile', defaults, { schema, serializer: siumSerializer(schema) })`.
|
||||
- Devtools: entradas vivas, namespace, adapter, defaults conflict, TTL restante.
|
||||
|
||||
## Modulo `timer`
|
||||
|
||||
### Estado
|
||||
|
||||
`timer` esta en buen estado. Es engine puro, clock inyectable, race-safe con version/id/key, abort signal por tarea, active wrapper ligero y tests robustos. Es de las piezas mas solidas del framework.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Actualizar docs antiguas.** README sigue usando `$timr` y `App.Timers`; alias real `$timer`, propiedad real `App.Timers` solo para core si se mantiene capitalizado. En codigo actual `createActiveApp()` si expone core `Timers`, asi aqui la capitalizacion es real, pero el alias no.
|
||||
2. **Consolidar exports de backoff.** `src/arts/timer/backoff.ts` re-exporta desde `$libs/timer`, y `index.ts` tambien lo expone. No es grave, pero para 1.0 conviene una unica historia: backoff vive en `libs/timer`, `arts/timer` lo reexporta en index por conveniencia.
|
||||
3. **Exponer random en helpers consumidores.** `computeBackoffDelay()` ya acepta `random`, pero `connection` no lo expone en sus reconnect options. Para 1.0, los consumidores deben poder hacer backoff determinista.
|
||||
4. **Nombrar mejor `timer` como clock/scheduler del ecosistema.** En docs debe quedar claro que no es una utilidad de UI, sino la fuente temporal para `http`, `session`, `connection`, `cache`, `orca`.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Fake clock oficial exportado para tests de ecosistema.
|
||||
- Metrics: drift, scheduled count, cancelled count, failed count por scope.
|
||||
- `cancelAll(scope)` documentado como primitive de teardown por modulo.
|
||||
- Helpers para deadline/timeout con `AbortSignal` para que `http`/`orca` no usen timers nativos.
|
||||
- Devtools de timers activos por scope.
|
||||
|
||||
## Modulo `logger`
|
||||
|
||||
### Estado
|
||||
|
||||
`logger` es potente: `Logger` comun en `$libs/logger`, `EngineLogger` extiende ese contrato, transports, buffers, batching, failure routing con `deniedFor`, adapters Sentry/Datadog/Logtail/Loki/OTel, vitals y tests grandes. Es un pilar enterprise real.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **P1/P2 - Alinear filtro global con la regla de niveles habilitados.** Los transports usan `levels?: LevelConfig`, que permite `{ [LogLevel.WARN]: { enabled: true }, ... }`. Pero el engine global aun usa `state.level` como threshold (`if (lvl < state.level) return`). Si la regla final del framework es "habilitacion por nivel, no threshold tradicional", `LoggerOptions` deberia aceptar `levels` tambien a nivel global, y `level` quedarse como shorthand o deprecated antes de 1.0.
|
||||
2. **P2 - Reducir dependencia directa de `console` dentro del engine.** `handleFailure()` emite `console.error` ademas de crear synthetic failure entry. En un entorno enterprise puede duplicar salida o saltarse transports. Para 1.0 conviene `onInternalError`, `internalTransport`, o `consoleFallback?: boolean`.
|
||||
3. **P2 - Inyectar clock/id factory opcional.** IDs fallback usan `Date.now()`/`Math.random()`, failure throttle usa `Date.now()`, timers de buffer usan `setTimeout`. Como Logger se crea antes de `Timers`, no puede depender de `App.Timers`, pero si puede aceptar `clock`, `idFactory` y `setTimeout` opcionales para tests y runtimes especiales.
|
||||
4. **P2 - `dispose()` de child logger solo advierte en DEV con `console.warn`.** Es correcto como defensa, pero debe estar documentado en API: solo el root owns lifecycle.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Global `levels` con shorthand `levelsAtLeast`.
|
||||
- Redaction pipeline: campos `token`, `password`, `secret`, `authorization`, JWT-like values.
|
||||
- Correlation helpers: `logger.withTrace(traceId)`, `logger.withActor(actorRef)`.
|
||||
- Error bridge: si `error instanceof CodeError`, derivar category/module/code automaticamente.
|
||||
- Async flush result: `flush(): Promise<FlushResult>` para transports remotos.
|
||||
- Backpressure policy para buffers grandes: drop, block, sample.
|
||||
|
||||
## Modulo `frontend`
|
||||
|
||||
### Estado
|
||||
|
||||
`frontend` es pequeno y util: locale, dir, theme, mode, reduced motion/sound, density y aplicacion DOM via `ActiveDom` o helper de `$libs/dom`. La factory ya integra `dom` y `lang` si existen. Pero esta menos maduro que los demas modulos.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **P1 - Docs antiguas tras service schema.** README afirma que `createActiveApp()` construye `Frontend` y usa `App.Lang`/`App.Dom`; ahora `frontend` se declara en `services` y se expone como `App.frontend`.
|
||||
2. **P2 - Lifecycle incompleto.** Tras `dispose()`, los setters (`setLocale`, `setTheme`, etc.) siguen funcionando y pueden aplicar DOM. Para 1.0 debe haber flag `disposed` y semantica uniforme.
|
||||
3. **P2 - Persistencia de preferencias quedo fuera.** La factory dice que storage persistence es responsabilidad de la app. Es una buena separacion, pero para 1.0 conviene un preset/helper oficial, porque tema/densidad/dir son caso principal de `storage`.
|
||||
4. **P2 - Falta snapshot unico.** Hay getters individuales, pero no `snapshot()` con `{ locale, dir, theme, mode, reducedMotion, reducedSound, density }`. Para UI/debug/tests es mucho mas comodo.
|
||||
5. **P3 - Validacion de valores.** `setDensity`, `setMode`, `setDir` aceptan strings tipados en TS, pero runtime JS podria pasar valores invalidos. Si es API publica, conviene validar o documentar TypeScript-only.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `snapshot()` y `onChange(snapshot)`.
|
||||
- Preset `persistFrontendPreferences(storage)`.
|
||||
- Eventos de bus opcionales: frontend.preference.changed.
|
||||
- Media query injector para tests SSR/browser.
|
||||
- Documentar CSS contract: atributos `data-theme`, `data-mode`, `dir`, density, reduced motion.
|
||||
|
||||
## Modulo `format`
|
||||
|
||||
### Estado
|
||||
|
||||
`format` esta bien organizado: numbers, currency, dates y units como submodulos, engines y active wrappers, locale source comun e integracion con `lang` desde factory. Tests cubren cada subdominio. Es funcional y extensible.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **P1 - Docs con `$fmts` y `App.Format`.** Alias real `$format`, servicio real `App.format`. Ademas hay un typo documental: `import { createRates } from '$formats/currency'` cuando el alias real es `$format`.
|
||||
2. **P2 - `createActiveFormat().dispose()` no tiene guard idempotente propio.** Los submodulos tienen runtime dispose, pero el root deberia seguir la regla general del ecosistema.
|
||||
3. **P2 - Rates usa `Date.now()` por defecto.** `createRates({ now })` acepta inyeccion, pero `defineActiveFormat` no ofrece wiring con `Timers.clock`. Para 1.0, usar clock de App si se declaran rates con expiracion.
|
||||
4. **P2 - Cache global de Intl.NumberFormat sin limite.** `engine-currency.ts` mantiene `formatCache` module-global. En apps multi-locale/multi-currency/larga sesion puede crecer indefinidamente. Conviene LRU pequeno o cache por engine con `dispose()`.
|
||||
5. **P2 - Locale source doble puede duplicar notificaciones.** `createActiveFormat.setLocale()` actualiza localeState y cada submodulo manualmente. Funciona, pero para 1.0 conviene una sola fuente reactiva que notifique y submodulos se sincronicen una vez.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Integracion con `lang` interpolation: formatters nombrados para `{{price | currency}}`.
|
||||
- Formatter registry: `format.register('filesize', fn)`.
|
||||
- Ranges: date range, number range, relative time, list format, display names.
|
||||
- Rates provider HTTP/cache bridge con stale-if-error.
|
||||
- Unit catalog ampliado y aliases por dominio de negocio.
|
||||
- Tests por locale de alto riesgo: `ar`, `en-US`, `es-AR`, `fr-FR`, `de-DE`.
|
||||
|
||||
## Modulo `connection`
|
||||
|
||||
### Estado
|
||||
|
||||
`connection` mejoro mucho desde el primer audit: ya no es un monolito puro, ahora tiene piezas separadas para acks, heartbeat, reconnect, session wiring, channel registry, transport runtime, sender, serializer y active wrapper. Tambien exige `TimerScheduler`, que es correcto. Aun asi, es el modulo con mas riesgo operacional de esta tanda.
|
||||
|
||||
### Hallazgos y refactorizaciones
|
||||
|
||||
1. **P1 - `autoReauthOn` existe en tipos pero no se usa.** `EngineConnectionsOptions` declara `autoReauthOn?: ConnectionAutoReauthOn`, pero no aparece en `engine-connections.ts`, `connection.ts` ni presets. Es una opcion publica sin efecto. Para 1.0 hay que implementarla o eliminarla hasta que `orca`/presets la usen.
|
||||
2. **P1 - README desactualizado.** Usa `$conn`, `App.createActiveConnections()`, `App.Sess`, `App.Bus`, `App.Timers`. El codigo real usa `$connection`, `defineActiveConnections`, `App.connections`, `core.timers`, y el bus no se consume directamente salvo wiring/presets.
|
||||
3. **P2 - WebSocket coverage es casi inexistente.** `websocket.test.ts` solo valida error cuando WebSocket no existe. Faltan tests de open/message/close/error, binaryType, protocols, URL factory, bufferedAmount y cleanup de listeners.
|
||||
4. **P2 - `connection.ts` sigue concentrando demasiado wiring.** Aunque bajo de tamano frente al monolito anterior, sigue siendo el composition point de 10 KB con mucho cierre mutable (`disposed`, `intentionalClose`, lifecycle, detachSession, detachBrowserReconnect). Para 1.0 conviene dividir construction runtime en una factory interna que devuelva partes o un `ConnectionRuntimeContext`.
|
||||
5. **P2 - Reauth concurrente no esta serializada.** `wireConnectionSession()` puede disparar `reauthenticate()` en cambios de sesion sucesivos sin singleflight/cancelacion. Si llega refresh + external changed, pueden salir dos auth frames. Para 1.0, `reauthenticate()` deberia ser singleflight o tener politica.
|
||||
6. **P2 - Reconnect backoff no expone random determinista.** Usa `computeBackoffDelay()` sin pasar `random`; aunque el helper lo soporta, connection no lo deja configurar.
|
||||
7. **P2 - Payloads de channels no validan schema.** Tipado TS ayuda en compile-time, pero los frames de red son `unknown`. Para 1.0, una opcion por channel con Sium/StandardSchema reduciria bugs de mensajes malformados.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Implementar o retirar `autoReauthOn`.
|
||||
- `ConnectionContext` para acciones internas: publish diagnostics/events, timers, logger, abort signal.
|
||||
- Singleflight para connect/reconnect/reauthenticate.
|
||||
- WebSocket test suite real con mock constructor.
|
||||
- Channel schemas: `channel('chat', { incoming: { message: schema }, outgoing: { send: schema } })`.
|
||||
- Backpressure policies mas completas: buffer por topic, drop-oldest/drop-newest, metrics.
|
||||
- Reconnect policies documentadas: online/visible, queue/drop/replace si hay intento en vuelo.
|
||||
- Integracion con `orca`: reauth/disconnect como accion opt-in ante identity change, no acoplamiento directo a session/cache/perm.
|
||||
|
||||
## Roadmap recomendado para esta segunda tanda
|
||||
|
||||
### Sprint A - Docs y naming
|
||||
|
||||
- Corregir aliases obsoletos en `sium`, `storage`, `timer`, `logger`, `frontend`, `format`, `connection`.
|
||||
- Reemplazar ejemplos `App.X` capitalizados por `App.x` servicios, excepto core reales (`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`) si se mantienen asi.
|
||||
- Actualizar README de `connection`, `frontend`, `format` y `storage` antes de tocarlos mas: ahora mismo son los que mas pueden confundir.
|
||||
|
||||
### Sprint B - Lifecycle uniforme
|
||||
|
||||
- `storage`: error/no-op post-dispose.
|
||||
- `frontend`: flag disposed y snapshot.
|
||||
- `format`: root dispose idempotente.
|
||||
- Tests de lifecycle para todos los servicios declarables.
|
||||
|
||||
### Sprint C - Determinismo temporal
|
||||
|
||||
- `storage`: clock en `EngineStorageOptions`.
|
||||
- `format`: rates con clock de App.
|
||||
- `logger`: opciones `clock`, `idFactory`, `timer` o documentar excepcion por ser core bootstrap.
|
||||
- `connection`: random inyectable para reconnect.
|
||||
|
||||
### Sprint D - Riesgos operacionales
|
||||
|
||||
- `connection`: implementar/eliminar `autoReauthOn`, singleflight de reauth, WebSocket tests.
|
||||
- `logger`: global levels por habilitacion si esa es la regla final.
|
||||
- `sium`: locale activo con Lang.
|
||||
- `storage`: no operar despues de dispose.
|
||||
|
||||
## Prioridad resumida de la segunda tanda
|
||||
|
||||
| Prioridad | Tema | Modulos | Motivo |
|
||||
|---|---|---|---|
|
||||
| P1 | Aliases/docs obsoletos | sium/storage/timer/logger/frontend/format/connection | API 1.0 no puede ensenar imports inexistentes. |
|
||||
| P1 | `storage.dispose()` no cierra superficie | storage | Permite crear entradas tras teardown. |
|
||||
| P1 | `autoReauthOn` sin efecto | connection | Opcion publica enganosa en un modulo critico. |
|
||||
| P1/P2 | Filtro global por threshold vs habilitacion por nivel | logger | Debe alinearse con la regla final del ecosistema. |
|
||||
| P2 | Locale activo no seguido por Sium | sium/lang | Validaciones pueden resolver mensajes en locale inicial. |
|
||||
| P2 | Determinismo de tiempo incompleto | storage/logger/format/connection | Falta clock/random/timer injection en caminos 1.0. |
|
||||
| P2 | Lifecycle incompleto | frontend/format/storage | Consistencia de servicios declarables. |
|
||||
| P2 | WebSocket tests escasos | connection | Superficie critica con cobertura baja. |
|
||||
|
||||
## Criterio de cierre 1.0 para esta tanda
|
||||
|
||||
- Ningun README usa alias viejo ni propiedad App antigua.
|
||||
- Todo servicio declarable tiene lifecycle post-dispose definido y probado.
|
||||
- `storage`, `format.rates`, `connection.reconnect` y `logger` tienen historia determinista o excepcion documentada.
|
||||
- `sium` resuelve issues con el locale activo cuando se integra con `lang`.
|
||||
- `connection` no expone opciones muertas y tiene tests reales de WebSocket.
|
||||
- `logger` deja cerrada la decision global: threshold o per-level enable, pero no una mezcla confusa.
|
||||
- `frontend` tiene snapshot y persistencia oficial opt-in con `storage`.
|
||||
|
||||
Conclusion de la segunda tanda: `timer`, `sium`, `logger` y `storage` tienen una base muy fuerte; `format` esta sano pero necesita pulido de cache/locales; `frontend` necesita madurar contrato; `connection` es potente, pero debe cerrar opciones muertas, concurrencia de reauth y cobertura WebSocket antes de poder llamarse 1.0.
|
||||
@ -0,0 +1,173 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createConnectionRequestRuntime } from '../connection-requests.ts';
|
||||
import type { ConnectionAckRegistry } from '../acks.ts';
|
||||
import type { ConnectionSender } from '../sender.ts';
|
||||
import type {
|
||||
ConnectionAckResult,
|
||||
ConnectionFrame,
|
||||
ConnectionSendResult
|
||||
} from '../types.ts';
|
||||
|
||||
interface FakeAckController {
|
||||
readonly registry: ConnectionAckRegistry;
|
||||
resolveLatest(result: ConnectionAckResult<unknown>): void;
|
||||
}
|
||||
|
||||
function fakeAckRegistry(): FakeAckController {
|
||||
const waiters = new Map<string, (result: ConnectionAckResult<unknown>) => void>();
|
||||
const order: string[] = [];
|
||||
|
||||
const registry: ConnectionAckRegistry = {
|
||||
wait<TResult>(id: string): Promise<ConnectionAckResult<TResult>> {
|
||||
return new Promise<ConnectionAckResult<TResult>>((resolve) => {
|
||||
waiters.set(id, resolve as (result: ConnectionAckResult<unknown>) => void);
|
||||
order.push(id);
|
||||
});
|
||||
},
|
||||
resolve(id, result) {
|
||||
const waiter = waiters.get(id);
|
||||
if (waiter === undefined) return;
|
||||
waiters.delete(id);
|
||||
const idx = order.indexOf(id);
|
||||
if (idx !== -1) order.splice(idx, 1);
|
||||
waiter(result);
|
||||
},
|
||||
resolveAll(result) {
|
||||
for (const id of [...waiters.keys()]) {
|
||||
registry.resolve(id, result);
|
||||
}
|
||||
},
|
||||
resolveFromFrame(frame: ConnectionFrame) {
|
||||
if (frame.replyTo === undefined) return;
|
||||
registry.resolve(frame.replyTo, { ok: true, payload: frame.payload });
|
||||
},
|
||||
mapSendFailure(_: ConnectionSendResult): ConnectionAckResult<unknown> {
|
||||
return { ok: true, payload: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
registry,
|
||||
resolveLatest(result) {
|
||||
const id = order[0];
|
||||
if (id !== undefined) registry.resolve(id, result);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fakeSender(): { sender: ConnectionSender; sent: ConnectionFrame[] } {
|
||||
const sent: ConnectionFrame[] = [];
|
||||
const sender: ConnectionSender = {
|
||||
sendFrame: vi.fn(async (frame: ConnectionFrame): Promise<ConnectionSendResult> => {
|
||||
sent.push(frame);
|
||||
return { ok: true };
|
||||
}),
|
||||
flushBuffer: vi.fn(async () => {}),
|
||||
clearBuffer: vi.fn(() => {})
|
||||
};
|
||||
return { sender, sent };
|
||||
}
|
||||
|
||||
describe('connection request runtime — runAuth singleflight', () => {
|
||||
it('coalesces concurrent runAuth() calls into a single auth round', async () => {
|
||||
const acks = fakeAckRegistry();
|
||||
const { sender, sent } = fakeSender();
|
||||
let getAuthCalls = 0;
|
||||
let nextIdCalls = 0;
|
||||
|
||||
const runtime = createConnectionRequestRuntime({
|
||||
auth: {
|
||||
getAuth: () => {
|
||||
getAuthCalls++;
|
||||
return { token: `t-${getAuthCalls}` };
|
||||
}
|
||||
},
|
||||
isConnected: () => true,
|
||||
nextId: () => `id-${++nextIdCalls}`,
|
||||
acks: acks.registry,
|
||||
sender
|
||||
});
|
||||
|
||||
const a = runtime.runAuth();
|
||||
const b = runtime.runAuth();
|
||||
const c = runtime.runAuth();
|
||||
|
||||
// Microtask hop so getAuth() resolves and the auth frame is sent.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(getAuthCalls).toBe(1);
|
||||
expect(sent).toHaveLength(1);
|
||||
expect(sent[0].type).toBe('connection.auth');
|
||||
|
||||
acks.resolveLatest({ ok: true, payload: undefined });
|
||||
|
||||
await expect(a).resolves.toEqual({ ok: true });
|
||||
await expect(b).resolves.toEqual({ ok: true });
|
||||
await expect(c).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('starts a fresh auth round after the previous one settles', async () => {
|
||||
const acks = fakeAckRegistry();
|
||||
const { sender, sent } = fakeSender();
|
||||
let getAuthCalls = 0;
|
||||
let nextIdCalls = 0;
|
||||
|
||||
const runtime = createConnectionRequestRuntime({
|
||||
auth: {
|
||||
getAuth: () => {
|
||||
getAuthCalls++;
|
||||
return { token: `t-${getAuthCalls}` };
|
||||
}
|
||||
},
|
||||
isConnected: () => true,
|
||||
nextId: () => `id-${++nextIdCalls}`,
|
||||
acks: acks.registry,
|
||||
sender
|
||||
});
|
||||
|
||||
const first = runtime.runAuth();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
acks.resolveLatest({ ok: true, payload: undefined });
|
||||
await expect(first).resolves.toEqual({ ok: true });
|
||||
|
||||
const second = runtime.runAuth();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(getAuthCalls).toBe(2);
|
||||
expect(sent).toHaveLength(2);
|
||||
|
||||
acks.resolveLatest({ ok: true, payload: undefined });
|
||||
await expect(second).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('shares a rejected outcome with all in-flight callers', async () => {
|
||||
const acks = fakeAckRegistry();
|
||||
const { sender } = fakeSender();
|
||||
const runtime = createConnectionRequestRuntime({
|
||||
auth: {
|
||||
getAuth: () => ({})
|
||||
},
|
||||
isConnected: () => true,
|
||||
nextId: () => 'id-1',
|
||||
acks: acks.registry,
|
||||
sender
|
||||
});
|
||||
|
||||
const a = runtime.runAuth();
|
||||
const b = runtime.runAuth();
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
acks.resolveLatest({ ok: false, reason: 'connection.ack.rejected', error: 'bad token' });
|
||||
|
||||
const resA = await a;
|
||||
const resB = await b;
|
||||
expect(resA.ok).toBe(false);
|
||||
expect(resB.ok).toBe(false);
|
||||
expect(resA).toEqual(resB);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createConnectionReconnectPolicy } from '../reconnect.ts';
|
||||
|
||||
describe('connection reconnect policy', () => {
|
||||
it('honors maxAttempts and rejects further attempts', () => {
|
||||
const policy = createConnectionReconnectPolicy({ maxAttempts: 2, jitterMs: 0 });
|
||||
const first = policy.next(0);
|
||||
expect(first.ok).toBe(true);
|
||||
|
||||
const second = policy.next(1);
|
||||
expect(second.ok).toBe(true);
|
||||
|
||||
const third = policy.next(2);
|
||||
expect(third).toEqual({ ok: false, reconnectAttempt: 2, maxAttempts: 2 });
|
||||
});
|
||||
|
||||
it('threads the injected `random` source through computeBackoffDelay', () => {
|
||||
// random() = 1 → jitter = +jitterMs (max positive offset)
|
||||
const positivePolicy = createConnectionReconnectPolicy({
|
||||
minDelayMs: 100,
|
||||
maxDelayMs: 1000,
|
||||
factor: 1, // no exponential growth so we read the jitter cleanly
|
||||
jitterMs: 50,
|
||||
random: () => 1
|
||||
});
|
||||
const positive = positivePolicy.next(0);
|
||||
expect(positive.ok).toBe(true);
|
||||
if (!positive.ok) return;
|
||||
expect(positive.delayMs).toBe(150);
|
||||
|
||||
// random() = 0 → jitter = -jitterMs (max negative offset, clamped to min)
|
||||
const negativePolicy = createConnectionReconnectPolicy({
|
||||
minDelayMs: 100,
|
||||
maxDelayMs: 1000,
|
||||
factor: 1,
|
||||
jitterMs: 50,
|
||||
random: () => 0
|
||||
});
|
||||
const negative = negativePolicy.next(0);
|
||||
expect(negative.ok).toBe(true);
|
||||
if (!negative.ok) return;
|
||||
// 100 - 50 = 50, but the helper clamps to minDelayMs.
|
||||
expect(negative.delayMs).toBe(50);
|
||||
});
|
||||
|
||||
it('disabled reconnect is detected via isEnabled()', () => {
|
||||
const policy = createConnectionReconnectPolicy(false);
|
||||
expect(policy.disabled).toBe(true);
|
||||
expect(policy.isEnabled({ disposed: false, intentionalClose: false })).toBe(false);
|
||||
});
|
||||
|
||||
it('refuses reconnection when disposed or after intentional close', () => {
|
||||
const policy = createConnectionReconnectPolicy({ enabled: true });
|
||||
expect(policy.isEnabled({ disposed: true, intentionalClose: false })).toBe(false);
|
||||
expect(policy.isEnabled({ disposed: false, intentionalClose: true })).toBe(false);
|
||||
expect(policy.isEnabled({ disposed: false, intentionalClose: false })).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,582 @@
|
||||
# Prefs
|
||||
|
||||
`prefs` defines the application preference layer for Active.
|
||||
|
||||
It is intentionally isolated for now. It is not wired into `active-app` yet, and
|
||||
no existing artifact should depend on it until the surrounding modules are ready
|
||||
to consume narrow preference ports.
|
||||
|
||||
## Purpose
|
||||
|
||||
`prefs` owns user/application preferences that influence how other artifacts
|
||||
behave, render or format data.
|
||||
|
||||
It answers questions like:
|
||||
|
||||
- What locale does the user prefer?
|
||||
- What locale should formatting use?
|
||||
- What currency should money defaults use?
|
||||
- Which unit system should measurements use?
|
||||
- Which timezone should date/time formatting use?
|
||||
- Which UI density, theme or motion preference should the frontend apply?
|
||||
|
||||
It does not translate, format, persist by itself, apply DOM changes directly, or
|
||||
own technical runtime settings.
|
||||
|
||||
The intended boundary is:
|
||||
|
||||
```txt
|
||||
Prefs = preference state
|
||||
Lang = translation resolution
|
||||
Format = number/date/currency/unit formatting
|
||||
Frontend = DOM/UI application
|
||||
Storage = persistence
|
||||
Bus = event publication
|
||||
Orca = optional app-level orchestration
|
||||
```
|
||||
|
||||
## Why This Exists
|
||||
|
||||
Until now, `locale` has naturally drifted toward `lang` because translation is
|
||||
the most visible locale consumer. That coupling becomes limiting:
|
||||
|
||||
- Applications may need locale-aware formatting without translations.
|
||||
- `format` needs locale, currency, timezone and unit preferences.
|
||||
- `frontend` may need direction, theme, density and motion preferences.
|
||||
- `lang` should not become the authority for every user preference.
|
||||
|
||||
`prefs` separates preference ownership from preference consumption.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
`prefs` is not:
|
||||
|
||||
- A translation engine.
|
||||
- A formatting engine.
|
||||
- A storage adapter.
|
||||
- A feature flag system.
|
||||
- A tenant configuration system.
|
||||
- A security policy system.
|
||||
- A server settings registry.
|
||||
- A generic key/value bag.
|
||||
|
||||
If a value configures framework behavior, infrastructure, security, routes,
|
||||
providers, permissions, transport, logging or deployment, it does not belong in
|
||||
`prefs`.
|
||||
|
||||
## Future Location
|
||||
|
||||
Planned structure:
|
||||
|
||||
```txt
|
||||
src/
|
||||
libs/
|
||||
prefs/
|
||||
index.ts
|
||||
consts.ts
|
||||
types.ts
|
||||
ports.ts
|
||||
guards.ts
|
||||
|
||||
arts/
|
||||
prefs/
|
||||
index.ts
|
||||
README.md
|
||||
engine-prefs.ts
|
||||
active-prefs.svelte.ts
|
||||
events.ts
|
||||
persistence.ts
|
||||
test/
|
||||
```
|
||||
|
||||
`libs/prefs` will contain contracts and shared types.
|
||||
|
||||
`arts/prefs` will contain the runtime implementation.
|
||||
|
||||
`active-app` will eventually instantiate it and expose it as `App.Prefs`, but
|
||||
that integration is deliberately postponed.
|
||||
|
||||
## Public Shape
|
||||
|
||||
The final public API should feel simple from application code:
|
||||
|
||||
```ts
|
||||
const Prefs = createActivePrefs({
|
||||
initial: {
|
||||
locale: { preferredLocale: 'es-MX', contentLocale: 'es-ES', fallbackContentLocale: 'en-US' },
|
||||
format: { currency: 'EUR', unitSystem: 'metric', timezone: 'Europe/Madrid' },
|
||||
ui: { theme: 'system', density: 'comfortable' }
|
||||
}
|
||||
});
|
||||
|
||||
Prefs.setLocale('en-US');
|
||||
Prefs.setCurrency('USD');
|
||||
Prefs.setUnitSystem('imperial');
|
||||
Prefs.setTheme('dark');
|
||||
```
|
||||
|
||||
After `active-app` integration:
|
||||
|
||||
```ts
|
||||
App.Prefs.setLocale('es-ES');
|
||||
App.Prefs.setCurrency('EUR');
|
||||
App.Prefs.setUnitSystem('metric');
|
||||
|
||||
App.Lang.t('#?checkout.title');
|
||||
App.Format.currency(19.95);
|
||||
```
|
||||
|
||||
## Snapshot Model
|
||||
|
||||
The snapshot should be explicit and serializable.
|
||||
|
||||
```ts
|
||||
export interface PrefsSnapshot {
|
||||
readonly locale: PrefsLocaleSnapshot;
|
||||
readonly format: PrefsFormatSnapshot;
|
||||
readonly ui: PrefsUiSnapshot;
|
||||
readonly version: number;
|
||||
readonly updatedAt?: number;
|
||||
}
|
||||
|
||||
export interface PrefsLocaleSnapshot {
|
||||
/** User preference, e.g. from profile, storage, browser or explicit UI choice. */
|
||||
readonly preferredLocale?: string;
|
||||
/** Locale detected from `Accept-Language`, `navigator.languages` or another source. */
|
||||
readonly detectedLocale?: string;
|
||||
/** Locale used by translated content/catalogs. It must exist in `lang`. */
|
||||
readonly contentLocale: string;
|
||||
readonly fallbackContentLocale?: string;
|
||||
readonly source?: PrefsLocaleSource;
|
||||
readonly contentResolution?: PrefsLocaleResolution;
|
||||
readonly direction?: 'ltr' | 'rtl' | 'auto';
|
||||
}
|
||||
|
||||
export type PrefsLocaleSource =
|
||||
| 'explicit'
|
||||
| 'persisted'
|
||||
| 'server'
|
||||
| 'browser'
|
||||
| 'tenant'
|
||||
| 'route'
|
||||
| 'default';
|
||||
|
||||
export type PrefsLocaleResolution =
|
||||
| 'exact'
|
||||
| 'language_match'
|
||||
| 'fallback'
|
||||
| 'default'
|
||||
| 'unsupported';
|
||||
|
||||
export interface PrefsFormatSnapshot {
|
||||
readonly timezone?: string;
|
||||
readonly currency?: string;
|
||||
readonly unitSystem?: 'metric' | 'imperial' | 'auto';
|
||||
readonly numberingSystem?: string;
|
||||
readonly hourCycle?: 'h11' | 'h12' | 'h23' | 'h24' | 'auto';
|
||||
}
|
||||
|
||||
export interface PrefsUiSnapshot {
|
||||
readonly theme?: 'light' | 'dark' | 'system';
|
||||
readonly colorScheme?: string;
|
||||
readonly density?: 'compact' | 'comfortable' | 'spacious';
|
||||
readonly reducedMotion?: boolean | 'system';
|
||||
readonly reducedTransparency?: boolean | 'system';
|
||||
}
|
||||
```
|
||||
|
||||
The snapshot is not a dumping ground. New groups should only be added when more
|
||||
than one artifact can reasonably consume them or when the preference represents
|
||||
an application-level user choice.
|
||||
|
||||
## Engine Contract
|
||||
|
||||
`EnginePrefs` is the non-Svelte core. It should be usable in unit tests,
|
||||
server-side setup and non-reactive consumers.
|
||||
|
||||
```ts
|
||||
export interface EnginePrefs {
|
||||
readonly kind: 'prefs';
|
||||
|
||||
getSnapshot(): PrefsSnapshot;
|
||||
|
||||
patch(patch: PrefsPatch): PrefsSnapshot;
|
||||
reset(next?: Partial<PrefsSnapshot>): PrefsSnapshot;
|
||||
|
||||
setLocale(locale: string): PrefsSnapshot;
|
||||
setPreferredLocale(locale: string | undefined): PrefsSnapshot;
|
||||
adoptDetectedLocale(locale: string | undefined, source: PrefsLocaleSource): PrefsSnapshot;
|
||||
resolveContentLocale(input: PrefsContentLocaleResolveInput): PrefsSnapshot;
|
||||
setFallbackContentLocale(locale: string | undefined): PrefsSnapshot;
|
||||
setDirection(direction: PrefsLocaleSnapshot['direction']): PrefsSnapshot;
|
||||
|
||||
setTimezone(timezone: string | undefined): PrefsSnapshot;
|
||||
setCurrency(currency: string | undefined): PrefsSnapshot;
|
||||
setUnitSystem(unitSystem: PrefsFormatSnapshot['unitSystem']): PrefsSnapshot;
|
||||
setNumberingSystem(numberingSystem: string | undefined): PrefsSnapshot;
|
||||
setHourCycle(hourCycle: PrefsFormatSnapshot['hourCycle']): PrefsSnapshot;
|
||||
|
||||
setTheme(theme: PrefsUiSnapshot['theme']): PrefsSnapshot;
|
||||
setColorScheme(colorScheme: string | undefined): PrefsSnapshot;
|
||||
setDensity(density: PrefsUiSnapshot['density']): PrefsSnapshot;
|
||||
setReducedMotion(value: PrefsUiSnapshot['reducedMotion']): PrefsSnapshot;
|
||||
setReducedTransparency(value: PrefsUiSnapshot['reducedTransparency']): PrefsSnapshot;
|
||||
|
||||
onChange(handler: PrefsChangeHandler): PrefsUnsubscribe;
|
||||
dispose(): void;
|
||||
}
|
||||
```
|
||||
|
||||
## Active Contract
|
||||
|
||||
`ActivePrefs` wraps the engine with Svelte runes and exposes a reactive snapshot.
|
||||
|
||||
```ts
|
||||
export interface ActivePrefs extends EnginePrefs {
|
||||
readonly state: {
|
||||
readonly snapshot: PrefsSnapshot;
|
||||
readonly pending: boolean;
|
||||
readonly lastError: unknown;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The active layer must not invent separate semantics. If `EnginePrefs.setLocale`
|
||||
updates the snapshot and emits a change event, `ActivePrefs.setLocale` must do
|
||||
the same.
|
||||
|
||||
## Narrow Ports
|
||||
|
||||
Consumers should not depend on the full `Prefs` object.
|
||||
|
||||
Each artifact should consume the smallest interface it needs:
|
||||
|
||||
```ts
|
||||
export interface LocalePrefsPort {
|
||||
getPreferredLocale(): string | undefined;
|
||||
getDetectedLocale(): string | undefined;
|
||||
getContentLocale(): string;
|
||||
onLocaleChange(handler: (locale: string) => void): PrefsUnsubscribe;
|
||||
}
|
||||
|
||||
export interface FormatPrefsPort {
|
||||
getCurrency(): string | undefined;
|
||||
getUnitSystem(): 'metric' | 'imperial' | 'auto' | undefined;
|
||||
getTimezone(): string | undefined;
|
||||
onFormatPrefsChange(handler: (snapshot: PrefsFormatSnapshot) => void): PrefsUnsubscribe;
|
||||
}
|
||||
|
||||
export interface UiPrefsPort {
|
||||
getTheme(): PrefsUiSnapshot['theme'];
|
||||
getDensity(): PrefsUiSnapshot['density'];
|
||||
getReducedMotion(): PrefsUiSnapshot['reducedMotion'];
|
||||
onUiPrefsChange(handler: (snapshot: PrefsUiSnapshot) => void): PrefsUnsubscribe;
|
||||
}
|
||||
```
|
||||
|
||||
Expected consumers:
|
||||
|
||||
```txt
|
||||
Lang consumes LocalePrefsPort and uses contentLocale
|
||||
Format consumes LocalePrefsPort + FormatPrefsPort
|
||||
Frontend consumes LocalePrefsPort + UiPrefsPort
|
||||
Storage persists PrefsSnapshot through an adapter/bridge
|
||||
Bus receives preference change events
|
||||
Orca may orchestrate app-level reactions to preference changes
|
||||
```
|
||||
|
||||
This keeps module dependencies pointed at contracts, not at the concrete
|
||||
`ActivePrefs` instance.
|
||||
|
||||
## Events
|
||||
|
||||
All event names must be constants. No module should publish ad-hoc strings.
|
||||
|
||||
Planned constants:
|
||||
|
||||
```ts
|
||||
export const PREFS_EVENT_CHANGED = 'prefs.changed';
|
||||
export const PREFS_EVENT_LOCALE_CHANGED = 'prefs.locale.changed';
|
||||
export const PREFS_EVENT_FORMAT_CHANGED = 'prefs.format.changed';
|
||||
export const PREFS_EVENT_UI_CHANGED = 'prefs.ui.changed';
|
||||
export const PREFS_EVENT_RESET = 'prefs.reset';
|
||||
```
|
||||
|
||||
Events should contain safe, serializable payloads only:
|
||||
|
||||
```ts
|
||||
export interface PrefsChangedPayload {
|
||||
readonly previous: PrefsSnapshot;
|
||||
readonly next: PrefsSnapshot;
|
||||
readonly changed: readonly PrefsChangedPath[];
|
||||
readonly cause?: 'user' | 'system' | 'storage' | 'route' | 'tenant' | 'test';
|
||||
}
|
||||
```
|
||||
|
||||
Events must not contain secrets, tokens, cookies or credentials.
|
||||
|
||||
## Locale Negotiation
|
||||
|
||||
`prefs` must distinguish locale domains. A user locale can be perfectly valid for
|
||||
`Intl` formatting while not having any translation catalog in `lang`.
|
||||
|
||||
Core rule:
|
||||
|
||||
```txt
|
||||
preferredLocale = what the user/browser/server says the user wants
|
||||
contentLocale = supported locale used by translated content/catalogs
|
||||
```
|
||||
|
||||
`contentLocale` and the locale used by `Format` are allowed to diverge. `Format`
|
||||
does not need a separate stored locale; it derives from the locale preference
|
||||
chain.
|
||||
|
||||
Example:
|
||||
|
||||
```txt
|
||||
User/browser locale: es-MX
|
||||
Application locales: en-US, es-ES, fr-FR
|
||||
Content locale: es-ES
|
||||
Format uses: es-MX
|
||||
Content resolution: language_match
|
||||
```
|
||||
|
||||
Another example:
|
||||
|
||||
```txt
|
||||
User/browser locale: ca-ES
|
||||
Application locales: en-US, es-ES, fr-FR
|
||||
Content locale: en-US
|
||||
Format uses: ca-ES
|
||||
Content resolution: fallback
|
||||
```
|
||||
|
||||
This distinction matters because different artifacts consume different pieces:
|
||||
|
||||
```txt
|
||||
Lang should use contentLocale because catalogs may not exist for preferredLocale.
|
||||
Format may use preferredLocale, then detectedLocale, then contentLocale.
|
||||
Frontend may use direction resolved from preferredLocale, detectedLocale or contentLocale.
|
||||
Storage should persist the user's preference, not only the fallback result.
|
||||
```
|
||||
|
||||
`prefs` should expose enough information for UI to explain what happened:
|
||||
|
||||
```ts
|
||||
export interface PrefsContentLocaleResolveInput {
|
||||
readonly preferredLocale?: string;
|
||||
readonly detectedLocale?: string;
|
||||
readonly supportedContentLocales: readonly string[];
|
||||
readonly fallbackContentLocale: string;
|
||||
readonly source?: PrefsLocaleSource;
|
||||
}
|
||||
```
|
||||
|
||||
The content locale resolver should be deterministic:
|
||||
|
||||
- Exact match wins.
|
||||
- Language-only match may be used when explicitly enabled by policy.
|
||||
- Fallback content locale is used when no supported content locale matches.
|
||||
- The original preferred/detected locale is preserved for future use.
|
||||
- Unsupported user preferences are not discarded silently.
|
||||
|
||||
The format locale resolver is different. It should prefer the most specific user
|
||||
locale that the runtime can format:
|
||||
|
||||
```txt
|
||||
preferredLocale
|
||||
detectedLocale
|
||||
contentLocale
|
||||
framework default
|
||||
```
|
||||
|
||||
This lets an application display translated UI in `es-ES` while still formatting
|
||||
numbers, dates, currencies and units using `es-MX` conventions.
|
||||
|
||||
## Detection
|
||||
|
||||
Locale detection may happen on the server or in the browser.
|
||||
|
||||
Server-side detection can use:
|
||||
|
||||
- User profile preference.
|
||||
- Session/account preference.
|
||||
- Cookie preference.
|
||||
- `Accept-Language`.
|
||||
- Tenant/application default.
|
||||
|
||||
Browser-side detection can use:
|
||||
|
||||
- `navigator.languages`.
|
||||
- `navigator.language`.
|
||||
- Browser color scheme and motion preferences for UI prefs.
|
||||
- Browser timezone from `Intl.DateTimeFormat().resolvedOptions().timeZone`.
|
||||
|
||||
Hydration must remain deterministic. The recommended rule is:
|
||||
|
||||
```txt
|
||||
Server snapshot initializes Prefs.
|
||||
Browser detection may refine missing values after hydration.
|
||||
Browser detection must not overwrite an explicit or persisted user preference.
|
||||
```
|
||||
|
||||
If both server and browser detect different locales, the source priority rules
|
||||
decide which value wins. The losing value can remain recorded as `detectedLocale`
|
||||
for diagnostics and UI messaging.
|
||||
|
||||
## Source Priority
|
||||
|
||||
Preference resolution often has several possible sources. The module should
|
||||
support this without hiding the rules.
|
||||
|
||||
Recommended priority:
|
||||
|
||||
```txt
|
||||
explicit runtime override
|
||||
user persisted preference
|
||||
server user/session preference
|
||||
tenant/application default
|
||||
route/layout override
|
||||
browser/system preference
|
||||
framework default
|
||||
```
|
||||
|
||||
For v0, this can be represented simply through `initial` plus explicit setter
|
||||
calls. A future version can add a resolver if needed.
|
||||
|
||||
## Persistence
|
||||
|
||||
Persistence should be optional and external.
|
||||
|
||||
`Prefs` should not import `storage` directly. Instead, a bridge can connect both:
|
||||
|
||||
```ts
|
||||
createPrefsStorageBridge({
|
||||
prefs,
|
||||
storage,
|
||||
key: 'active:prefs'
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Persist only safe preferences.
|
||||
- Do not persist secrets.
|
||||
- Do not persist temporary route overrides unless explicitly requested.
|
||||
- Persist the user's preferred values, not only resolved fallback values.
|
||||
- Persisted values must include a schema version for migrations.
|
||||
- Persistence should support scope: anonymous device, authenticated user,
|
||||
tenant, or test.
|
||||
- Storage failures must not corrupt in-memory preferences.
|
||||
- Hydration must be deterministic: initial server snapshot wins until the client
|
||||
intentionally adopts persisted preferences.
|
||||
- If persisted preferences disagree with server-authoritative user preferences,
|
||||
the server-authoritative preference wins unless the app explicitly chooses a
|
||||
client-first policy.
|
||||
|
||||
## Validation
|
||||
|
||||
The module should validate preference values at the boundary:
|
||||
|
||||
- Locale strings should be syntactically valid BCP 47 tags when possible.
|
||||
- Currency should be ISO 4217-like uppercase codes when possible.
|
||||
- Timezone should be accepted only if the runtime recognizes it or if validation
|
||||
is explicitly disabled.
|
||||
- Unit system, density, theme and hour cycle should be closed unions.
|
||||
|
||||
Invalid preferences should fail clearly in development and return structured
|
||||
errors in production-oriented APIs.
|
||||
|
||||
## Logging
|
||||
|
||||
`prefs` may accept a narrow logger port, but it must not depend on the concrete
|
||||
logger implementation.
|
||||
|
||||
Use cases:
|
||||
|
||||
- Invalid persisted snapshot rejected.
|
||||
- Storage bridge failed to read/write.
|
||||
- Unknown preference key ignored during migration.
|
||||
- Browser/system preference could not be detected.
|
||||
|
||||
Normal preference changes should not spam logs. They are application events, not
|
||||
errors.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
`dispose()` must be idempotent.
|
||||
|
||||
After dispose:
|
||||
|
||||
- Listeners are removed.
|
||||
- Storage bridges are detached.
|
||||
- Bus subscriptions are detached.
|
||||
- Further writes either no-op or throw a documented `prefs` error. The preferred
|
||||
behavior is to throw in dev/test and no-op in production only if explicitly
|
||||
configured.
|
||||
|
||||
## Integration Plan
|
||||
|
||||
The integration into `active-app` should happen only after the current ecosystem
|
||||
docs and contracts are aligned.
|
||||
|
||||
Planned sequence:
|
||||
|
||||
1. Implement isolated `libs/prefs` contracts and `arts/prefs` engine.
|
||||
2. Add unit tests for snapshot updates, events, ports and dispose behavior.
|
||||
3. Add `active-prefs.svelte.ts` and rune-state tests.
|
||||
4. Add optional storage bridge tests using the existing storage artifact.
|
||||
5. Update `lang` to accept `LocalePrefsPort` without requiring `Prefs`.
|
||||
6. Update `format` to accept `LocalePrefsPort` and `FormatPrefsPort`.
|
||||
7. Update `frontend` to accept `UiPrefsPort`.
|
||||
8. Add `App.Prefs` to `active-app` as an always-present, low-cost service.
|
||||
9. Add composed tests: changing locale updates `lang`, `format` and `frontend`
|
||||
consumers without direct coupling.
|
||||
10. Update the Active documentation page with the new preference model.
|
||||
|
||||
## Test Agenda
|
||||
|
||||
Minimum tests before integration:
|
||||
|
||||
```txt
|
||||
src/arts/prefs/test/engine-prefs.test.ts
|
||||
src/arts/prefs/test/active-prefs.test.ts
|
||||
src/arts/prefs/test/ports.test.ts
|
||||
src/arts/prefs/test/persistence.test.ts
|
||||
src/arts/prefs/test/dispose.test.ts
|
||||
```
|
||||
|
||||
Required scenarios:
|
||||
|
||||
- Default snapshot is stable and serializable.
|
||||
- `setLocale` emits locale and global change events.
|
||||
- Locale negotiation preserves `preferredLocale` when the system falls back.
|
||||
- Browser detection fills missing locale values but does not overwrite explicit
|
||||
or persisted preferences.
|
||||
- Server initial locale wins during hydration unless the adoption policy says
|
||||
otherwise.
|
||||
- `setCurrency` emits format and global change events.
|
||||
- `setTheme` emits UI and global change events.
|
||||
- `patch` reports changed paths accurately.
|
||||
- `reset` restores defaults and emits reset.
|
||||
- Listeners can unsubscribe.
|
||||
- `dispose` is idempotent.
|
||||
- Writes after dispose follow the documented policy.
|
||||
- Storage bridge hydrates from a valid persisted snapshot.
|
||||
- Storage bridge rejects invalid persisted snapshots without corrupting memory.
|
||||
- Consumers can use narrow ports without importing `ActivePrefs`.
|
||||
|
||||
## IA Agents
|
||||
|
||||
When modifying or integrating `prefs`, keep these invariants:
|
||||
|
||||
- Do not wire `prefs` into `active-app` until explicitly requested.
|
||||
- Do not make `prefs` import `lang`, `format`, `frontend`, `storage`, `orca` or
|
||||
`active-app`.
|
||||
- Consumers must depend on narrow ports, not on the full `ActivePrefs` object.
|
||||
- Do not add generic arbitrary settings.
|
||||
- Do not store secrets.
|
||||
- Do not format or translate inside `prefs`.
|
||||
- Do not publish event strings inline; add constants first.
|
||||
- Keep server, active and shared contracts separated.
|
||||
- Add tests before using `prefs` from another artifact.
|
||||
Loading…
Reference in new issue