@ -1,31 +1,31 @@
# connection
`connection` 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 estructurado s.
`connection` is the realtime connections artifact. It is not "a WebSocket
wrapper": it is a connection registry with a swappable transport, reconnection ,
heartbeat, request/reply, channels, optional session integration and structured
diagnostic s.
La regla de nombres es importante :
The naming rule matters :
| Concepto | Nombre |
| ----------------------------------- | ------------------------------------------------- |
| Raíz imperativa | `createEngineConnections()` / `EngineConnections` |
| Raíz reactiva | `createActiveConnections()` / `ActiveConnections` |
| Unidad individual | `Connection` |
| Topic lógico dentro de una conexió n | `ConnectionChannel` |
| Concept | Name |
| --------------------------------- | ------------------------------------------------- |
| Imperative root | `createEngineConnections()` / `EngineConnections` |
| Reactive root | `createActiveConnections()` / `ActiveConnections` |
| Individual unit | `Connection` |
| Logical topic within a connectio 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` .
There is no `EngineConnection` or `ActiveConnection` . `Engine*` and `Active*`
are reserved for artifact roots; an individual connection is not a root, it is
an entity managed by `EngineConnections` .
## Uso Mínimo
## Minimal Use
```ts
import { createEngineConnections, createWebSocketTransport } from '$connection';
import { createEngineTimers } from '$timer';
// `timers` es OBLIGATORIO: connection no construye su propio scheduler.
// En composición se pasa `App.timers` ; standalone, `createEngineTimers()` .
// `timers` is REQUIRED: connection does not build its own scheduler.
// In composition you pass `App.timers` ; standalone, `createEngineTimers()` .
const Connections = createEngineConnections({ timers: createEngineTimers() });
const Main = Connections.createConnection('main', {
@ -36,7 +36,7 @@ const Main = Connections.createConnection('main', {
await Main.connect();
```
La raíz mantiene el mapa de conexiones :
The root keeps the connection map :
```ts
Connections.names();
@ -47,22 +47,21 @@ await Connections.reconnectAll('network-restored');
Connections.dispose();
```
`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*` .
The root's `dispose()` closes connections, cancels its own timers and clears the
listeners. After `dispose()` , public operations throw typed `Conn*` errors.
## ActiveConnections
La capa activa añade estado derivado para UI:
The active layer adds derived state for the UI:
```ts
// `timers` también es obligatorio aquí (se reenvía al engine interno ).
// `timers` is also required here (it is forwarded to the internal engine ).
const Connections = createActiveConnections({ timers: createEngineTimers() });
Connections.size;
Connections.states; // Record< name , ConnectionState >
// Nombres agrupados por estado (reactivos ):
// Names grouped by state (reactive ):
Connections.activeNames;
Connections.connectedNames;
Connections.connectingNames;
@ -70,7 +69,7 @@ Connections.reconnectingNames;
Connections.failedNames;
Connections.closedNames;
// Booleanos derivado s:
// Derived boolean s:
Connections.allConnected;
Connections.anyConnected;
Connections.anyConnecting;
@ -78,13 +77,13 @@ Connections.anyReconnecting;
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 .
`ActiveConnections` keeps the same creation/query API as the engine, but its
collections reflect each connection's state changes .
## Integración Con App
## App Integration
`active-app` expone el servicio `connections` cuando la app lo declara porqu e
las conexiones son app-scoped y suelen tener tipos específicos del proyecto :
`active-app` exposes the `connections` service when the app declares it, becaus e
connections are app-scoped and usually have project-specific types :
```ts
const App = createActiveApp({
@ -96,16 +95,15 @@ const App = createActiveApp({
const Connections = App.connections;
```
App inyecta :
App injects :
- `App.logger` , como `Logger` común de `$libs/logger` .
- `App.timers` , para reconexión, heartbeat y timeouts de ack .
- `App.logger` , as the common `Logger` from `$libs/logger` .
- `App.timers` , for reconnection, heartbeat and ack timeouts .
### Reacción a cambios de identidad
### Reacting to identity changes
El patrón canónico para reaccionar a cambios de sesión vive en los
**presets de `arts/active-app` **, no dentro del registry de
conexiones:
The canonical pattern for reacting to session changes lives in the
**`arts/active-app` presets**, not inside the connection registry:
```ts
import { applyStandardOrca } from '$active-app/presets';
@ -120,22 +118,21 @@ const App = createActiveApp({
});
applyStandardOrca(App);
// → registra applyConnectionsReauthOnIdentityChange y
// applyConnectionsCloseOnRevoke entre otros, todos ví a orca.
// → registers applyConnectionsReauthOnIdentityChange and
// applyConnectionsCloseOnRevoke among others, all vi a orca.
```
`applyConnectionsReauthOnIdentityChange` escucha
`SESSION_EVENT_IDENTITY_CHANGED` y llama
`App.connections.reauthenticateAll()` . `applyConnectionsCloseOnRevoke`
escucha `SESSION_EVENT_REVOKED` y llama `App.connections.closeAll()` .
Apps que prefieran granularidad pueden llamar a los presets
individuales en lugar del agregador.
`applyConnectionsReauthOnIdentityChange` listens to
`SESSION_EVENT_IDENTITY_CHANGED` and calls
`App.connections.reauthenticateAll()` . `applyConnectionsCloseOnRevoke` listens
to `SESSION_EVENT_REVOKED` and calls `App.connections.closeAll()` . Apps that
prefer granularity can call the individual presets instead of the aggregator.
### Sesión per-connection (modo manual )
### Per-connection session (manual mode )
Para casos donde una conexión concreta tiene u n `ConnectionSessionSource`
propio (ajeno al `App.session` global), o para usos standalone sin
orca, cada conexión sigue aceptando `session` en sus opcione s:
For cases where a specific connection has its ow n `ConnectionSessionSource`
(separate from the global `App.session` ), or for standalone uses without orca,
each connection still accepts `session` in its option s:
```ts
const Main = Connections.createConnection('main', {
@ -149,26 +146,26 @@ const Main = Connections.createConnection('main', {
});
```
Si la conexión declara `session.enabled` , su lógica interna
(`session-wiring.ts`) escucha el `onChange` del source y reacciona
con `reauthenticate()` / `disconnect()` . La reautenticación necesita
`auth` : define qué credencial nueva se envía cuando el source dice
"identidad cambió". Sin `auth` no se puede emitir un frame de reauth
y debe resolverse con reconnect/disconnect manual .
Las dos vías (preset orca a nivel App y `session` per-connection)
coexisten. La regla práctica: usa el preset cuando uses `arts/orca` y
`arts/session` ; usa `session` per-connection para casos standalone o
cuando la conexión vive fuera del ciclo App .
`connection` ** siempre** usa el `timers` inyectado — nunca construye su propio
scheduler (es un parámetro obligatorio). Los diagnósticos del scheduler salen bajo
la categoría `timer` ; los de conexiones salen bajo `connection` o
If the connection declares `session.enabled` , its internal logic
(`session-wiring.ts`) listens to the source's `onChange` and reacts with
`reauthenticate()` / `disconnect()` . Reauthentication needs `auth` : it defines
which new credential is sent when the source says "identity changed". Without
`auth` a reauth frame cannot be emitted and it must be resolved with a manual
reconnect/disconnect.
The two routes (App-level orca preset and per-connection `session` ) coexist. The
practical rule: use the preset when you use `arts/orca` and `arts/session` ; use
per-connection `session` for standalone cases or when the connection lives
outside the App lifecycle .
`connection` ** always** uses the injected `timers` — it never builds its own
scheduler (it is a required parameter). Scheduler diagnostics come out under the
`timer` category; connection diagnostics under `connection` or
`connection:<name>` .
## Estado s
## State s
Estados de conexión :
Connection states :
```txt
idle -> connecting -> open
@ -177,7 +174,7 @@ open -> closing -> closed
connecting/reconnecting -> failed
```
Campos útile s:
Useful field s:
```ts
Main.state;
@ -190,13 +187,13 @@ 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 .
`generation` changes when a new connection opens. Ack, heartbeat and reconnect
timeouts use that generation so they never resolve stale work against a new
connection .
## Transports
El contrato mínimo e s `ConnectionTransport` :
The minimal contract i s `ConnectionTransport` :
```ts
interface ConnectionTransport {
@ -214,17 +211,17 @@ interface ConnectionTransport {
}
```
Incluidos :
Included :
- `createWebSocketTransport()` para navegador/runtime con WebSocket.
- `createMockTransport()` para tests, loopback y páginas de diagnóstico .
- `createWebSocketTransport()` for browser/runtime with WebSocket.
- `createMockTransport()` for tests, loopback and diagnostic pages .
El transporte no decide reconexión, auth, heartbeat ni canales. Solo abre,
envía, cierra y emite evento s.
The transport does not decide reconnection, auth, heartbeat or channels. It only
opens, sends, closes and emits event s.
## Frames
El frame canónico :
The canonical frame :
```ts
interface ConnectionFrame< TType extends string = string, TPayload = unknown > {
@ -239,13 +236,13 @@ interface ConnectionFrame<TType extends string = string, TPayload = unknown> {
}
```
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 `ConnectionInvalidFrameError` según el punto de entrada .
The default serializer is JSON and validates the frame's minimal shape.
Encode/decode errors are not thrown as scattered strings: they return tagged
results or `ConnectionInvalidFrameError` errors depending on the entry point .
## Send Y Request/Reply
## Send and Request/Reply
`send()` devuelve un resultado tagged :
`send()` returns a tagged result :
```ts
const result = await Main.send('project.updated', { id: 'p1' });
@ -255,8 +252,8 @@ if (!result.ok) {
}
```
`request()` usa `ack: true` , genera un `id` y espera un frame entrante con
`replyTo` igual a ese id:
`request()` uses `ack: true` , generates an `id` and waits for an incoming frame
with `replyTo` equal to that id:
```ts
const reply = await Main.request< { id: string }, { ok: boolean }>('project.sync', {
@ -268,7 +265,7 @@ if (reply.ok) {
}
```
Razones de fallo principale s:
Main failure reason s:
- `timeout`
- `closed`
@ -276,9 +273,9 @@ Razones de fallo principales:
- `transport_error`
- `invalid_reply`
## Canale s
## Channel s
Los canales son topics nombrados dentro de una conexión. Se cachean por nombr e:
Channels are named topics within a connection. They are cached by nam e:
```ts
type ProjectEvents = {
@ -296,19 +293,19 @@ await Projects.send('project.updated', { id: 'p1', version: 2 });
await Projects.leave();
```
Estados de canal :
Channel states :
```txt
idle -> joining -> joined -> leaving -> left
joining -> failed
```
`dispose()` del canal limpia listeners y deja el canal en estado terminal
`left` .
The channel's `dispose()` clears listeners and leaves the channel in the
terminal `left` state .
## Reconexió n
## Reconnectio n
La reconexión usa `timer` y backoff configurable :
Reconnection uses `timer` and a configurable backoff :
```ts
Connections.createConnection('main', {
@ -326,10 +323,10 @@ Connections.createConnection('main', {
});
```
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 funciones reducidas por severidad; recibe `Diagnostics` , que
incluye el `Logger` completo y emite eventos catalogado s.
If `reconnectOnVisible` or `reconnectOnOnline` are active, the module listens to
browser events and requests reconnection when the connection is closed or
failed. That layer receives no severity-narrowed functions; it receives
`Diagnostics` , which includes the full `Logger` and emits catalogued event s.
## Heartbeat
@ -346,13 +343,13 @@ Connections.createConnection('main', {
});
```
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 d e
reconexión decida el siguiente paso .
The heartbeat sends `pingType` periodically and waits for `pongType` . If the
timeout elapses, it closes the connection with `heartbeat_timeout` and lets th e
reconnection policy decide the next step .
## Auth Y Sesió n
## Auth and Sessio n
Auth de conexión :
Connection auth :
```ts
Connections.createConnection('main', {
@ -365,7 +362,7 @@ Connections.createConnection('main', {
});
```
El resultado de auth e s tagged:
The auth result i s tagged:
```ts
await Main.reauthenticate(); // { ok: true } | { ok: false, reason, error? }
@ -395,12 +392,12 @@ Both routes coexist: pick the orca preset when using `arts/orca` +
`arts/session` ; pick the per-connection `session` option for manual
control.
`connection` no crea sesiones ni decide permisos. En servidor, los joins/sends
de un canal deben validarse con `auth/session/perm` .
`connection` does not create sessions or decide permissions. On the server, a
channel's joins/sends must be validated with `auth/session/perm` .
## Buffer
Cuando la conexión no está abierta, `send()` puede comportarse según policy:
When the connection is not open, `send()` can behave according to a policy:
```ts
buffer: {
@ -410,21 +407,21 @@ buffer: {
}
```
- `buffer` : encola y drena al abrir .
- `drop` : acepta la llamada pero descarta .
- `fail` : devuelve `{ ok: false, reason: 'closed' }` .
- `buffer` : queues and drains on open .
- `drop` : accepts the call but discards it .
- `fail` : returns `{ ok: false, reason: 'closed' }` .
## Diagnostics Y Logger
## Diagnostics and Logger
Las opciones públicas aceptan `logger?: Logger` desde `$libs/logger` . No existe
un contrato local de logger para `connection` .
The public options accept `logger?: Logger` from `$libs/logger` . There is no
local logger contract for `connection` .
```ts
import type { Logger } from '$libs/logger';
```
Internamente `connection` usa `createConnectionDiagnostics(logger)` y eventos
catalogados e n `CONNECTION_DIAGNOSTIC_EVENTS` :
Internally `connection` uses `createConnectionDiagnostics(logger)` and catalogued
events i n `CONNECTION_DIAGNOSTIC_EVENTS` :
- `connect_failed`
- `transport_error`
@ -441,13 +438,13 @@ catalogados en `CONNECTION_DIAGNOSTIC_EVENTS`:
- `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 ;
`connection` solo emite al contrato común .
Messages and categories live in `consts.ts` . If you want to send events to
Sentry, Loki or Datadog, inject an `EngineLogger` with the right transport ;
`connection` only emits to the common contract .
## Errore s
## Errors
Programmer errors lanzan clases tipada s:
Programmer errors throw typed classe s:
- `ConnectionDisposedError`
- `ConnectionAlreadyExistsError`
@ -458,18 +455,19 @@ Programmer errors lanzan clases tipadas:
- `ConnectionChannelNotFoundError`
- `ConnectionWebSocketUnavailableError`
Fallos runtime de transporte/envío/auth/request devuelven resultados tagged
para que el consumidor pueda decidir sin `try/catch` obligatorio .
Runtime transport/send/auth/request failures return tagged results so the
consumer can decide without a mandatory `try/catch` .
## Página De Prueba
## Test Page
La demo interactiva vive en `/active/docs/conn` (chat WebSocket + ciclo de vida de
conexión/canal). La demo de ecosistema usa `connection` con transporte mock loopback
para probar la integracion con `active-app` , `timer` , `logger` , `perm` y `cache` .
The interactive demo lives at `/active/docs/conn` (WebSocket chat + connection /
channel lifecycle). The ecosystem demo uses `connection` with a mock loopback
transport to test integration with `active-app` , `timer` , `logger` , `perm` and
`cache` .
## Testing
Tests principale s:
Main test s:
```txt
src/arts/connection/test/engine-connections.test.ts
@ -479,14 +477,14 @@ src/arts/connection/test/connection-state.test.ts
src/arts/connection/test/websocket.test.ts
```
Casos que deben mantenerse cubiertos :
Cases that must stay covered :
- creación duplicada y lookup inexistente ;
- `this` no requerido al desestructurar métodos del engine ;
- reloj inyectado para timestamps determinista s;
- reconnect con fake timers;
- duplicate creation and nonexistent lookup ;
- no `this` required when destructuring engine methods ;
- injected clock for deterministic timestamp s;
- reconnect with fake timers;
- heartbeat timeout;
- request/reply con timeout y rechazo ;
- request/reply with timeout and rejection ;
- channel join/leave/dispose;
- bridge de sesión refresh/expire/revoke;
- serializer inválido y transport errors.
- session bridge refresh/expire/revoke;
- invalid serializer and transport errors.