Bloque F1+F2 — connection backoff random + reauth singleflight

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
dev 5 months ago
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.

@ -40,11 +40,17 @@ export interface ConnectionRequestRuntime {
export function createConnectionRequestRuntime( export function createConnectionRequestRuntime(
runtime: ConnectionRequestRuntimeOptions runtime: ConnectionRequestRuntimeOptions
): ConnectionRequestRuntime { ): ConnectionRequestRuntime {
// Singleflight gate: if an auth round is in flight, every concurrent
// caller awaits the same promise. Prevents duplicate auth frames when
// `session.changed` and `session.external_changed` land back-to-back
// (or any other reauth source double-fires).
let inFlightAuth: Promise<ConnectionAuthResult> | null = null;
function hasAuthProvider(): boolean { function hasAuthProvider(): boolean {
return resolveConnectionAuth(runtime.auth).provider !== null; return resolveConnectionAuth(runtime.auth).provider !== null;
} }
async function runAuth(): Promise<ConnectionAuthResult> { async function runAuthOnce(): Promise<ConnectionAuthResult> {
if (!runtime.isConnected()) return { ok: false, reason: CONNECTION_AUTH_REASON_CLOSED }; if (!runtime.isConnected()) return { ok: false, reason: CONNECTION_AUTH_REASON_CLOSED };
const auth = resolveConnectionAuth(runtime.auth); const auth = resolveConnectionAuth(runtime.auth);
if (auth.provider === null) return missingConnectionAuthProvider(); if (auth.provider === null) return missingConnectionAuthProvider();
@ -58,6 +64,15 @@ export function createConnectionRequestRuntime(
} }
} }
function runAuth(): Promise<ConnectionAuthResult> {
if (inFlightAuth !== null) return inFlightAuth;
const pending = runAuthOnce().finally(() => {
if (inFlightAuth === pending) inFlightAuth = null;
});
inFlightAuth = pending;
return pending;
}
async function requestFrame<TPayload, TResult>( async function requestFrame<TPayload, TResult>(
type: string, type: string,
payload: TPayload, payload: TPayload,

@ -50,12 +50,16 @@ export function createConnectionReconnectPolicy(
return { return {
ok: true, ok: true,
attempt, attempt,
delayMs: computeBackoffDelay(attempt - 1, { delayMs: computeBackoffDelay(
minDelayMs: options?.minDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_MS, attempt - 1,
maxDelayMs: options?.maxDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MAX_DELAY_MS, {
factor: options?.factor ?? CONNECTION_DEFAULT_RECONNECT_FACTOR, minDelayMs: options?.minDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_MS,
jitterMs: options?.jitterMs ?? CONNECTION_DEFAULT_RECONNECT_JITTER_MS maxDelayMs: options?.maxDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MAX_DELAY_MS,
}) factor: options?.factor ?? CONNECTION_DEFAULT_RECONNECT_FACTOR,
jitterMs: options?.jitterMs ?? CONNECTION_DEFAULT_RECONNECT_JITTER_MS
},
options?.random
)
}; };
} }
}; };

@ -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);
});
});

@ -156,6 +156,12 @@ export interface ConnectionReconnectOptions {
readonly maxAttempts?: number; readonly maxAttempts?: number;
readonly reconnectOnVisible?: boolean; readonly reconnectOnVisible?: boolean;
readonly reconnectOnOnline?: boolean; readonly reconnectOnOnline?: boolean;
/**
* Random source used by the backoff jitter. Defaults to `Math.random`.
* Inject a deterministic generator for tests or for reproducible
* staggering across many clients.
*/
readonly random?: () => number;
} }
export interface ConnectionHeartbeatOptions { export interface ConnectionHeartbeatOptions {

@ -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…
Cancel
Save

Powered by TurnKey Linux.