From 7b54bdac4cd4ebd61ae2d0dcfaaf10e266f30a13 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 29 Apr 2026 13:14:31 +0200 Subject: [PATCH] Consolidate docs and format runtime cleanup --- NEXT_STEPS.md | 14 +- src/arts/aapp/README.md | 21 +- .../aapp/test/ecosystem.integration.test.ts | 51 ++- src/arts/adom/README.md | 8 +- src/arts/adom/apply.ts | 2 +- src/arts/conn/README.md | 396 ++++++++++++++++-- src/arts/fmts/README.md | 2 +- src/arts/fmts/active-runtime.svelte.ts | 21 + src/arts/fmts/curr/README.md | 2 +- src/arts/fmts/dates/active-dates.svelte.ts | 82 +--- src/arts/fmts/nums/active-numbers.svelte.ts | 133 ++---- src/arts/fmts/unts/active-units.svelte.ts | 63 +-- src/arts/sium/README.md | 12 +- src/arts/sium/core/types.ts | 11 +- src/arts/sium/langs/langs.ts | 2 +- src/libs/auth/consts.ts | 14 +- src/libs/color/parse.ts | 2 +- src/libs/days/segments.ts | 2 +- src/web/routes/test/auth/+page.svelte | 4 +- src/web/routes/test/ecosystem/+page.svelte | 2 +- tsconfig.json | 2 +- vite.config.ts | 4 +- 22 files changed, 574 insertions(+), 276 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index ce091b4..e12bc24 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -4,14 +4,20 @@ Estado al cierre: - Suite unitaria verde: `npm test` -> 103 archivos, 1189 tests. - `fmts` verde: `npx vitest run src/arts/fmts` -> 14 archivos, 39 tests. +- `conn` verde: `npx vitest run src/arts/conn` -> 5 archivos, 28 tests. +- `auth` verde: `npx vitest run src/arts/auth src/svrs/auth src/libs/auth` -> 4 archivos, 14 tests. +- `/test/ecosystem` revisado en navegador: carga sin errores de consola, `ar` cambia a `rtl`, Formats se actualiza por locale, Perm cambia con rol `viewer`, Cach re-scopea por locale y Conn loopback publica/recibe. +- `src/arts/aapp/test/ecosystem.integration.test.ts` ampliado para cubrir rol `viewer` no-allow y cache re-scoped por locale. +- Referencias residuales de marca anterior eliminadas de `src/` fuera de rutas temporales: docs, páginas de test y constantes de cookies/headers auth usan ahora `Active/active`. +- `src/arts/conn/README.md` ampliado: contrato de raíz/conexión/canal, estados, transportes, request/reply, reconnect, heartbeat, sesión, diagnostics/logger, errores y testing. +- `fmts` redujo boilerplate activo con helpers `readFrom` / `writeTo` en `active-runtime.svelte.ts`; APIs públicas sin cambios. - No tocar `src/web/routes/temp/` hasta decidir que hacer con esa pagina. - No commitear `.idea/`, `.claude/` ni `.opencode/`. Pendiente para manana: -- Ejecutar una pasada completa sobre `/test/ecosystem` en navegador y corregir cualquier fallo real de integracion. -- Revisar la adopcion final del contrato comun `Logger` / diagnostics en todos los modulos, sin acoplar artefactos a `arts/logr`. -- Continuar la reduccion de archivos grandes y boilerplate: prioridad `conn`, `auth`, `cach` y cualquier wrapper activo repetitivo. +- Revisar documentacion restante de `cach`, `perm`, `auth` y `fmts` con ojo de consumidor externo, no solo tecnico. +- Continuar la reduccion de archivos grandes: prioridad `conn/connection.ts`, `libs/cach/engine.ts`, `svrs/auth/engine-auth.ts` y `svrs/auth/adapters/memory-store.ts`. - Ampliar tests de integracion cruzada: `auth + sess + perm + cach + http + stor + fmts + conn + timr + logr`. -- Revisar documentacion raiz de `arts`, `aapp`, `auth`, `cach`, `perm`, `conn` y `fmts` para que refleje el estado real del framework. +- Revisar la adopcion final del contrato comun `Logger` / diagnostics en todos los modulos, sin acoplar artefactos a `arts/logr`; primera pasada limpia salvo `aapp` como composition root, `arts/logr` y tests. - Decidir que hacer con la pagina temporal que bloquea `npm run check`; mientras tanto, validar con `npm test` y tests focalizados. diff --git a/src/arts/aapp/README.md b/src/arts/aapp/README.md index a204e0d..9e6450f 100644 --- a/src/arts/aapp/README.md +++ b/src/arts/aapp/README.md @@ -42,6 +42,7 @@ App.dispose(); | `App.Cache` | yes | `ActiveCache` backed by memory by default. Configure `cache: { adapter, policies, scopeResolver }` for persistence, custom policies or tenant/actor/permission-aware keys | | `App.Sess` | no | created lazily through `App.createActiveSession(...)`. Logger is injected automatically; storage, refresh/revoke handlers and HTTP hooks remain explicit so auth policy does not become hidden magic | | `App.Auth` | no | created lazily through `App.createActiveAuth(...)`. App injects `Http` and `Timers`; the server authority remains `$svrs/auth` | +| `App.Permissions` | no | created lazily through `App.createActivePermissions(...)`. App injects `Http` and `Logger`; the authoritative policy engine remains `$svrs/perm` | | `Connections` | no | created lazily through `App.createActiveConnections(...)`. App injects Logger, Timers and a structural session bridge; each connection opts into session behavior independently | Server-authoritative engines that have a browser reflector live under @@ -129,6 +130,13 @@ option. 10. **Sess** — created lazily via `App.createActiveSession(...)`, not from `createActiveApp(...)` options. App injects Logger, but the consumer keeps auth policy explicit (`storage`, `onRefresh`, `onRevoke`, HTTP hooks). +11. **Connections** — created lazily via `App.createActiveConnections(...)`. + App injects Logger, Timers and a structural session bridge. +12. **Permissions** — created lazily via `App.createActivePermissions(...)`. + App injects Logger and Http; endpoint/defaults can be provided either in + `createActiveApp({ permissions })` or at the factory call site. +13. **Auth** — created lazily via `App.createActiveAuth(...)`. App injects + Http, Cache and Logger; `$svrs/auth` remains the server authority. `dispose()` runs in reverse order. @@ -464,6 +472,8 @@ interface ActiveAppOptions { timers?: Omit; cache?: Omit; connections?: Omit; + permissions?: Omit; + auth?: Omit; } interface ActiveApp { @@ -476,7 +486,6 @@ interface ActiveApp { readonly Http: EngineHttp; readonly Timers: ActiveTimers; readonly Cache: ActiveCache; - readonly Sess: ActiveSession | undefined; getLocale(): SupportedLocale; setLocale(locale: SupportedLocale): void; @@ -489,6 +498,14 @@ interface ActiveApp { createActiveConnections( options?: Omit ): ActiveConnections; + createActivePermissions( + options?: Partial> + ): ActivePermissions; + createActiveAuth(options?: Omit): ActiveAuth; + + readonly Sess: ActiveSession | undefined; + readonly Permissions: ActivePermissions | undefined; + readonly Auth: ActiveAuth | undefined; dispose(): void; } @@ -509,5 +526,5 @@ just to use Lang or Formats. Keeping Sium page-scoped means: The standard pattern is one line at the top of the page module: ```ts -const sium = createEngineSium({ lang: App.Lang, logger: App.Logger }); +const sium = App.createSiumEngine(); ``` diff --git a/src/arts/aapp/test/ecosystem.integration.test.ts b/src/arts/aapp/test/ecosystem.integration.test.ts index 2729670..dc96953 100644 --- a/src/arts/aapp/test/ecosystem.integration.test.ts +++ b/src/arts/aapp/test/ecosystem.integration.test.ts @@ -22,6 +22,7 @@ import { import { LogLevel, type LogEntry } from '$logr'; import { PERMISSION_EFFECT_ALLOW, + PERMISSION_EFFECT_NOT_APPLICABLE, actor, allow, and, @@ -44,6 +45,7 @@ const PROJECT_ACTION_UPDATE = 'project.update'; const PROJECT_RELATION_MEMBER = 'project.member'; const PROJECT_SCHEMA_VERSION = 'ProjectPayload:v1'; const ROLE_ADMIN = 'admin'; +const ROLE_VIEWER = 'viewer'; const TIMER_KEY = 'ecosystem:test'; const CONNECTION_NAME = 'updates'; const LOG_CATEGORY = 'test.ecosystem'; @@ -85,20 +87,21 @@ describe('ActiveApp — total ecosystem integration', () => { it('wires auth, sess, perm, cach, http, stor, sium, fmts, fend, adom, timr, conn and logr', async () => { const entries: LogEntry[] = []; const authCurrent = createAuthCurrent(); - const actorRef: SubjectRef = { + let actorRole = ROLE_ADMIN; + const actorRef = (): SubjectRef => ({ type: 'user', id: ACTOR_ID, - role: ROLE_ADMIN, + role: actorRole, tenantIds: [TENANT_ID] - }; + }); const resource: ProjectResource = { type: 'project', id: PROJECT_ID, tenantId: TENANT_ID, locked: false }; - const permissionEngine = createPermissionEngine(actorRef); - const permissionHandlers = createPermissionHttpHandlers(permissionEngine, () => actorRef); + const permissionEngine = createPermissionEngine(); + const permissionHandlers = createPermissionHttpHandlers(permissionEngine, actorRef); let projectFetches = 0; const appFetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise => { @@ -164,7 +167,7 @@ describe('ActiveApp — total ecosystem integration', () => { scopeResolver: (): ResolvedScopeValues => ({ tenantId: TENANT_ID, actorId: ACTOR_ID, - permissionHash: ROLE_ADMIN, + permissionHash: actorRole, locale: App.getLocale() }), policies: { @@ -218,7 +221,7 @@ describe('ActiveApp — total ecosystem integration', () => { const Permissions = App.createActivePermissions({ endpoint: PERM_ENDPOINT, - scopeKey: ACTOR_ID + scopeKey: () => `${ACTOR_ID}:${actorRole}` }); const decision = await Permissions.check({ action: PROJECT_ACTION_UPDATE, @@ -227,6 +230,18 @@ describe('ActiveApp — total ecosystem integration', () => { }); expect(decision.effect).toBe(PERMISSION_EFFECT_ALLOW); + actorRole = ROLE_VIEWER; + Permissions.invalidate(); + const viewerDecision = await Permissions.check({ + action: PROJECT_ACTION_UPDATE, + resource, + context: { risk: { mfa: true } } + }); + expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE); + + actorRole = ROLE_ADMIN; + Permissions.invalidate(); + const project = await App.Cache.query({ key: ['project', PROJECT_ID], scope: CACHE_SCOPE_TENANT, @@ -255,6 +270,24 @@ describe('ActiveApp — total ecosystem integration', () => { expect(cached.version).toBe(1); expect(projectFetches).toBe(1); + App.setLocale('es'); + const localizedProject = await App.Cache.query({ + key: ['project', PROJECT_ID], + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, + schemaVersion: PROJECT_SCHEMA_VERSION, + tags: [{ type: 'project', id: PROJECT_ID }], + fetcher: async () => { + const response = await App.Http.get(HTTP_PROJECT_PATH, { + schema: JSON_PASSTHROUGH_SCHEMA + }); + if (!response.ok) throw new Error('localized project request failed'); + return response.value as ProjectPayload; + } + }); + expect(localizedProject.version).toBe(2); + expect(projectFetches).toBe(2); + let timerRan = false; App.Timers.schedule(TIMER_KEY, 0, () => { timerRan = true; @@ -280,7 +313,7 @@ describe('ActiveApp — total ecosystem integration', () => { }); }); -function createPermissionEngine(actorRef: SubjectRef) { +function createPermissionEngine() { const schema = definePermSchema({ actors: { user: { @@ -323,7 +356,7 @@ function createPermissionEngine(actorRef: SubjectRef) { relations: { hasRelation({ relation, resource, subject }) { if (relation !== PROJECT_RELATION_MEMBER) return 'unknown'; - expect(subject.id).toBe(actorRef.id); + expect(subject.id).toBe(ACTOR_ID); return Array.isArray(subject.tenantIds) && subject.tenantIds.includes(resource.tenantId); } } diff --git a/src/arts/adom/README.md b/src/arts/adom/README.md index 963bdf7..948d310 100644 --- a/src/arts/adom/README.md +++ b/src/arts/adom/README.md @@ -19,7 +19,7 @@ Su responsabilidad actual es más pequeña y más concreta: En otras palabras: ```text -libs/dom -> arts/adom -> app.dom / soma.dom +libs/dom -> arts/adom -> App.Dom / app.dom puro reactivo consumo de app ``` @@ -68,8 +68,8 @@ Runtime reactivo de DOM: `ActiveDom` vive a nivel de aplicación: ```ts +App.Dom; app.dom; -soma.dom; ``` La implementación se consume desde la capa de aplicación y desde artefactos que @@ -183,7 +183,7 @@ top-level `window`). Además, `uix/adom` puede alojar helpers DOM con estado global real, como `BodyScrollLock`, o helpers scoped de runtime como `DOMContext`, cuando ya no -son primitives puras de `libs/dom` pero tampoco pertenecen a `Soma`. +son primitives puras de `libs/dom` pero tampoco pertenecen a un componente UI concreto. También caben aquí helpers runtime de foco con estado propio, como `RovingFocusGroup`, que reutilizan `uix/lib/dom` por debajo pero ya no son @@ -230,7 +230,7 @@ Su código actual sirve como referencia histórica para extraer utilidades hacia Lo que ya está cerrado: - `app.dom` -- `soma.dom` +- `App.Dom` - `viewport` - `breakpoints` - `currentBreakpoint` diff --git a/src/arts/adom/apply.ts b/src/arts/adom/apply.ts index 0793837..3052ba5 100644 --- a/src/arts/adom/apply.ts +++ b/src/arts/adom/apply.ts @@ -1,7 +1,7 @@ /** * `apply` / `remove` — the only mutation surface of `ActiveDom`. * - * `ActiveDom` doesn't know Morfo, Sema, Soma, or Eidos. It receives instructions + * `ActiveDom` doesn't know product frameworks. It receives instructions * already resolved by upper layers and applies them to the DOM. Nothing else. * * Contract: diff --git a/src/arts/conn/README.md b/src/arts/conn/README.md index e2f9411..cbdce32 100644 --- a/src/arts/conn/README.md +++ b/src/arts/conn/README.md @@ -1,6 +1,24 @@ -# `arts/conn` +# conn -`conn` manages runtime realtime connections without coupling the app to WebSocket as a concept. The root artifact is a registry: +`conn` es el artefacto de conexiones realtime. No es "un wrapper de +WebSocket": es un registro de conexiones con transporte intercambiable, +reconexión, heartbeat, request/reply, canales, integración opcional con sesión +y diagnósticos estructurados. + +La regla de nombres es importante: + +| Concepto | Nombre | +| -------- | ------ | +| Raíz imperativa | `createEngineConnections()` / `EngineConnections` | +| Raíz reactiva | `createActiveConnections()` / `ActiveConnections` | +| Unidad individual | `Connection` | +| Topic lógico dentro de una conexión | `ConnectionChannel` | + +No existe `EngineConnection` ni `ActiveConnection`. `Engine*` y `Active*` +quedan reservados para raíces de artefacto; una conexión individual no es una +raíz, es una entidad gestionada por `EngineConnections`. + +## Uso Mínimo ```ts import { createEngineConnections, createWebSocketTransport } from '$conn'; @@ -15,27 +33,59 @@ const Main = Connections.createConnection('main', { await Main.connect(); ``` -## Naming +La raíz mantiene el mapa de conexiones: -- Root engine: `createEngineConnections()` / `EngineConnections`. -- Reactive root: `createActiveConnections()` / `ActiveConnections`. -- Individual unit: `Connection`. -- Logical topic: `ConnectionChannel`. +```ts +Connections.names(); +Connections.connection('main'); +await Connections.openConnection('main'); +Connections.closeConnection('main', 'manual'); +await Connections.reconnectAll('network-restored'); +Connections.dispose(); +``` -There is intentionally no `EngineConnection` or `ActiveConnection`; `Engine*` and `Active*` are reserved for artifact roots. +`dispose()` de la raíz cierra conexiones, cancela timers propios y limpia los +listeners. Después de `dispose()`, las operaciones públicas lanzan errores +tipados `Conn*`. -## App Integration +## ActiveConnections -`aapp` exposes a factory instead of a permanent property: +La capa activa añade estado derivado para UI: + +```ts +const Connections = createActiveConnections(); + +Connections.size; +Connections.activeNames; +Connections.states; +Connections.connectedNames; +Connections.failedNames; +Connections.anyConnected; +Connections.anyFailed; +``` + +`ActiveConnections` conserva la misma API de creación/consulta que el engine, +pero sus colecciones reflejan los cambios de estado de cada conexión. + +## Integración Con App + +`aapp` expone una factory porque las conexiones son app-scoped y suelen tener +tipos específicos del proyecto: ```ts const Connections = App.createActiveConnections(); ``` -App injects `Logger`, `Timers` and a structural session event bridge. A connection reacts to session events only when it opts in: +App inyecta: + +- `App.Logger`, como `Logger` común de `$libs/logr`. +- `App.Timers`, para reconexión, heartbeat y timeouts de ack. +- Un bridge estructural de sesión, si `App.Sess` existe. + +Cada conexión decide si usa la sesión: ```ts -Connections.createConnection('main', { +const Main = Connections.createConnection('main', { transport: createWebSocketTransport({ url: '/realtime' }), auth: () => ({ token: App.Sess?.current?.credential }), session: { @@ -46,33 +96,325 @@ Connections.createConnection('main', { }); ``` -When `Timers` is not injected, `createEngineConnections()` creates a private -timer engine with the same logger. Those scheduler diagnostics are emitted -under the `timr` logger category; connection-specific logs stay scoped as -`conn:`. +Cuando `Timers` no se inyecta, `createEngineConnections()` crea un scheduler +privado con el mismo logger. Los diagnósticos del scheduler salen bajo la +categoría `timr`; los de conexiones salen bajo `conn` o `conn:`. + +## Estados + +Estados de conexión: -## Channels +```txt +idle -> connecting -> open +open -> reconnecting -> open +open -> closing -> closed +connecting/reconnecting -> failed +``` -Channels are cached by name and route frames by `topic`. +Campos útiles: ```ts -const Orders = Main.channel<{ 'order.updated': { id: string } }>('orders'); +Main.state; +Main.connected; +Main.generation; +Main.error; +Main.openedAt; +Main.closedAt; +Main.lastMessageAt; +Main.reconnectAttempt; +``` + +`generation` cambia cuando se abre una conexión nueva. Los timeouts de ack, +heartbeat y reconexión usan esa generación para no resolver trabajo viejo sobre +una conexión nueva. + +## Transports + +El contrato mínimo es `ConnectionTransport`: + +```ts +interface ConnectionTransport { + readonly kind: string; + readonly state: ConnectionTransportState; + readonly bufferedAmount: number; + readonly canSend: boolean; + open(): Promise; + send(data: string | ArrayBuffer): Promise | void; + close(code?: number, reason?: string): void; + onOpen(listener: () => void): () => void; + onMessage(listener: (message: string | ArrayBuffer) => void): () => void; + onClose(listener: (event: ConnectionCloseEvent) => void): () => void; + onError(listener: (error: unknown) => void): () => void; +} +``` + +Incluidos: + +- `createWebSocketTransport()` para navegador/runtime con WebSocket. +- `createMockTransport()` para tests, loopback y páginas de diagnóstico. + +El transporte no decide reconexión, auth, heartbeat ni canales. Solo abre, +envía, cierra y emite eventos. + +## Frames + +El frame canónico: + +```ts +interface ConnectionFrame { + readonly id?: string; + readonly topic?: string; + readonly type: TType; + readonly payload: TPayload; + readonly ts?: number; + readonly ack?: boolean; + readonly replyTo?: string; + readonly error?: ConnectionFrameError; +} +``` + +El serializer por defecto es JSON y valida la forma mínima del frame. Errores +de encode/decode no se lanzan como strings dispersos: devuelven resultados +tagged o errores `ConnInvalidFrameError` según el punto de entrada. + +## Send Y Request/Reply + +`send()` devuelve un resultado tagged: + +```ts +const result = await Main.send('project.updated', { id: 'p1' }); + +if (!result.ok) { + console.log(result.reason); +} +``` + +`request()` usa `ack: true`, genera un `id` y espera un frame entrante con +`replyTo` igual a ese id: + +```ts +const reply = await Main.request<{ id: string }, { ok: boolean }>('project.sync', { + id: 'p1' +}); + +if (reply.ok) { + reply.payload.ok; +} +``` + +Razones de fallo principales: + +- `timeout` +- `closed` +- `rejected` +- `transport_error` +- `invalid_reply` + +## Canales + +Los canales son topics nombrados dentro de una conexión. Se cachean por nombre: + +```ts +type ProjectEvents = { + 'project.updated': { id: string; version: number }; +}; + +const Projects = Main.channel('tenant:projects'); + +Projects.on('project.updated', (payload, meta) => { + console.log(payload.id, meta.receivedAt); +}); + +await Projects.join({ tenantId: 'acme' }); +await Projects.send('project.updated', { id: 'p1', version: 2 }); +await Projects.leave(); +``` -Orders.on('order.updated', (payload) => { - console.log(payload.id); +Estados de canal: + +```txt +idle -> joining -> joined -> leaving -> left +joining -> failed +``` + +`dispose()` del canal limpia listeners y deja el canal en estado terminal +`left`. + +## Reconexión + +La reconexión usa `timr` y backoff configurable: + +```ts +Connections.createConnection('main', { + transport, + reconnect: { + enabled: true, + minDelayMs: 500, + maxDelayMs: 15_000, + factor: 1.8, + jitterMs: 500, + maxAttempts: 8, + reconnectOnVisible: true, + reconnectOnOnline: true + } }); +``` + +Si `reconnectOnVisible` o `reconnectOnOnline` están activos, el módulo escucha +eventos del navegador y pide reconexión cuando la conexión está cerrada o +fallida. Esa capa no recibe una función `logDebug`; recibe `Diagnostics`, que +incluye el `Logger` completo y emite eventos catalogados. -await Orders.join({ tenant: 'acme' }); +## Heartbeat + +```ts +Connections.createConnection('main', { + transport, + heartbeat: { + enabled: true, + intervalMs: 25_000, + timeoutMs: 10_000, + pingType: 'conn.ping', + pongType: 'conn.pong' + } +}); ``` -## Request / Reply +El heartbeat envía `pingType` periódicamente y espera `pongType`. Si vence el +timeout, cierra la conexión con `heartbeat_timeout` y deja que la política de +reconexión decida el siguiente paso. + +## Auth Y Sesión -`request()` creates a frame id, sends `ack: true`, and resolves when an incoming frame carries `replyTo` with that id. +Auth de conexión: ```ts -const result = await Main.request<{ id: string }, { ok: boolean }>('order.sync', { - id: 'o1' +Connections.createConnection('main', { + transport, + auth: { + getAuth: () => ({ token }), + authType: 'conn.auth', + timeoutMs: 10_000 + } }); ``` -Runtime failures return tagged results. Programmer errors such as duplicated names or invalid connection names throw typed `Conn*` errors. +El resultado de auth es tagged: + +```ts +await Main.reauthenticate(); // { ok: true } | { ok: false, reason, error? } +``` + +Con `session.enabled`, el bridge de sesión puede: + +- reautenticar cuando `sess` emite refresh; +- desconectar cuando la sesión expira; +- desconectar cuando la sesión se revoca. + +`conn` no crea sesiones ni decide permisos. En servidor, los joins/sends de un +canal deben validarse con `auth/sess/perm`. + +## Buffer + +Cuando la conexión no está abierta, `send()` puede comportarse según policy: + +```ts +buffer: { + policy: 'buffer', // 'buffer' | 'drop' | 'fail' + maxMessages: 100, + maxBytes: 1_000_000 +} +``` + +- `buffer`: encola y drena al abrir. +- `drop`: acepta la llamada pero descarta. +- `fail`: devuelve `{ ok: false, reason: 'closed' }`. + +## Diagnostics Y Logger + +Las opciones públicas aceptan `logger?: Logger` desde `$libs/logr`. No existe +un `ConnectionLogger` propio. + +```ts +import type { Logger } from '$libs/logr'; +``` + +Internamente `conn` usa `createConnectionDiagnostics(logger)` y eventos +catalogados en `CONNECTION_DIAGNOSTIC_EVENTS`: + +- `connect_failed` +- `transport_error` +- `send_failed` +- `frame_decode_failed` +- `frame_encode_failed` +- `auth_failed` +- `reauth_failed` +- `heartbeat_timeout` +- `reconnect_exhausted` +- `browser_reconnect` +- `session_refreshed` +- `session_expired` +- `session_revoked` +- `listener_threw` + +Los mensajes y categorías viven en `consts.ts`. Si quieres enviar eventos a +Sentry, Loki o Datadog, inyecta un `EngineLogger` con el transporte adecuado; +`conn` solo emite al contrato común. + +## Errores + +Programmer errors lanzan clases tipadas: + +- `ConnDisposedError` +- `ConnConnectionAlreadyExistsError` +- `ConnConnectionNotFoundError` +- `ConnInvalidConnectionNameError` +- `ConnInvalidFrameError` +- `ConnChannelAlreadyExistsError` +- `ConnChannelNotFoundError` +- `ConnWebSocketUnavailableError` + +Fallos runtime de transporte/envío/auth/request devuelven resultados tagged +para que el consumidor pueda decidir sin `try/catch` obligatorio. + +## Página De Prueba + +La página interactiva está en: + +```txt +/test/conn +``` + +Incluye chat WebSocket real usando `scripts/conn-chat-server.mjs`: + +```txt +npm run dev:conn-chat +``` + +También `/test/ecosystem` usa `conn` dentro de la demo total con transporte +mock loopback para probar integración con `aapp`, `timr`, `logr`, `perm` y +`cach`. + +## Testing + +Tests principales: + +```txt +src/arts/conn/test/engine-connections.test.ts +src/arts/conn/test/active-connections.test.ts +src/arts/conn/test/connection.test.ts +src/arts/conn/test/connection-state.test.ts +src/arts/conn/test/websocket.test.ts +``` + +Casos que deben mantenerse cubiertos: + +- creación duplicada y lookup inexistente; +- `this` no requerido al desestructurar métodos del engine; +- reloj inyectado para timestamps deterministas; +- reconnect con fake timers; +- heartbeat timeout; +- request/reply con timeout y rechazo; +- channel join/leave/dispose; +- bridge de sesión refresh/expire/revoke; +- serializer inválido y transport errors. diff --git a/src/arts/fmts/README.md b/src/arts/fmts/README.md index d86ef46..e2da6bd 100644 --- a/src/arts/fmts/README.md +++ b/src/arts/fmts/README.md @@ -1,7 +1,7 @@ # Formats `Formats` es la API publica del artefacto `fmts`, que agrupa formatos -localizados de Soma. +localizados de Active. Agrupa los subdominios que dependen de locale: diff --git a/src/arts/fmts/active-runtime.svelte.ts b/src/arts/fmts/active-runtime.svelte.ts index 3d96586..4fb8d5c 100644 --- a/src/arts/fmts/active-runtime.svelte.ts +++ b/src/arts/fmts/active-runtime.svelte.ts @@ -96,3 +96,24 @@ export function createActiveFormatsRuntime( } }; } + +export function readFrom( + runtime: Pick, + fn: (...args: TArgs) => TResult +): (...args: TArgs) => TResult { + return (...args) => { + runtime.read(); + return fn(...args); + }; +} + +export function writeTo( + runtime: Pick, + fn: (...args: TArgs) => TResult +): (...args: TArgs) => TResult { + return (...args) => { + const result = fn(...args); + runtime.notifyChange(); + return result; + }; +} diff --git a/src/arts/fmts/curr/README.md b/src/arts/fmts/curr/README.md index 99e1bde..221e46d 100644 --- a/src/arts/fmts/curr/README.md +++ b/src/arts/fmts/curr/README.md @@ -1,6 +1,6 @@ # Currency -Currency es la API publica de moneda de Soma: formato, moneda por locale y conversion +Currency es la API publica de moneda de Active: formato, moneda por locale y conversion mediante factores cacheados. La API publica sigue el mismo patron que el resto de artefactos: diff --git a/src/arts/fmts/dates/active-dates.svelte.ts b/src/arts/fmts/dates/active-dates.svelte.ts index 7d4b665..18ac990 100644 --- a/src/arts/fmts/dates/active-dates.svelte.ts +++ b/src/arts/fmts/dates/active-dates.svelte.ts @@ -1,4 +1,4 @@ -import { createActiveFormatsRuntime } from '../active-runtime.svelte'; +import { createActiveFormatsRuntime, readFrom, writeTo } from '../active-runtime.svelte'; import { createEngineDates } from './engine-dates'; import type { ActiveDates, ActiveDatesOptions } from './types'; @@ -16,67 +16,31 @@ export function createActiveDates(options: ActiveDatesOptions = {}): ActiveDates }); return { - getLocale() { - runtime.read(); - return engine.getLocale(); - }, + getLocale: readFrom(runtime, engine.getLocale), setLocale: runtime.syncLocale, - getDateOrder() { - runtime.read(); - return engine.getDateOrder(); - }, - - setDateOrder(order) { - engine.setDateOrder(order); - runtime.notifyChange(); - }, - - clearDateOrder() { - engine.clearDateOrder(); - runtime.notifyChange(); - }, - - isDateOrderAuto() { - runtime.read(); - return engine.isDateOrderAuto(); - }, - - getHourCycle() { - runtime.read(); - return engine.getHourCycle(); - }, - - setHourCycle(hourCycle) { - engine.setHourCycle(hourCycle); - runtime.notifyChange(); - }, - - clearHourCycle() { - engine.clearHourCycle(); - runtime.notifyChange(); - }, - - isHourCycleAuto() { - runtime.read(); - return engine.isHourCycleAuto(); - }, - - formatDate(value, options) { - runtime.read(); - return engine.formatDate(value, options); - }, - - formatTime(value, options) { - runtime.read(); - return engine.formatTime(value, options); - }, - - formatDateTime(value, options) { - runtime.read(); - return engine.formatDateTime(value, options); - }, + getDateOrder: readFrom(runtime, engine.getDateOrder), + + setDateOrder: writeTo(runtime, engine.setDateOrder), + + clearDateOrder: writeTo(runtime, engine.clearDateOrder), + + isDateOrderAuto: readFrom(runtime, engine.isDateOrderAuto), + + getHourCycle: readFrom(runtime, engine.getHourCycle), + + setHourCycle: writeTo(runtime, engine.setHourCycle), + + clearHourCycle: writeTo(runtime, engine.clearHourCycle), + + isHourCycleAuto: readFrom(runtime, engine.isHourCycleAuto), + + formatDate: readFrom(runtime, engine.formatDate), + + formatTime: readFrom(runtime, engine.formatTime), + + formatDateTime: readFrom(runtime, engine.formatDateTime), onPreferenceChange: runtime.onChange, diff --git a/src/arts/fmts/nums/active-numbers.svelte.ts b/src/arts/fmts/nums/active-numbers.svelte.ts index 0d69046..af3af1f 100644 --- a/src/arts/fmts/nums/active-numbers.svelte.ts +++ b/src/arts/fmts/nums/active-numbers.svelte.ts @@ -1,6 +1,6 @@ -import { createActiveFormatsRuntime } from '../active-runtime.svelte'; +import { createActiveFormatsRuntime, readFrom, writeTo } from '../active-runtime.svelte'; import { createEngineNumbers } from './engine-numbers'; -import type { ActiveNumbers, ActiveNumbersOptions, NumbersFormatOptions } from './types'; +import type { ActiveNumbers, ActiveNumbersOptions } from './types'; export function createActiveNumbers(options: ActiveNumbersOptions = {}): ActiveNumbers { const localeSource = options.localeSource; @@ -16,102 +16,45 @@ export function createActiveNumbers(options: ActiveNumbersOptions = {}): ActiveN }); return { - getLocale() { - runtime.read(); - return engine.getLocale(); - }, + getLocale: readFrom(runtime, engine.getLocale), setLocale: runtime.syncLocale, - format(value: number, options?: NumbersFormatOptions): string { - runtime.read(); - return engine.format(value, options); - }, - - formatPercent(value, options) { - runtime.read(); - return engine.formatPercent(value, options); - }, - - formatCompact(value, options) { - runtime.read(); - return engine.formatCompact(value, options); - }, - - formatCurrency(value, currency, options) { - runtime.read(); - return engine.formatCurrency(value, currency, options); - }, - - formatUnit(value, unit, options) { - runtime.read(); - return engine.formatUnit(value, unit, options); - }, - - parse(value) { - runtime.read(); - return engine.parse(value); - }, - - getDecimalSeparator() { - runtime.read(); - return engine.getDecimalSeparator(); - }, - - getGroupSeparator() { - runtime.read(); - return engine.getGroupSeparator(); - }, - - getGrouping() { - runtime.read(); - return engine.getGrouping(); - }, - - setDecimalSeparator(separator) { - engine.setDecimalSeparator(separator); - runtime.notifyChange(); - }, - - clearDecimalSeparator() { - engine.clearDecimalSeparator(); - runtime.notifyChange(); - }, - - isDecimalSeparatorAuto() { - runtime.read(); - return engine.isDecimalSeparatorAuto(); - }, - - setGroupSeparator(separator) { - engine.setGroupSeparator(separator); - runtime.notifyChange(); - }, - - clearGroupSeparator() { - engine.clearGroupSeparator(); - runtime.notifyChange(); - }, - - isGroupSeparatorAuto() { - runtime.read(); - return engine.isGroupSeparatorAuto(); - }, - - setGrouping(enabled) { - engine.setGrouping(enabled); - runtime.notifyChange(); - }, - - clearGrouping() { - engine.clearGrouping(); - runtime.notifyChange(); - }, - - isGroupingAuto() { - runtime.read(); - return engine.isGroupingAuto(); - }, + format: readFrom(runtime, engine.format) as ActiveNumbers['format'], + + formatPercent: readFrom(runtime, engine.formatPercent), + + formatCompact: readFrom(runtime, engine.formatCompact), + + formatCurrency: readFrom(runtime, engine.formatCurrency), + + formatUnit: readFrom(runtime, engine.formatUnit), + + parse: readFrom(runtime, engine.parse), + + getDecimalSeparator: readFrom(runtime, engine.getDecimalSeparator), + + getGroupSeparator: readFrom(runtime, engine.getGroupSeparator), + + getGrouping: readFrom(runtime, engine.getGrouping), + + setDecimalSeparator: writeTo(runtime, engine.setDecimalSeparator), + + clearDecimalSeparator: writeTo(runtime, engine.clearDecimalSeparator), + + isDecimalSeparatorAuto: readFrom(runtime, engine.isDecimalSeparatorAuto), + + setGroupSeparator: writeTo(runtime, engine.setGroupSeparator), + + clearGroupSeparator: writeTo(runtime, engine.clearGroupSeparator), + + isGroupSeparatorAuto: readFrom(runtime, engine.isGroupSeparatorAuto), + + setGrouping: writeTo(runtime, engine.setGrouping), + + clearGrouping: writeTo(runtime, engine.clearGrouping), + + isGroupingAuto: readFrom(runtime, engine.isGroupingAuto), onPreferenceChange: runtime.onChange, diff --git a/src/arts/fmts/unts/active-units.svelte.ts b/src/arts/fmts/unts/active-units.svelte.ts index dc9348a..5743174 100644 --- a/src/arts/fmts/unts/active-units.svelte.ts +++ b/src/arts/fmts/unts/active-units.svelte.ts @@ -1,6 +1,6 @@ -import { createActiveFormatsRuntime } from '../active-runtime.svelte'; +import { createActiveFormatsRuntime, readFrom, writeTo } from '../active-runtime.svelte'; import { createEngineUnits } from './engine-units'; -import type { ActiveUnits, ActiveUnitsOptions, UnitId, UnitKind, UnitSystemMode } from './types'; +import type { ActiveUnits, ActiveUnitsOptions } from './types'; export function createActiveUnits(options: ActiveUnitsOptions = {}): ActiveUnits { const localeSource = options.localeSource; @@ -16,52 +16,25 @@ export function createActiveUnits(options: ActiveUnitsOptions = {}): ActiveUnits }); return { - getLocale() { - runtime.read(); - return engine.getLocale(); - }, + getLocale: readFrom(runtime, engine.getLocale), setLocale: runtime.syncLocale, - getSystem() { - runtime.read(); - return engine.getSystem(); - }, - - setSystem(system: UnitSystemMode) { - engine.setSystem(system); - runtime.notifyChange(); - }, - - clearSystem() { - engine.clearSystem(); - runtime.notifyChange(); - }, - - isSystemAuto() { - runtime.read(); - return engine.isSystemAuto(); - }, - - getDefaultUnit(kind: UnitKind, system) { - runtime.read(); - return engine.getDefaultUnit(kind, system); - }, - - isDefaultUnit(unit: UnitId, kind, system) { - runtime.read(); - return engine.isDefaultUnit(unit, kind, system); - }, - - format(value, unit, options) { - runtime.read(); - return engine.format(value, unit, options); - }, - - formatDefault(value, kind, options) { - runtime.read(); - return engine.formatDefault(value, kind, options); - }, + getSystem: readFrom(runtime, engine.getSystem), + + setSystem: writeTo(runtime, engine.setSystem), + + clearSystem: writeTo(runtime, engine.clearSystem), + + isSystemAuto: readFrom(runtime, engine.isSystemAuto), + + getDefaultUnit: readFrom(runtime, engine.getDefaultUnit), + + isDefaultUnit: readFrom(runtime, engine.isDefaultUnit), + + format: readFrom(runtime, engine.format), + + formatDefault: readFrom(runtime, engine.formatDefault), convert: engine.convert, convertToDefault: engine.convertToDefault, diff --git a/src/arts/sium/README.md b/src/arts/sium/README.md index dd0ddc2..a7db444 100644 --- a/src/arts/sium/README.md +++ b/src/arts/sium/README.md @@ -1,8 +1,8 @@ # Sium -Sium es la capa de contratos, validacion e introspeccion de Soma. +Sium es la capa de contratos, validacion e introspeccion de Active. -Su objetivo no es competir con Zod, Valibot o ArkType como libreria generalista. Sium existe para que Soma tenga un contrato nativo entre datos, validacion, errores traducibles, tipos de dominio y UI auto-generada. +Su objetivo no es competir con Zod, Valibot o ArkType como libreria generalista. Sium existe para que Active tenga un contrato nativo entre datos, validacion, errores traducibles, tipos de dominio y UI auto-generada. Este archivo es la unica documentacion Markdown del modulo. Si cambia el comportamiento de Sium, actualiza este README en lugar de crear documentos paralelos. @@ -98,9 +98,9 @@ interface Schema { ## Por Que Existe -Sium cubre cuatro huecos que las librerias externas no resuelven de forma nativa dentro de Soma. +Sium cubre cuatro huecos que las librerias externas no resuelven de forma nativa dentro de Active. -- Tipos de dominio: `ColorValue`, `DateValue`, `TimeValue` y sus segmentos ya existen en Soma. +- Tipos de dominio: `ColorValue`, `DateValue`, `TimeValue` y sus segmentos ya existen en Active. - UI metadata: `meta.widget`, `meta.channel`, labels y opciones viajan dentro del schema. - Errores traducibles: los issues llevan `code`, `message` idlangref, `params` y `path`. - Interop: cada schema tambien habla Standard Schema v1. @@ -649,7 +649,7 @@ Estado actual: - El core puede usarse sin Svelte. - Los schemas exponen `~standard`, asi que pueden conectarse a consumidores Standard Schema. -- El adapter `svelte/` no esta presente en este arbol porque depende de piezas de Soma UI pendientes de port. +- El adapter `svelte/` no esta presente en este arbol porque depende de piezas UI pendientes de port. - Hasta completar esa migracion, las pruebas y auditorias de Sium deben omitir `src/arts/sium/svelte`. ## Recetas Rapidas @@ -744,7 +744,7 @@ Los errores emitidos dentro de schemas hijos deben usar `ctx.path`. Sium pasa pa ### `npm run check` falla en `sium/svelte` -Es esperado mientras `$uix/soma/provider` no este portado. El core de Sium se valida con la suite sin Svelte. +Es esperado mientras el provider UI no este portado. El core de Sium se valida con la suite sin Svelte. ## Verificacion Recomendada diff --git a/src/arts/sium/core/types.ts b/src/arts/sium/core/types.ts index 2e34961..93b065c 100644 --- a/src/arts/sium/core/types.ts +++ b/src/arts/sium/core/types.ts @@ -4,7 +4,7 @@ * Pure TypeScript — no Svelte runtime, no runes, no DOM. Runnable in Node, * Bun, Deno, Cloudflare Workers, or a browser. The Svelte adapter lives at * `$lib/sium/svelte/` and the UI renderer (`Form.AutoFields`) at - * `$uix/soma/form/`. + * the framework UI form package. * * Contract and design rules are frozen in the module README. Key invariants: * @@ -79,8 +79,8 @@ export type SchemaEffect = 'refine' | 'transform' | 'codec'; * — the semantic role lives here so `Form.AutoFields` can optionally * route to a specialized widget keyed by channel without inflating the * `kind` vocabulary. - * - `label` / `description`: free-form text or a resolution key. The soma - * adapter routes these through `soma.langs.ts()`. + * - `label` / `description`: free-form text or a resolution key. The UI + * adapter routes these through the active language engine. * - `example`: a canonical example value (used in docs or placeholder fallback). * - `readOnly` / `deprecated`: behavioural flags. */ @@ -191,7 +191,7 @@ export type Result = * fields have decoded). Inside **field-level** refines it is partial or * `undefined` (the sibling fields may not be decoded yet). * - `locale` — set by adapters (`sium/svelte` injects it from - * `soma.langs.getLocale()`). Used by refinements that need locale-aware + * the active language engine). Used by refinements that need locale-aware * validation (date parsing, collation, etc.). * - `meta` — adapter-specific extras. */ @@ -240,7 +240,7 @@ export interface RefineFailure { * ### Standard Schema v1 interop * * Every sium schema exposes `'~standard'` — consumers that accept - * `StandardSchemaV1` (including soma's own `createForm`) can use sium + * `StandardSchemaV1` (including Active form builders) can use sium * schemas as-is, and vice versa (sium schemas interoperate with Zod / * Valibot / ArkType via the same contract). */ @@ -324,4 +324,3 @@ export class SiumAsyncSchemaError extends Error { } } - diff --git a/src/arts/sium/langs/langs.ts b/src/arts/sium/langs/langs.ts index 02d2ceb..d88136e 100644 --- a/src/arts/sium/langs/langs.ts +++ b/src/arts/sium/langs/langs.ts @@ -39,7 +39,7 @@ const maxLengthPlural = p({ * * const langs = createLang({ * sium: siumLangs, - * components: somaComponentLangs, + * components: componentLangs, * ... * }, 'es'); * diff --git a/src/libs/auth/consts.ts b/src/libs/auth/consts.ts index 052c6f4..0e5e3e6 100644 --- a/src/libs/auth/consts.ts +++ b/src/libs/auth/consts.ts @@ -51,10 +51,10 @@ export const AUTH_CONTENT_TYPES = { } as const; export const AUTH_COOKIE_NAMES = { - CSRF: '__Host-asoma.auth.csrf', - FLOW: '__Host-asoma.auth.flow', - DEVICE: '__Host-asoma.auth.device', - REFRESH: '__Host-asoma.auth.refresh' + CSRF: '__Host-active.auth.csrf', + FLOW: '__Host-active.auth.flow', + DEVICE: '__Host-active.auth.device', + REFRESH: '__Host-active.auth.refresh' } as const; export const AUTH_COOKIE_ATTRIBUTES = { @@ -74,8 +74,8 @@ export const AUTH_HEADER_NAMES = { CONTENT_TYPE: 'content-type', COOKIE: 'cookie', SET_COOKIE: 'set-cookie', - CSRF: 'x-asoma-auth-csrf', - REQUEST_ID: 'x-asoma-request-id', + CSRF: 'x-active-auth-csrf', + REQUEST_ID: 'x-active-request-id', FETCH_SITE: 'sec-fetch-site', FETCH_MODE: 'sec-fetch-mode', FETCH_DEST: 'sec-fetch-dest', @@ -352,7 +352,7 @@ export const AUTH_TEST_IDS = { } as const; export const AUTH_TEST_COOKIE_NAMES = { - SESSION: 'asoma.auth.test.session' + SESSION: 'active.auth.test.session' } as const; export const AUTH_TEST_ACTIONS = { diff --git a/src/libs/color/parse.ts b/src/libs/color/parse.ts index b94a2d1..a732f5f 100644 --- a/src/libs/color/parse.ts +++ b/src/libs/color/parse.ts @@ -101,7 +101,7 @@ export function isValidColor(value: string): boolean { } /** - * Type guard for the normalized `ColorValue` shape used across air/soma. + * Type guard for the normalized `ColorValue` shape used across Active UI layers. */ export function isColorValue(value: unknown): value is ColorValue { if (!isRecord(value)) return false; diff --git a/src/libs/days/segments.ts b/src/libs/days/segments.ts index 011384d..6806a05 100644 --- a/src/libs/days/segments.ts +++ b/src/libs/days/segments.ts @@ -4,7 +4,7 @@ * * Everything in this module is **pure** — no DOM, no reactivity, no framework. * UI/DOM concerns (segment navigation, keyboard handling, screen reader - * announcements) live next to the consumer (e.g. `soma/datetime/`). + * announcements) live next to the consumer (for example a datetime component). * * The surface is organised as: * diff --git a/src/web/routes/test/auth/+page.svelte b/src/web/routes/test/auth/+page.svelte index 6650dd4..c337d65 100644 --- a/src/web/routes/test/auth/+page.svelte +++ b/src/web/routes/test/auth/+page.svelte @@ -74,12 +74,12 @@ - Active/Soma auth test + Active auth test
-

Active/Soma artifact lab

+

Active artifact lab

auth test surface

Validación funcional del engine server-authoritative: password flow, sesión de test, CSRF, diff --git a/src/web/routes/test/ecosystem/+page.svelte b/src/web/routes/test/ecosystem/+page.svelte index 4b4b3b7..837fefa 100644 --- a/src/web/routes/test/ecosystem/+page.svelte +++ b/src/web/routes/test/ecosystem/+page.svelte @@ -765,7 +765,7 @@ - Active/Soma ecosystem test + Active ecosystem test

diff --git a/tsconfig.json b/tsconfig.json index c81d41e..ada16d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "strict": true, "moduleResolution": "bundler" }, - "exclude": ["src/cach-v1/**", "src/active-soma-auth-implementation/**"] + "exclude": [] // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // diff --git a/vite.config.ts b/vite.config.ts index bd369a4..8bb08f4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ instances: [{ browser: 'chromium', headless: true }] }, include: ['src/**/*.svelte.{test,spec}.{js,ts}'], - exclude: ['src/libs/server/**', 'src/active-soma-auth-implementation/**'] + exclude: ['src/libs/server/**'] } }, @@ -27,7 +27,7 @@ export default defineConfig({ name: 'server', environment: 'node', include: ['src/**/*.{test,spec}.{js,ts}'], - exclude: ['src/**/*.svelte.{test,spec}.{js,ts}', 'src/active-soma-auth-implementation/**'] + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] } } ]