Rename modules from 4-letter aliases to full English words

Drops the 4-letter alias convention in favour of a single homogeneous
naming axis: full English words across filesystem, alias, wire format
and constants.

Module renames:
- arts/aapp     → arts/active-app    (libs/aapp also)
- arts/buss     → arts/bus           (libs/buss also)
- arts/cach     → arts/cache         (libs/cach + svrs/cach also)
- arts/conn     → arts/connection
- arts/fend     → arts/frontend
- arts/fmts     → arts/formats       (curr→currency, nums→numbers, unts→units)
- arts/logr     → arts/logger        (libs/logr also)
- arts/perm     → arts/permissions   (libs/perm + svrs/perm also)
- arts/sess     → arts/session
- arts/stor     → arts/storage
- arts/timr     → arts/timer         (libs/timers → libs/timer)

Modules left as-is: auth, dom, errs, http, lang, sium (already match
their canonical name or are proper names).

Special case: `aapp` could not become `app` because `$app` is reserved
by SvelteKit (`$app/stores`, `$app/navigation`, ...). Compromise:
- Filesystem and alias use `active-app` / `$active-app`.
- Constants and class names use `App` / `APP_*` (no `active-` prefix).
The `active-` prefix only disambiguates the alias from SvelteKit's
namespace; the module is App.

Special case: `permissions` keeps the plural for filesystem/alias/wire
but constants and classes use the singular `PERMISSION_*` /
`Permission*` because they describe the concept ("a permission
effect"), not the module collection.

Constants follow the new module name in caps: `STORAGE_*`, `BUS_*`,
`CACHE_*`, `CONNECTION_*`, `FORMATS_*`, `LOGGER_*`, `SESSION_*`,
`TIMER_*`, etc. Module values: `STORAGE_MODULE = 'storage'`,
`BUS_MODULE = 'bus'`, `APP_MODULE = 'app'`,
`PERMISSION_MODULE = 'permissions'`, etc.

Wire/code format moved accordingly: `'storage::*'`, `'bus::*'`,
`'session::*'`, `'permissions::*'`, etc. Diagnostic event values
updated: `'storage.error'`, `'bus.event.published'`,
`'connection.auth_failed'`, etc. App events use `'app.*'`:
`AAPP_EVENT_* → APP_EVENT_*` with values `'app.user.identity.changed'`.

Class renames (where they used the abbreviation):
- AappAlreadyCreatedError → AppAlreadyCreatedError
- BussError* → BusError* (where applicable)
- Cach* → Cache*
- Conn* → Connection* (e.g. ConnDisposedError → ConnectionDisposedError;
  ConnConnection* collapsed to Connection*)
- Logr*Error → Logger*Error
- Sess* → Session* (SessInvalidSessionError → SessionInvalidError)
- Stor* → Storage*
- Timr* → Timer* (TimrInactiveTimerError → TimerInactiveError)
- AuthCachPort → AuthCachePort
- AuthClientCach* → AuthClientCache*
- AuthPermPort → AuthPermissionsPort

Property renames in option types:
- `cach?:` → `cache?:` in AuthClient options
- `logr:` → `logger:` in svrs/auth ports
- `timr:` → `timer:` in svrs/auth ports

`docs/conventions.md` rewritten:
- Rule 1 dropped the 4-letter alias mandate; lists the full English
  module names and special-cases active-app, lang, sium, permissions.
- Rule 2 documents the new constant prefix convention and its two
  exceptions (APP_* for active-app, PERMISSION_* singular for
  permissions).
- Rule 6 codifies that all error infrastructure (codes, messages,
  classes, guards) lives in a single `errors.ts` per module —
  removing the `consts.ts` / `errors.ts` split for error-related
  symbols.

`libs/errs` adds `ErrorMessages` type so every module can declare its
catalog as `<MOD>_ERROR_MESSAGES: ErrorMessages` instead of repeating
the `Readonly<Record<ErrCode, string | (...args) => string>>` shape.
Storage migrated as the first proof of the canonical pattern.

All 1334 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent dace6d00d1
commit 4db6bd2b3b

@ -11,25 +11,54 @@ decision recorded here.
---
## 1. Module identifier — 4-letter alias
## 1. Module identifier — full English word
Every artifact and lib that pairs with one is identified by a stable
**4-letter alias** that already lives in `svelte.config.js` as a
bundler alias (`$buss`, `$conn`, `$sess`, …).
The same alias is the canonical module identifier across the codebase:
- **Path scoping in string values** (already enforced after audit
section 2): `'buss.event.published'`, `'conn.auth_failed'`,
`'sess.lifecycle.adopted'`, `'perm.client.remote_check_failed'`.
- **Constant name prefix**: `BUSS_*`, `CONN_*`, `SESS_*`, `PERM_*`,
`TIMR_*`, `LOGR_*`, `CACH_*`, `STOR_*`, `FMTS_*`, `FEND_*`, `ADOM_*`,
`AAPP_*`, `AUTH_*`, `LANG_*`, `HTTP_*`, `SIUM_*`, `ERRS_*`.
Forbidden: full English words like `BUS_*`, `CONNECTION_*`, `SESSION_*`,
`PERMISSION_*`, `TIMER_*`, `LOGGER_*`, `CACHE_*`, `STORAGE_*`,
`FORMATS_*`, `FRONTEND_*`. Drift on this axis is being closed in a
dedicated audit pass.
**full English word** that doubles as filesystem name, alias and
canonical wire identifier:
| Folder | Alias | Wire / value |
|---|---|---|
| `arts/active-app/`, `libs/active-app/` | `$active-app` | `'app'` |
| `arts/auth/`, `libs/auth/`, `svrs/auth/` | `$auth` | `'auth'` |
| `arts/bus/`, `libs/bus/` | `$bus` | `'bus'` |
| `arts/cache/`, `libs/cache/`, `svrs/cache/` | `$cache` | `'cache'` |
| `arts/connection/` | `$connection` | `'connection'` |
| `libs/dom/` | `$libs/dom` | `'dom'` |
| `libs/errs/` | `$libs/errs` | `'errs'` |
| `arts/frontend/` | `$frontend` | `'frontend'` |
| `arts/formats/` (+ `currency/`, `numbers/`, `units/`, `dates/`) | `$formats` | `'formats'` |
| `arts/http/`, `libs/http/` | `$http` | `'http'` |
| `arts/lang/`, `libs/lang/` | `$lang` | `'lang'` |
| `arts/logger/`, `libs/logger/` | `$logger` | `'logger'` |
| `arts/permissions/`, `libs/permissions/`, `svrs/permissions/` | `$permissions` | `'permissions'` |
| `arts/session/` | `$session` | `'session'` |
| `arts/sium/` | `$sium` | `'sium'` (proper name of the validator) |
| `arts/storage/` | `$storage` | `'storage'` |
| `arts/timer/`, `libs/timer/` | `$timer` | `'timer'` |
### Special cases
- **`active-app`** uses a hyphen in the alias because `$app` is a
reserved namespace in SvelteKit (`$app/stores`, `$app/navigation`).
The constants and class names still use `App` / `APP_*`; the
`active-` prefix only appears in filesystem and alias.
- **`lang`** and **`sium`** are kept as-is. `lang` is the standard
HTML/i18n term (`<html lang="…">`); `sium` is the proper name of
the validator (like `zod`, `valibot`). They are not abbreviations.
- **`errs`** is the framework's own error system. Kept short
intentionally because it sits at the foundation; lowering it to
`'errs'` keeps the wire format compact for codes like
`'errs::format_invalid'`.
### What this replaces
Earlier the codebase used 4-letter abbreviations (`buss`, `cach`,
`conn`, `sess`, `stor`, `timr`, `logr`, `aapp`, `fmts`, `fend`,
`perm`, `curr`, `unts`, `nums`). That convention has been removed:
abbreviations created cognitive load with no real benefit.
---
@ -41,7 +70,21 @@ Every categorical module-level constant follows the pattern:
<MOD>_<CATEGORY>_<NAME>
```
- **`<MOD>`** — the module's 4-letter alias in caps (rule 1).
- **`<MOD>`** — the module's identifier in caps. For most modules this
matches the filesystem (`STORAGE_*`, `BUS_*`, `CACHE_*`, `LOGGER_*`,
`TIMER_*`, `CONNECTION_*`, `SESSION_*`, `FORMATS_*`, `FRONTEND_*`,
`HTTP_*`, `LANG_*`, `AUTH_*`, `SIUM_*`, `DOM_*`, `ERRS_*`).
**Exception**: the `permissions` module uses `PERMISSION_*` (singular)
because the constants describe the concept ("a permission effect",
"a permission decision"), not the module collection. The `S` is
reserved for the folder/alias/wire which group the system as a
whole.
**Exception**: the `active-app` module uses `APP_*` (without the
`active-` prefix) because the `active-` prefix is purely a
disambiguator forced by SvelteKit's reserved `$app` namespace.
- **`<CATEGORY>`** — what kind of constant it is. Picked from the
fixed vocabulary in section 3.
- **`<NAME>`** — the specific value's identifier in
@ -51,20 +94,22 @@ Every categorical module-level constant follows the pattern:
```ts
// good
BUSS_DIAGNOSTIC_EVENTS
BUSS_DEFAULT_MAX_LISTENERS_PER_EVENT
BUSS_ERR_DISPOSED
SESS_EVENT_LIFECYCLE_ADOPTED
PERM_METHOD_CHECK
TIMR_STATE_PENDING
CONN_REASON_AUTH_FAILED
CACH_LIMIT_DEFAULT_MAX_ENTRIES
STOR_MODULE
// bad (will be flagged in audit)
BUS_DIAGNOSTIC_EVENTS // wrong module prefix (rule 1)
BUS_DIAGNOSTIC_EVENTS
BUS_DEFAULT_MAX_LISTENERS_PER_EVENT
BUS_ERR_DISPOSED
SESSION_EVENT_LIFECYCLE_ADOPTED
PERMISSION_METHOD_CHECK
TIMER_STATE_PENDING
CONNECTION_REASON_AUTH_FAILED
CACHE_LIMIT_DEFAULT_MAX_ENTRIES
STORAGE_MODULE // value: 'storage'
APP_MODULE // value: 'app'
PERMISSION_MODULE // value: 'permissions'
// bad
BUSS_DIAGNOSTIC_EVENTS // 4-letter alias is dead (rule 1)
DEFAULT_TIMER_SCOPE_SEPARATOR // category before module (rule 2)
PERMISSION_METHOD_CHECK // wrong module prefix (rule 1)
PERMISSIONS_EFFECT_ALLOW // wrong shape: concept is singular
AUTO_REAUTH_USER_IDENTITY_CHANGE // missing module prefix (rule 1)
```
@ -75,13 +120,13 @@ in the `<NAME>` part, not in `<CATEGORY>`:
```ts
// good
BUSS_LISTENER_ERROR_MODE_THROW
SESS_EVENT_LIFECYCLE_ADOPTED
SESS_EVENT_LIFECYCLE_REVOKED
PERM_DEFAULT_REMOTE_FAILURE_BACKOFF_MS
BUS_LISTENER_ERROR_MODE_THROW
SESSION_EVENT_LIFECYCLE_ADOPTED
SESSION_EVENT_LIFECYCLE_REVOKED
PERMISSION_DEFAULT_REMOTE_FAILURE_BACKOFF_MS
// bad
BUSS_LISTENER_ERR_MODE_THROW // category split across underscores
BUS_LISTENER_ERR_MODE_THROW // category split across underscores
```
---
@ -94,7 +139,7 @@ inside a module.
| Category | Use |
|---|---|
| `MODULE` | The module's canonical 4-letter alias as a string constant. **Single source of truth** for the module's identity. Used wherever the module's identifier is needed: logger category, error message prefix, event scope, etc. Every module declares exactly one `<MOD>_MODULE = '<alias>'`. |
| `MODULE` | The module's identifier as a string constant. **Single source of truth** for the module's identity. Used wherever the module's identifier is needed: logger category, error message prefix, event scope, etc. Every module declares exactly one `<MOD>_MODULE = '<value>'`. |
| `ERR` | Error codes (`ErrCode` values from `$libs/errs`) |
| `EVENT` | Bus or lifecycle event names (string discriminators) |
| `DIAGNOSTIC_EVENTS` | Event values published through `Logger` (catalog object) |
@ -110,18 +155,18 @@ inside a module.
| `ID_PREFIX` | Prefix used by ID factories |
| `LOG_MSG` | Log message strings |
| `ERROR_MSG` | Error message strings (technical, dev-facing). Must use a template referencing `<MOD>_MODULE` for the prefix, never a hardcoded literal: `` `[${MOD_MODULE}] ...` ``. |
| `ERROR_NAME` | Legacy `Error.name` strings (being replaced by `ERR` codes) |
| `ERROR_MESSAGES` | Catalogue object (`<MOD>_ERROR_MESSAGES: ErrorMessages`) indexed by `ErrCode`. Static strings or `(...args) => string` factories. |
| `CONTEXT_KEY` | Svelte context keys |
Categories that are **not** in this list (e.g. `AUTO_REAUTH`,
`AUTO_INVALIDATE`, `BACKOFF`, `BUFFER_POLICY`, `CHANNEL_STATE`) must
fold into one of the above. Examples:
- `CONNECTION_AUTO_REAUTH_*` → `CONN_REASON_AUTO_REAUTH_*` if the value
drives a "why this happened" branch, or `CONN_MODE_AUTO_REAUTH_*` if
it configures behavior.
- `CACHE_AUTO_INVALIDATE_*` → `CACH_MODE_AUTO_INVALIDATE_*`.
- `CONN_BUFFER_POLICY_*` → `CONN_MODE_BUFFER_*`.
- `CONNECTION_AUTO_REAUTH_*` → `CONNECTION_REASON_AUTO_REAUTH_*` if
the value drives a "why this happened" branch, or
`CONNECTION_MODE_AUTO_REAUTH_*` if it configures behavior.
- `CACHE_AUTO_INVALIDATE_*` → `CACHE_MODE_AUTO_INVALIDATE_*`.
- `CONNECTION_BUFFER_POLICY_*` → `CONNECTION_MODE_BUFFER_*`.
When in doubt, propose the addition here before introducing it.
@ -137,21 +182,21 @@ appears as a literal in error declarations:
```ts
import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs';
export const BUSS_ERR: ModuleSeed = moduleSeed('buss'); // 'buss::'
export const BUSS_ERR_DISPOSED: ErrCode = errCode(BUSS_ERR, 'disposed'); // 'buss::disposed'
export const BUSS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUSS_ERR, 'invalid_payload');
export const BUSS_ERR_LISTENER: ErrCode = errCode(BUSS_ERR, 'listener'); // 'buss::listener'
export const BUSS_ERR_LISTENER_FAILED: ErrCode = errCode(BUSS_ERR_LISTENER, 'failed'); // 'buss::listener.failed'
export const BUS_ERR: ModuleSeed = moduleSeed('bus'); // 'bus::'
export const BUS_ERR_DISPOSED: ErrCode = errCode(BUS_ERR, 'disposed'); // 'bus::disposed'
export const BUS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUS_ERR, 'invalid_payload');
export const BUS_ERR_LISTENER: ErrCode = errCode(BUS_ERR, 'listener'); // 'bus::listener'
export const BUS_ERR_LISTENER_FAILED: ErrCode = errCode(BUS_ERR_LISTENER, 'failed'); // 'bus::listener.failed'
```
The constant's `<NAME>` part mirrors the path segments inside the
runtime value: `BUSS_ERR_LISTENER_FAILED` ↔ `'buss::listener.failed'`.
The module name lives in exactly one place: `moduleSeed('buss')`.
runtime value: `BUS_ERR_LISTENER_FAILED` ↔ `'bus::listener.failed'`.
The module name lives in exactly one place: `moduleSeed('bus')`.
The `::` separator distinguishes module from hierarchy. `errCode`
picks the right separator automatically — `::` after a seed, `.`
between segments. A helper `codeToLangPath(c)` converts
`'buss::listener.failed'` → `'buss.listener.failed'` for the i18n
`'bus::listener.failed'` → `'bus.listener.failed'` for the i18n
path.
### Family matching
@ -159,41 +204,60 @@ path.
A single predicate `matches(err, family)` from `$libs/errs` covers
both common cases. `family` can be:
- A **module seed** (`BUSS_ERR`) — matches any error from that
- A **module seed** (`BUS_ERR`) — matches any error from that
module regardless of hierarchy.
- An **`ErrCode`** — matches the code itself or any hierarchical
descendant within the same module.
```ts
matches(err, BUSS_ERR_DISPOSED) // exact code
matches(err, BUSS_ERR_LISTENER) // any descendant of buss::listener
matches(err, BUSS_ERR) // any error declared by buss
matches(err, BUS_ERR_DISPOSED) // exact code
matches(err, BUS_ERR_LISTENER) // any descendant of bus::listener
matches(err, BUS_ERR) // any error declared by bus
```
### Error class names — full English word
Error classes use the full English word **of the concept**, not the
module's `<MOD>_*` prefix. The class name is consumer-facing API; the
constant prefix is internal organization:
```ts
// good
StorageInvalidTypeError // class
STORAGE_ERR_INVALID_TYPE // constant
BusDisposedError // class
BUS_ERR_DISPOSED // constant
PermissionDeniedError // class — singular concept
PERMISSION_ERR_DENIED // constant — singular prefix
AppAlreadyCreatedError // class — no Active prefix
APP_ERR_ALREADY_CREATED // constant
```
---
## 5. String values inside diagnostic / event constants
Already enforced after audit section 2:
Every string value emitted as an event identifier must start with the
module's identifier (rule 1) followed by `.` and the value-specific
path:
```ts
// good — value carries the module scope
export const CONN_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'conn.auth_failed',
RECONNECT_EXHAUSTED: 'conn.reconnect_exhausted'
export const CONNECTION_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'connection.auth_failed',
RECONNECT_EXHAUSTED: 'connection.reconnect_exhausted'
} as const;
// bad — bare value collides across modules
export const CONN_DIAGNOSTIC_EVENTS = {
export const CONNECTION_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'auth_failed', // collides with auth.* events
LISTENER_THREW: 'listener_threw' // collides with sess and timr
LISTENER_THREW: 'listener_threw' // collides with session and timer
} as const;
```
Rule: every string value emitted as an event identifier must start
with the module's 4-letter alias (rule 1) followed by `.` and the
value-specific path.
---
## 6. File and folder structure
@ -203,12 +267,13 @@ value-specific path.
| `libs/<mod>/` | Pure contracts: types, constants, error classes, helpers. No Svelte runes, no engine state. |
| `arts/<mod>/` | Runtime engine + `*.svelte.ts` active wrappers. May import from `$libs/<mod>` and from its own files. |
| `arts/<mod>/index.ts` | Public barrel. Re-exports the artifact's surface. |
| `arts/<mod>/errors.ts` | Single home for the module's error infrastructure: `<MOD>_ERR` seed, `<MOD>_ERR_*` codes, `<MOD>_ERROR_MESSAGES` catalog, error classes and `is*Error` guards. **Do not split** these across `consts.ts` and `errors.ts`; everything error-related lives together. |
| `arts/<mod>/test/` | Tests for engine and active wrappers. |
| `svrs/<mod>/` | Server-side counterparts (cookies, db adapters, route handlers). |
| `arts/aapp/` | Composition root. Allowed to import any `$<mod>`. |
| `arts/active-app/` | Composition root. Allowed to import any `$<mod>`. |
**Cross-artifact imports are forbidden** (`arts/X` cannot import
`$Y`). Sole exception: `arts/aapp/`.
`$Y`). Sole exception: `arts/active-app/`.
---
@ -216,10 +281,10 @@ value-specific path.
- A new category in section 3 requires a record-of-decision: who, when,
why.
- Module aliases are frozen: a new artifact picks a 4-letter alias
not yet taken; renaming an existing alias is a breaking change.
- Module names are frozen: a new artifact picks a name not yet taken;
renaming an existing module is a breaking change.
- Drift found in the codebase that violates these rules is filed as an
audit finding and resolved in a dedicated commit.
Last updated: `2026-05-02` — initial version after audit-1-5.md
sections 1–2 closed.
Last updated: `2026-05-02` — full English word convention, dropping
the 4-letter alias rule.

@ -0,0 +1,988 @@
REVISIÓN COMPLETA DE LA PARTE I
Fundamentos de la semántica perceptiva de la interfaz
Versión de revisión para Claude
OBJETIVO DEL DOCUMENTO
======================
Este documento corrige la revisión anterior de la Parte I. No es una lista breve de inserciones sobre Russell ni un parche aislado del Capítulo 10. Es una revisión integral de la Parte I como bloque editorial.
El problema detectado es estructural:
- las familias semánticas podían parecer categorías de diseño inventadas;
- los intents podían parecer una taxonomía arbitraria;
- los canales podían parecer capas técnicas;
- la crítica al semáforo podía quedar reducida a un resumen demasiado pobre;
- los fundamentos psicoperceptivos estaban presentes en la teoría, pero insuficientemente visibles en el manuscrito.
La revisión debe hacer explícito que el libro se apoya en:
- organización perceptiva y Gestalt;
- figura/fondo;
- affordances y acción situada;
- atención y saliencia;
- appraisal y evaluación del evento;
- Russell y el espacio valencia/activación;
- multisensorialidad;
- correspondencias crossmodales;
- esquemas sensoriomotores;
- accesibilidad como prueba de distribución semántica.
La tesis editorial de la revisión es:
> Este libro no inventa categorías.
> Nombra estructuras que ya participan en la forma en que los usuarios perciben, atienden, actúan, evalúan y comprenden los cambios de una interfaz.
REGLA DE INTEGRACIÓN
====================
No repetir autores en cada capítulo.
No convertir cada idea en cita académica.
No escribir como paper.
La distribución correcta es:
1. Capítulo 3 declara el mapa completo de fundamentos.
2. Capítulos 4–8 aplican esos fundamentos al evento, gramática, interacción semántica, criterios y familias.
3. Capítulo 9 aplica appraisal, figura/fondo y affordances a la diferencia valencial/transicional.
4. Capítulo 10 ancla los intents en Russell, valencia/activación y la crítica histórica al semáforo.
5. Parte II y Parte III retoman esos fundamentos sin volver a explicarlos desde cero.
ESTRUCTURA REVISADA DE LA PARTE I
=================================
Introducción — La pregunta que aparece al programar interfaces
Capítulo 1 — Qué entendemos por interfaz
Capítulo 2 — Por qué preguntamos ahora
Capítulo 3 — Fundamentos psicoperceptivos de la interfaz
Capítulo 4 — El evento interactivo como unidad mínima de significado
Capítulo 5 — Por qué una gramática del evento
Capítulo 6 — Qué es una interacción semántica
Capítulo 7 — Criterios para identificar una interacción semántica
Capítulo 8 — De las clases de evento a las familias semánticas
Capítulo 9 — Semánticas valenciales y transicionales
Capítulo 10 — Intent: más allá del semáforo
Cierre de Parte I
INTRODUCCIÓN
La pregunta que aparece al programar interfaces
===============================================
DIAGNÓSTICO
-----------
La introducción funciona bien si conserva el tono humilde del programador que se pregunta por qué todo está fragmentado: componentes, estilos, estados, motion, sonido, accesibilidad y lógica viven en capas distintas, mientras el usuario percibe una experiencia unificada.
PROBLEMA DETECTADO
------------------
Faltaba explicitar dos cosas:
1. El libro usa la web como campo principal de ejemplos.
2. La teoría no se limita a la web.
INSERCIÓN RECOMENDADA
---------------------
Ubicación: después de presentar la fragmentación entre componentes, estilos, motion, sonido, háptica y accesibilidad.
Texto:
Este libro usará principalmente la web como campo de observación y ejemplo.
No porque la teoría pertenezca solo a la web, sino porque en la web la fragmentación se vuelve especialmente visible. El desarrollador trabaja con DOM, CSS, JavaScript, eventos técnicos, componentes, estados, media queries, Web Audio API, preferencias de usuario y accesibilidad como capas separadas. Cada una tiene su propia lógica. Cada una se documenta de forma distinta. Cada una suele resolverse con herramientas distintas.
Pero el usuario no experimenta esas capas por separado.
El usuario no percibe “un cambio de clase CSS”, “un listener de click”, “una transición”, “un aria-live” o “un token de color”. Percibe que algo respondió, que algo se abrió, que algo quedó guardado, que algo reclama atención, que algo se perdió, que algo sigue ocurriendo.
La web será, por tanto, nuestro laboratorio principal. Pero no el límite de la teoría.
Un usuario puede percibir contacto, espera, pérdida, confirmación, señal, manipulación o cambio de marco en una app nativa, en un wearable, en una interfaz espacial, en un sistema embebido o en un panel físico. Lo que cambia es la realización técnica: latencia, fluidez, calidad háptica, acceso al sonido, rendimiento, políticas de plataforma y preferencias del usuario.
La teoría se formula para interfaces en general.
Los ejemplos partirán sobre todo de la web porque ahí la necesidad es especialmente clara.
Principio añadido:
> La web será el campo principal de ejemplos, no el límite de la teoría.
CAPÍTULO 1
Qué entendemos por interfaz
Hacia una definición operativa de la interfaz
=============================================
DIAGNÓSTICO
-----------
Este capítulo ya es fuerte. Redefine la interfaz como algo más que superficie visible y la formula como dimensión perceptible de un contexto operacional. Esa idea debe mantenerse.
PROBLEMA DETECTADO
------------------
El capítulo contiene intuiciones psicoperceptivas, pero todavía no anuncia suficientemente que después habrá un capítulo de fundamentos. Debe preparar el Capítulo 3 sin cargarlo todavía de teoría.
FUNCIÓN CORREGIDA DEL CAPÍTULO
------------------------------
Debe responder:
- qué ha significado tradicionalmente “interfaz”;
- por qué esa definición sigue siendo útil;
- por qué se queda corta si solo pensamos en componentes;
- por qué el usuario percibe eventos, no solo objetos;
- por qué la interfaz debe entenderse como dimensión perceptible de un contexto operacional.
INSERCIÓN 1: PUENTE HACIA FUNDAMENTOS
-------------------------------------
Ubicación: al final de la sección “La interfaz no es solo visual”.
Texto:
Esta idea se desarrollará con más precisión en el capítulo de fundamentos psicoperceptivos. Allí veremos que la interfaz no solo se organiza visualmente; también se apoya en percepción de campo, affordances, atención, evaluación, movimiento, sonido, multisensorialidad y experiencia corporal.
Por ahora basta con fijar una tesis:
> La interfaz no es visual más algunos extras.
> Es perceptiva.
INSERCIÓN 2: SUAVIZAR NEUROAFIRMACIONES
---------------------------------------
Si aparece una frase del tipo “cada canal tiene sus circuitos propios”, sustituir por:
Cada canal tiene condiciones perceptivas propias, formas propias de comunicar significado y formas propias de fallar. No se trata de reducir la interfaz a neurofisiología, sino de reconocer que color, forma, movimiento, sonido, háptica, tiempo y profundidad no son equivalentes.
INSERCIÓN 3: CIERRE REFORZADO
-----------------------------
Añadir en la síntesis:
La redefinición de interfaz como dimensión perceptible de un contexto operacional prepara el desplazamiento central del libro: del componente al evento. El componente sigue siendo necesario, pero ya no basta como unidad principal de significado. Cuando la interfaz cambia, responde, espera, interrumpe, confirma o pierde algo, el usuario no interpreta solo objetos: interpreta eventos.
CAPÍTULO 2
Por qué preguntamos ahora
Sedimentación, dependencia del camino y carga paradigmática
==========================================================
DIAGNÓSTICO
-----------
Este capítulo funciona como legitimación histórica. Explica por qué ciertas convenciones persistieron sin examen. Debe mantenerse.
PROBLEMA DETECTADO
------------------
Antes mezclaba justificación histórica con fundamentos científicos. Ahora, al existir Capítulo 3, debe dejar de desarrollar los fundamentos y limitarse a anunciarlos.
FUNCIÓN CORREGIDA
-----------------
Debe responder:
- por qué las convenciones actuales no son simples errores;
- cómo se sedimentan;
- cómo la dependencia del camino las estabiliza;
- cómo el paradigma dominante impide formular ciertas preguntas;
- por qué ahora sí tiene sentido revisar esas premisas.
INSERCIÓN RECOMENDADA: CIERRE HACIA EL CAPÍTULO 3
-------------------------------------------------
Ubicación: final de “Por qué ahora”.
Texto:
Lo que ha cambiado no es que hayamos descubierto de pronto la percepción humana. La psicología de la percepción, la teoría de affordances, la atención, los modelos de evaluación afectiva, la multisensorialidad y los esquemas corporales llevan décadas ofreciendo herramientas para pensar mejor la interacción.
Lo que ha cambiado es la necesidad de reunir esos marcos en una teoría de interfaz.
Los sistemas de diseño han madurado. La web y las plataformas nativas tienen más canales disponibles. Las interfaces son más dinámicas, más adaptativas, más autónomas y más temporales. Las convenciones heredadas empiezan a mostrar sus límites.
Por eso el siguiente capítulo hará una pausa antes de entrar en evento, gramática e interacción semántica. Presentará los fundamentos psicoperceptivos que sostendrán el resto del libro.
No para convertir el diseño en psicología aplicada mecánicamente.
Sino para evitar que la teoría semántica de la interfaz parezca una colección de intuiciones personales.
CORRECCIÓN DE TONO
------------------
Evitar afirmaciones como:
- “la neurociencia ha demostrado que…”;
- “esto es consenso absoluto…”;
- “el cerebro procesa…”.
Preferir:
- “este marco ayuda a explicar…”;
- “la literatura sobre percepción sugiere…”;
- “esta distinción es coherente con…”;
- “para los fines de este libro, resulta útil…”.
CAPÍTULO 3
Fundamentos psicoperceptivos de la interfaz
De qué está hecha la percepción interactiva
===========================================
DIAGNÓSTICO
-----------
Este capítulo debe convertirse en el gran mapa científico del libro. No basta con mencionar teorías. Debe decir para qué sirve cada una dentro de la arquitectura.
PROBLEMA DETECTADO
------------------
La versión anterior mencionaba fundamentos, pero no los distribuía con suficiente precisión en el sistema. Debe ser reescrito como capítulo ancla.
FUNCIÓN CORREGIDA
-----------------
Debe explicar:
- qué fundamentos existen;
- qué parte de la teoría sostiene cada uno;
- qué no pretende demostrar;
- cómo se usará después.
ESTRUCTURA REVISADA DEL CAPÍTULO
--------------------------------
1. Por qué necesitamos fundamentos perceptivos
2. Organización perceptiva: Gestalt, agrupación y figura/fondo
3. Affordances y acción situada
4. Atención, saliencia y carga cognitiva
5. Appraisal: evaluación del evento
6. El espacio evaluativo: valencia y activación
7. Movimiento, causalidad y continuidad
8. Sonido y correspondencias crossmodales
9. Integración multisensorial
10. Esquemas sensoriomotores y cognición corporeizada
11. Accesibilidad como prueba de distribución semántica
12. Cómo usará este libro estos fundamentos
SECCIÓN NUEVA OBLIGATORIA: EL ESPACIO EVALUATIVO
------------------------------------------------
Ubicación: después de Appraisal y antes de Movimiento.
Texto:
## El espacio evaluativo: valencia y activación
Hasta ahora hemos descrito cómo el usuario percibe, atiende y actúa. Falta una dimensión esencial: cómo evalúa lo que ocurre.
Cuando un evento interactivo sucede, el usuario no solo lo reconoce. También lo interpreta:
- ¿esto es bueno o malo para mí?
- ¿requiere acción inmediata?
- ¿puedo ignorarlo?
- ¿ya ocurrió o aún puedo evitarlo?
- ¿puedo corregirlo?
- ¿se resolvió algo que estaba pendiente?
Una forma útil de organizar estas evaluaciones es el espacio propuesto por James A. Russell, que describe los estados afectivos mediante dos dimensiones:
- valencia: positivo / negativo;
- activación: baja / alta.
Este modelo no pretende capturar toda la complejidad emocional. No dice todo lo que hay que decir sobre la experiencia afectiva. Pero resulta suficiente para distinguir diferencias que una interfaz necesita comunicar de forma consistente.
Por ejemplo:
- no es lo mismo algo negativo urgente que algo negativo ya ocurrido;
- no es lo mismo una confirmación leve que una culminación;
- no es lo mismo informar que advertir;
- no es lo mismo pedir atención que registrar una pérdida.
Estas diferencias no son estilísticas. Son operativas.
Este libro utilizará ese espacio como base para definir los intents, no como una teoría psicológica completa, sino como una estructura mínima para organizar la evaluación del evento.
FIGURA OBLIGATORIA
------------------
## Figura — Espacio valencia/activación aplicado a intents
Brief para Claude:
Crear un plano cartesiano.
Eje horizontal:
valencia negativa ←→ valencia positiva
Eje vertical:
activación baja ↑ activación alta
Colocar:
- threat: negativa / alta activación
- risk: negativa / activación media
- loss: negativa / baja activación
- neutral: centro / baja activación
- affirm: positiva / baja activación
- fulfill: positiva / activación media-alta
Pie:
Los intents no son colores. Son regiones evaluativas del evento.
TABLA CENTRAL OBLIGATORIA
-------------------------
## Tabla — Fundamentos y función dentro del libro
| Fundamento | Qué explica | Qué sostiene en la teoría |
|---|---|---|
| Gestalt | agrupación, figura/fondo, campo perceptivo | presencia, profundidad, emerge, shift |
| Gibson / affordances | acción posible | contexto operacional, contact, handle |
| Atención / saliencia | orientación del foco y carga cognitiva | signal, proporción, fatiga |
| Appraisal | evaluación respecto a metas, control y consecuencia | valencial/transicional, intent |
| Russell | valencia y activación | seis intents |
| Motion / causalidad | continuidad, agencia, trayectoria | contact, handle, emerge, shift |
| Spence / crossmodalidad | correspondencias entre canales | sonido, coordinación multicanal |
| Multisensorialidad | integración de señales | canales, sincronía, accesibilidad |
| Embodiment / Johnson | esquemas corporales | handle, contact, sustain, loss |
| Accesibilidad | variabilidad perceptiva | migración semántica |
CIERRE RECOMENDADO
------------------
Estos fundamentos no dictan una taxonomía cerrada. No prueban que solo existan siete familias ni seis intents. Lo que hacen es otra cosa: ofrecen criterios para que las categorías del libro no parezcan arbitrarias.
A partir de aquí, cuando hablemos de evento, gramática, interacción semántica, familias, intent y canales, no estaremos hablando de nombres inventados desde el diseño. Estaremos nombrando formas recurrentes de percibir, actuar, atender, evaluar e integrar cambios.
CAPÍTULO 4
El evento interactivo como unidad mínima de significado
======================================================
DIAGNÓSTICO
-----------
El capítulo es conceptualmente correcto, pero debe apoyarse explícitamente en el Capítulo 3.
FUNCIÓN CORREGIDA
-----------------
Debe mostrar que el evento no es una ocurrencia técnica, sino una modificación perceptible con relevancia operativa.
INSERCIÓN RECOMENDADA
---------------------
Ubicación: después de la definición de evento interactivo.
Texto:
Esta definición se apoya en los fundamentos anteriores.
Desde la organización perceptiva, un evento modifica el campo: algo aparece, cambia de figura, se subordina, se agrupa o se separa.
Desde las affordances, un evento modifica lo que el usuario puede hacer o cree que puede hacer.
Desde la atención, un evento puede orientar, reclamar, sostener o liberar foco.
Desde el appraisal, un evento puede volverse evaluable: favorable, riesgoso, urgente, perdido o resuelto.
Desde la multisensorialidad, un evento puede distribuirse entre varios canales y aun así percibirse como una sola unidad.
Por eso un evento interactivo no equivale a un click, a un callback ni a un cambio interno de estado. Un evento interactivo existe cuando una modificación perceptible altera la relación operativa entre usuario y sistema.
INSERCIÓN DE CONTRASTE
----------------------
Añadir una tabla breve:
| Plano | Qué ocurre | ¿Es evento interactivo? |
|---|---|---|
| Callback interno | cambia lógica | no necesariamente |
| Cambio visual decorativo | cambia apariencia | no necesariamente |
| Press visible | registra acción | sí, si afecta agencia |
| Estado fijado | cambia consecuencia | sí |
| Alerta | orienta atención | sí |
| Progreso | sostiene espera | sí |
CAPÍTULO 5
Por qué una gramática del evento
================================
DIAGNÓSTICO
-----------
El capítulo justifica la palabra gramática, pero debe apoyarse más en diferenciación perceptiva y carga cognitiva.
FUNCIÓN CORREGIDA
-----------------
Debe responder:
- qué es una gramática;
- por qué una interfaz necesita una gramática de eventos;
- por qué no bastan efectos, componentes o patrones.
INSERCIÓN RECOMENDADA
---------------------
Ubicación: después de definir gramática.
Texto:
La necesidad de una gramática no es solo conceptual. Es perceptiva.
La atención humana no procesa todos los cambios con el mismo peso. La percepción agrupa, separa y jerarquiza. El usuario aprende regularidades por exposición repetida. Si el sistema usa la misma forma perceptiva para eventos distintos, aumenta la carga interpretativa. Si usa formas distintas sin regularidad, impide aprendizaje implícito.
Una gramática del evento intenta reducir esa carga.
Permite que ciertas diferencias se vuelvan reconocibles:
- contacto no es consolidación;
- aparición no es señal;
- señal no es amenaza;
- amenaza no es pérdida;
- proceso no es resultado;
- manipulación no es contacto repetido;
- cambio de marco no es simple aparición.
Sin gramática, el usuario debe reconstruir esas diferencias caso por caso.
Con gramática, el sistema las vuelve perceptivamente disponibles.
PRINCIPIO AÑADIDO
-----------------
> Una gramática no añade complejidad al usuario.
> La retira del momento de interpretación y la desplaza al diseño del sistema.
CAPÍTULO 6
Qué es una interacción semántica
================================
DIAGNÓSTICO
-----------
El capítulo ya tiene una definición fuerte. Debe conectarse mejor con fundamentos y canales.
FUNCIÓN CORREGIDA
-----------------
Debe definir la interacción semántica como puente entre evento y realización perceptiva.
INSERCIÓN RECOMENDADA
---------------------
Ubicación: después de la definición de interacción semántica.
Texto:
Esta definición tiene una consecuencia importante: una interacción semántica no pertenece a un único canal.
Puede apoyarse en organización perceptiva, si necesita cambiar figura/fondo.
Puede apoyarse en affordance, si modifica acción posible.
Puede apoyarse en atención, si reclama foco.
Puede apoyarse en appraisal, si porta evaluación.
Puede apoyarse en motion, sonido, color, forma, presencia, profundidad o háptica.
Lo que la define no es el recurso que usa, sino la pregunta perceptiva que responde.
Una misma semántica puede migrar de un canal a otro sin perder identidad si conserva su función. Un contacto puede ser motion, sonido o háptica. Una señal puede ser color, forma, texto o sonido. Una pérdida puede ser retirada, huella, texto o silencio grave.
Por eso hablaremos de firma perceptiva, no de receta.
CAPÍTULO 7
Criterios para identificar una interacción semántica
===================================================
DIAGNÓSTICO
-----------
El capítulo funciona como disciplina taxonómica. Debe explicitar que cada criterio tiene un fundamento psicoperceptivo.
INSERCIÓN RECOMENDADA
---------------------
Ubicación: después de presentar los criterios.
Texto:
Estos criterios no son solo reglas internas de clasificación. Cada uno se apoya en una dimensión perceptiva:
- la relevancia operativa se apoya en affordances: una diferencia importa si cambia lo que el usuario puede hacer o interpretar;
- la diferenciabilidad se apoya en organización perceptiva y atención: una categoría debe poder percibirse como distinta;
- la recurrencia se apoya en aprendizaje implícito: una semántica necesita repetirse para volverse reconocible;
- la estabilidad se apoya en memoria perceptiva: la identidad debe sobrevivir a variaciones;
- la elasticidad se apoya en adaptación de canales: la misma función puede expresarse de maneras distintas;
- la economía cognitiva se apoya en reducción de carga interpretativa;
- la multicanalidad se apoya en integración sensorial;
- la accesibilidad se apoya en variabilidad perceptiva.
Una interacción semántica no se acepta porque tenga nombre. Se acepta porque nombra una diferencia que el usuario necesita percibir de forma recurrente.
TABLA AÑADIDA
-------------
| Criterio | Fundamento |
|---|---|
| Relevancia operativa | affordances |
| Diferenciabilidad | Gestalt / atención |
| Recurrencia | aprendizaje implícito |
| Estabilidad | memoria perceptiva |
| Elasticidad | variación de canales |
| Multicanalidad | integración multisensorial |
| Evaluabilidad | appraisal |
| Accesibilidad | migración semántica |
CAPÍTULO 8
De las clases de evento a las familias semánticas
=================================================
DIAGNÓSTICO
-----------
El capítulo debe seguir siendo la transición hacia las siete familias, pero debe hacer más explícito que las familias derivan de preguntas perceptivas y fundamentos.
FUNCIÓN CORREGIDA
-----------------
Debe explicar:
- por qué se reduce de semánticas exploratorias a siete familias;
- qué pregunta responde cada una;
- qué fundamento perceptivo la sostiene.
INSERCIÓN RECOMENDADA
---------------------
Ubicación: después de presentar las siete familias.
Texto:
Estas familias no son categorías de diseño. Son categorías perceptivas.
Cada una se apoya en una forma distinta de organizar la experiencia:
- contact se apoya en agencia, causalidad inmediata y esquema de contacto;
- commit se apoya en cierre cognitivo, consecuencia y memoria de estado;
- signal se apoya en atención, saliencia y orientación del foco;
- handle se apoya en acción corporal, control continuo y manipulación;
- emerge se apoya en figura/fondo, presencia y aparición perceptiva;
- shift se apoya en cambio de marco cognitivo y orientación contextual;
- sustain se apoya en continuidad temporal, espera y mantenimiento de confianza.
Dicho de otro modo:
> Las familias no salen de componentes.
> Salen de preguntas perceptivas recurrentes.
TABLA AÑADIDA
-------------
| Familia | Pregunta | Fundamento principal |
|---|---|---|
| contact | ¿me ha sentido? | agencia / causalidad |
| commit | ¿quedó fijado? | cierre / consecuencia |
| signal | ¿debo atender? | atención / saliencia |
| handle | ¿lo controlo? | acción corporal / manipulación |
| emerge | ¿apareció? | figura/fondo / presencia |
| shift | ¿cambió el marco? | modelo mental / orientación |
| sustain | ¿sigue ocurriendo? | continuidad temporal |
CAPÍTULO 9
Semánticas valenciales y transicionales
=======================================
DIAGNÓSTICO
-----------
Este capítulo es muy importante y debe ser reescrito como aplicación de fundamentos, no como introducción de fundamentos desde cero.
PROBLEMA DETECTADO
------------------
La versión anterior introducía appraisal, Gestalt, affordances, atención, afecto y esquemas como si el lector no los conociera. Ahora ya deben haber aparecido en Capítulo 3.
FUNCIÓN CORREGIDA
-----------------
Debe responder:
- qué eventos son mensaje y qué eventos son marco;
- por qué no todos aceptan intent;
- cómo se compone marco + mensaje.
ESTRUCTURA REVISADA
-------------------
1. El problema: colorear el contenedor
2. La distinción central: mensaje y marco
3. Aplicación de appraisal
4. Aplicación figura/fondo
5. Aplicación affordances y atención
6. Aplicación esquemas sensoriomotores
7. Mapa de familias según evaluabilidad
8. Reglas de composición
9. Antipatrones
10. Ejemplo completo
11. Síntesis
INSERCIÓN DE APERTURA REVISADA
------------------------------
Texto:
El Capítulo 3 presentó varios fundamentos que ahora convergen en una distinción central.
Desde el appraisal, algunos eventos se evalúan respecto a metas, control, urgencia y consecuencia. Desde la Gestalt, algunos eventos funcionan como figura y otros como fondo. Desde las affordances, algunos eventos permiten o comunican acción; otros organizan el campo donde la acción ocurre. Desde la atención, algunas señales reclaman foco; otras solo reorientan el contexto. Desde los esquemas sensoriomotores, algunos eventos son agentivos; otros son estructurales.
Todas esas distinciones apuntan a una misma idea:
> Hay eventos que son mensaje.
> Hay eventos que son marco.
Los primeros pueden portar intent.
Los segundos normalmente no.
INSERCIÓN CLAVE
---------------
Ubicación: después de “colorear el contenedor”.
Texto:
El error no es usar intent. El error es aplicarlo al evento equivocado.
Un modal puede contener una amenaza, pero el modal no es la amenaza. Un spinner puede preceder a un fallo, pero el spinner no es el fallo. Una vista puede mostrar un éxito, pero la navegación hasta esa vista no es el éxito.
El intent debe vivir en el evento que porta evaluación.
Regla:
> El intent modula la figura evaluable, no el fondo estructural.
MAPA DE FAMILIAS SEGÚN EVALUABILIDAD
------------------------------------
| Familia | Naturaleza dominante | Relación con intent |
|---|---|---|
| contact | agentiva mínima | leve / anticipatoria |
| commit | evaluable | plena |
| signal | evaluable / atencional | plena |
| handle | mixta | sobre todo en drop |
| emerge | estructural | normalmente no |
| shift | estructural | normalmente no |
| sustain | estructural | normalmente no |
CAPÍTULO 10
Intent: más allá del semáforo
=============================
DIAGNÓSTICO
-----------
Este capítulo era el más afectado. Había quedado demasiado resumido. Debe recuperar la fuerza del documento original “Más allá del semáforo”.
PROBLEMAS DETECTADOS
--------------------
1. La genealogía del semáforo estaba amputada.
2. Russell aparecía poco.
3. El paso de color a espacio afectivo no estaba suficientemente dramatizado.
4. Las líneas de cada intent eran demasiado escuetas.
5. Info no quedaba suficientemente separado de intent.
6. Threat/loss y affirm/fulfill necesitaban más peso.
ESTRUCTURA REVISADA
-------------------
1. El intent como problema no examinado
2. La herencia del semáforo
3. De la carretera a la pantalla
4. Qué codifica realmente la convención heredada
5. Qué debería ser un intent
6. El espacio afectivo: Russell, valencia y activación
7. Las seis regiones evaluativas
8. Lo que la convención heredada pierde
9. Cómo modula el intent
10. Neutralidad no es ausencia de intent
11. Familia e intent
12. Antipatrones
13. Ejemplo narrativo
14. Principios
15. Síntesis
SUSTITUCIÓN AMPLIADA PARA SECCIONES 1–4
---------------------------------------
## 1. El intent como problema no examinado
Existe en el diseño de interfaces una convención tan extendida que ha dejado de parecer convención.
Cuando algo ha ido mal, usamos rojo.
Cuando algo requiere precaución, usamos amarillo.
Cuando algo ha ido bien, usamos verde.
Cuando algo es informativo, usamos azul.
A ese conjunto lo llamamos danger, warning, success, info. O lo llamamos error, positive, negative, notice. Pero la estructura permanece.
Lo notable no es que usemos esa convención. Lo notable es que casi nunca preguntamos de dónde viene, qué cubre, qué omite y qué mezcla.
¿Por qué success debe cubrir tanto una operación importante completada como un autoguardado menor?
¿Por qué danger debe cubrir tanto una amenaza activa como una pérdida ya consumada?
¿Por qué warning mezcla un riesgo corregible, una precaución leve y a veces una amenaza seria?
¿Por qué info aparece junto a los intents si muchas veces no comunica valencia, sino solo saliencia atencional?
¿Por qué el intent se ha definido durante años como color contextual y no como evaluación del evento?
Estas preguntas son sencillas. Pero no aparecen con frecuencia porque la convención está normalizada. Entró en frameworks, tokens, clases CSS, sistemas de diseño, documentación, temas y componentes. Y una vez que algo entra en infraestructura, deja de parecer una decisión.
Parece la forma natural de hacer las cosas.
La pregunta del capítulo es:
> ¿Qué ocurre cuando dejamos de tratar el intent como color y empezamos a tratarlo como evaluación perceptiva del evento?
## 2. La herencia del semáforo
La taxonomía heredada de intents no nació en las interfaces digitales.
Antes de que existieran botones verdes de success, alertas rojas de danger o banners amarillos de warning, esos colores ya cargaban con una larga historia de señalización industrial, ferroviaria y vial.
El rojo, el amarillo y el verde pertenecían a entornos donde una señal debía reconocerse rápido, a distancia y con consecuencias físicas claras. No eran colores decorativos. Eran instrucciones operativas. Detenerse. Proceder con precaución. Avanzar. Prestar atención. Evitar una colisión. No entrar. Continuar.
Esa estructura era adecuada para muchos sistemas físicos de señalización porque resolvía un problema urgente: permitir decisiones rápidas con bajo coste interpretativo.
Pero esa estructura no era una teoría afectiva completa.
No pretendía distinguir entre una amenaza activa y una pérdida consumada. No pretendía separar una confirmación suave de una culminación significativa. No pretendía diferenciar saliencia informativa de evaluación emocional. Su función era mucho más concreta: codificar órdenes y estados operativos mediante colores muy reconocibles.
Cuando las interfaces gráficas empezaron a necesitar comunicar estados del sistema —error, advertencia, éxito, información—, heredaron esa estructura porque ya era comprensible. La cultura ya había aprendido que rojo pesa más que verde, que amarillo pide precaución, que verde permite continuar.
El diseño digital no inventó esa asociación. La recibió.
El problema no fue heredar.
El problema fue olvidar que se estaba heredando.
Con el tiempo, la asociación rojo / amarillo / verde dejó de parecer una convención histórica y empezó a parecer una estructura natural de la interfaz. El rojo ya no era simplemente una señal cromática útil: pasó a llamarse danger, error, destructive. El verde pasó a llamarse success. El amarillo pasó a llamarse warning. Y el azul, que no pertenecía del mismo modo a la lógica del semáforo, terminó ocupando el cajón residual de lo informativo: info.
Ahí ocurrió la mutación decisiva:
> una convención cromática empezó a funcionar como si fuera una taxonomía semántica.
## 3. De la carretera a la pantalla
La web estabilizó esa mutación de forma especialmente clara.
Frameworks como Bootstrap necesitaban resolver un problema práctico: ofrecer clases contextuales simples para botones, alertas, tablas, mensajes y estados. La solución era evidente, reconocible y fácil de usar:
success
warning
danger
info
El éxito se volvió verde.
La advertencia, amarilla.
El peligro, rojo.
La información, azul.
Aquella decisión era razonable. Funcionaba. Era barata cognitivamente y barata técnicamente. Cualquier equipo podía aplicarla sin abrir una discusión teórica sobre afecto, percepción o semántica. Bastaba con asignar una clase.
Pero al entrar en clases CSS, documentación, temas, tokens, componentes y guías, esa convención dejó de parecer un atajo. Se convirtió en vocabulario. Después otros frameworks y sistemas de diseño heredaron la estructura con variaciones menores. Algunos cambiaron nombres. Otros añadieron tonos intermedios. Otros introdujeron primary, secondary, neutral, destructive.
Pero el núcleo siguió siendo el mismo:
positivo
precaución
negativo grave
informativo
neutral
Ese núcleo parecía suficiente mientras el intent solo modificaba color.
Si todo lo que hace danger es pintar un borde en rojo, quizá no necesitamos mucha más teoría. Si todo lo que hace success es poner verde un mensaje, quizá la convención basta.
Pero en una interfaz perceptiva multicanal, el intent ya no cambia solo color. Puede modular:
motion
sonido
duración
ritmo
intensidad
persistencia
profundidad
forma
háptica
silencio
En ese momento, la convención heredada empieza a mostrar sus límites.
Porque una cosa es elegir un color.
Otra muy distinta es decidir cómo debe moverse, sonar, durar, aparecer, persistir o sentirse un evento.
## 4. Qué codifica realmente la convención heredada
Si examinamos con atención success, warning, danger e info, vemos que no forman una taxonomía afectiva completa.
Forman una taxonomía cromática útil.
Eso no las vuelve inútiles. Las vuelve insuficientes.
Danger codifica una región negativa intensa, pero mezcla al menos tres cosas distintas:
amenaza activa
fallo grave
pérdida consumada
Una amenaza activa todavía permite actuar. Una pérdida consumada ya ocurrió. Un fallo grave puede bloquear una operación, pero no siempre equivale a destrucción. Tratar todo eso como danger produce una interfaz afectivamente torpe.
Success codifica una región positiva, pero mezcla también cosas distintas:
confirmación suave
resolución positiva
culminación de una tensión
estado correcto rutinario
Un autoguardado correcto no merece la misma energía perceptiva que completar una subida larga o enviar un formulario importante. Si todo es success, nada se siente realmente cumplido.
Warning codifica precaución, pero no distingue bien entre riesgo corregible, advertencia moderada y amenaza próxima. Algunas advertencias piden revisión tranquila; otras exigen atención inmediata. No son el mismo evento afectivo.
Info es el caso más problemático. Muchas veces no expresa evaluación afectiva. Expresa simplemente que el sistema quiere que algo se note. No responde a:
¿cómo se evalúa esto?
Responde a:
¿debo atender esto?
Eso lo coloca más cerca de signal que del sistema de intents. En esta gramática, muchos usos de info se expresan mejor como:
signal.announce + neutral
signal.inform + neutral
signal.notify + neutral
La familia signal aporta la saliencia.
El intent neutral indica ausencia de juicio fuerte.
Esta es la diferencia central:
> La convención heredada codifica colores útiles.
> La teoría del intent debe codificar regiones evaluativas del evento.
Por eso necesitamos separar:
threat ≠ loss
affirm ≠ fulfill
info ≠ intent
La interfaz puede distinguir esas diferencias. El usuario puede sentir esas diferencias. La gramática también debe poder nombrarlas.
SECCIÓN 6 AMPLIADA: RUSSELL Y ESPACIO AFECTIVO
----------------------------------------------
## El espacio afectivo
El sistema de intents no se deriva de colores heredados, sino de la evaluación del evento dentro de un espacio afectivo.
Este capítulo se apoya en un modelo sencillo: el espacio valencia/activación descrito por James A. Russell. No se utiliza como teoría completa de la emoción, sino como base operativa para distinguir tipos de evaluación que la interfaz necesita expresar.
Los seis intents pueden entenderse como regiones discretas de ese espacio aplicadas al evento interactivo.
La valencia distingue lo positivo de lo negativo.
La activación distingue baja y alta movilización atencional o corporal.
No es lo mismo threat que loss. Ambos son negativos, pero uno tiene alta activación y convoca acción; el otro registra una consecuencia consumada con activación más baja.
Tampoco es lo mismo affirm que fulfill. Ambos son positivos, pero uno confirma suavemente; el otro resuelve una tensión u objetivo.
El espacio afectivo no nos dice qué color usar. Nos dice qué diferencia evaluativa debe preservar la interfaz.
LÍNEAS POR INTENT
-----------------
Incluir debajo de cada intent:
Neutral:
Neutral ocupa la región de baja activación sin valencia fuerte: el evento no requiere evaluación afectiva significativa.
Affirm:
Affirm ocupa la región de valencia positiva con baja activación: confirma sin alterar significativamente el foco atencional ni la energía del sistema.
Fulfill:
Fulfill corresponde a una región positiva con mayor activación: marca la resolución de una tensión o la culminación de una acción relevante.
Risk:
Risk se sitúa en una región negativa de activación media: indica un problema corregible que requiere atención, pero no una reacción inmediata.
Threat:
Threat pertenece a la región de alta activación negativa: exige atención urgente y posible acción inmediata.
Loss:
Loss comparte valencia negativa con threat, pero con menor activación y distinta temporalidad: registra una consecuencia ya consumada.
FRASE OBLIGATORIA AL FINAL DE LA SECCIÓN DE INTENTS
---------------------------------------------------
Los intents no son colores ni estilos. Son regiones evaluativas del evento que pueden expresarse mediante distintos canales perceptivos.
FIGURA OBLIGATORIA
------------------
## Figura — Del evento al intent
Brief:
evento evaluable
→ appraisal / evaluación
→ valencia + activación
→ intent
→ canales perceptivos
Ejemplo:
commit.delete
→ negativo + baja activación + consecuencia consumada
→ loss
→ retirada + huella + color desaturado + posible sonido grave
CIERRE DE PARTE I REVISADO
==========================
Con este capítulo se cierra la Parte I.
La interfaz ya no ha sido definida solo como superficie, sino como dimensión perceptible de un contexto operacional. El evento interactivo ha quedado establecido como unidad mínima de significado. La gramática del evento ha sido presentada como sistema de diferencias, relaciones y reglas de composición. La interacción semántica ha sido definida como configuración perceptiva recurrente. Las familias han sido reducidas a siete preguntas fundamentales. Los eventos evaluables se han separado de los estructurales. Y el intent ha dejado de ser color para convertirse en región evaluativa del evento.
La Parte II podrá entrar ahora en los canales:
tiempo
motion
presencia
profundidad
forma
color
sonido
háptica
accesibilidad
coordinación
Pero esos canales ya no serán efectos. Serán formas de hacer sensible una gramática.
RESUMEN EJECUTIVO DE CORRECCIONES
=================================
1. La Introducción debe aclarar que la web es campo de ejemplos, no límite de la teoría.
2. Capítulo 1 debe preparar el paso hacia fundamentos sin sobrecargarse.
3. Capítulo 2 debe anunciar fundamentos, no desarrollarlos.
4. Capítulo 3 debe ser el mapa completo de fundamentos psicoperceptivos.
5. Capítulo 4 debe conectar evento con percepción, affordance, atención, appraisal y multisensorialidad.
6. Capítulo 5 debe fundamentar gramática en diferenciación perceptiva y reducción de carga cognitiva.
7. Capítulo 6 debe mostrar que interacción semántica no pertenece a un canal único.
8. Capítulo 7 debe relacionar cada criterio con su base psicoperceptiva.
9. Capítulo 8 debe mostrar que las familias son categorías perceptivas.
10. Capítulo 9 debe aplicar fundamentos a mensaje/marco, no introducirlos desde cero.
11. Capítulo 10 debe recuperar la fuerza de “Más allá del semáforo” y anclar claramente Russell.
12. Todo el bloque debe sostener esta idea:
Este libro no inventa categorías; nombra estructuras perceptivas recurrentes.
FIN DEL DOCUMENTO

@ -98,7 +98,7 @@ errors are typed.
- Runtime code must not throw inline string/template errors outside tests or
vendored code.
- Public programmer errors use artifact-specific classes and guards:
`SessDisposedError`, `ConnInvalidNameError`, `UnitsUnknownUnitError`, etc.
`SessionDisposedError`, `ConnInvalidNameError`, `UnitsUnknownUnitError`, etc.
- Expected runtime failures should be returned as tagged data/results when the
artifact already has such a contract (`http`, `conn`, `perm`, `cach`).
- Validation failures are data (`SiumValidationError.issues`) and diagnostics
@ -121,7 +121,7 @@ errors are typed.
| [`conn`](./conn/README.md) | `EngineConnections`, `ActiveConnections` | Realtime connection registry: transports, reconnect, heartbeat, request/reply, channels, session bridge | `$timr`, `$logr` (optional), `$sess` bridge (optional) |
| [`auth`](./auth/README.md) | `ActiveAuth` (`EngineAuth` in `$svrs/auth`) | Authentication: password flows, CSRF, current session reflector, devices, logout, server-authoritative auth handlers | `$libs/auth`, `$http`, `$cach`, `$svrs/auth` |
| [`perm`](./perm/README.md) | `ActivePermissions` (`EnginePermissions` in `$svrs/perm`) | Authorization: policy runtime adapter, HTTP client/handlers, cache snapshot, `<Can />` guard | `$libs/perm`, `$libs/svrs`, `$http`, `$logr` (optional) |
| [`cach`](./cach/README.md) | `ActiveCache` (`EngineCache` in `$svrs/cach`) | Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | `$libs/cach`, `$stor` (adapter), `$logr` (optional) |
| [`cach`](./cach/README.md) | `ActiveCache` (`EngineCache` in `$svrs/cache`) | Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | `$libs/cach`, `$stor` (adapter), `$logr` (optional) |
| [`aapp`](./aapp/README.md) | `ActiveApp` | App composition: wires Logger + Lang + Formats + Frontend + Dom + Storage + Http + Timers + Cache; factories for Sess, Conn, Auth, Perm, Sium | every artifact above |
## Composition
@ -193,7 +193,7 @@ adom ─── fend \ | / | conn
and `cach` through ports rather than owning their state.
- `perm` splits cleanly: `$svrs/perm` owns the authoritative engine/HTTP
handlers, while `$perm` owns the active UI reflector and `<Can />`.
- `cach` splits cleanly: `$svrs/cach` owns the imperative engine, while
- `cach` splits cleanly: `$svrs/cache` owns the imperative engine, while
`$cach` owns the active Svelte wrapper and can consume `stor` through its
storage adapter.
- `adom` depends only on the pure helpers in `libs/dom` and on `libs/reactive`.
@ -216,9 +216,9 @@ alias: {
$aapp: 'src/arts/aapp',
$adom: 'src/arts/adom',
$auth: 'src/arts/auth',
$cach: 'src/arts/cach',
$cache: 'src/arts/cach',
$conn: 'src/arts/conn',
$fend: 'src/arts/fend',
$frontend: 'src/arts/fend',
$fmts: 'src/arts/fmts',
$http: 'src/arts/http',
$lang: 'src/arts/lang',

@ -1,29 +0,0 @@
export { AAPP_MODULE } from '$libs/aapp/consts';
import { AAPP_ERR, AAPP_MODULE } from '$libs/aapp/consts';
import { errCode, type ErrCode } from '$libs/errs';
export const AAPP_LOG_MSG_SESSION_CACHE_CLEAR_FAILED =
'session change cache clear failed';
export const AAPP_CONNECTION_CLOSE_REASON_IDENTITY_CLEARED = 'identity_cleared';
export const AAPP_ORCHESTRATION_STANDARD = 'standard';
export const AAPP_ORCHESTRATION_SILENT = 'silent';
export const AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY = 'identity';
export const AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH = 'permissions-refresh';
export const AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED = 'tenant-switched';
export const AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY = 'connectivity';
export const AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE = 'dispose';
export const AAPP_ERR_ALREADY_CREATED: ErrCode = errCode(AAPP_ERR, 'already_created');
export const AAPP_ERROR_ALREADY_CREATED_SESSION =
`[${AAPP_MODULE}] App.createActiveSession() called twice — only one session per App.`;
export const AAPP_ERROR_ALREADY_CREATED_PERMISSIONS =
`[${AAPP_MODULE}] App.createActivePermissions() called twice — only one permissions client per App.`;
export const AAPP_ERROR_ALREADY_CREATED_AUTH =
`[${AAPP_MODULE}] App.createActiveAuth() called twice — only one active auth client per App.`;
export const AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED =
`[${AAPP_MODULE}] App.createActivePermissions() requires endpoint via options.permissions or argument.`;

@ -1,64 +0,0 @@
import type { AppEventMap, AppUserIdentityChangeCause } from '$libs/aapp/events';
import {
AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER,
AAPP_USER_IDENTITY_CAUSE_SESSION_EXPIRED,
AAPP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED,
AAPP_USER_IDENTITY_CAUSE_SESSION_REFRESHED,
AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged
} from '$libs/aapp/events';
import {
SESS_EVENT_LIFECYCLE_ADOPTED,
SESS_EVENT_LIFECYCLE_ADOPTED_SERVER,
SESS_EVENT_LIFECYCLE_EXPIRED,
SESS_EVENT_LIFECYCLE_EXTERNAL_CHANGED,
SESS_EVENT_LIFECYCLE_REFRESHED,
SESS_EVENT_LIFECYCLE_REVOKED,
SESS_EVENT_CHANGED
} from '$sess/consts';
import type { SessEventMap, SessLifecyclePayload, SessionEvent } from '$sess/types';
import type { EngineBus } from '$libs/buss';
import { AAPP_MODULE } from '../consts.ts';
type SessionTranslatorBusEvents = AppEventMap & SessEventMap;
export function wireSessionTranslator(bus: EngineBus<SessionTranslatorBusEvents>): () => void {
const subscription = bus.on(SESS_EVENT_CHANGED, (event) => {
publishAppIdentityFromSessionLifecycleEvent(bus, event.payload);
});
return () => subscription.unsubscribe();
}
export function publishAppIdentityFromSessionLifecycleEvent(
bus: EngineBus<SessionTranslatorBusEvents>,
payload: SessLifecyclePayload
): void {
const cause = resolveAppIdentityCause(payload.event);
if (cause === undefined) return;
publishAppUserIdentityChanged(
bus,
{
event: payload.event,
generation: payload.generation,
identity: {
from: payload.identity.from,
to: payload.identity.to
},
cause
},
{
source: AAPP_MODULE
}
);
}
function resolveAppIdentityCause(event: SessionEvent): AppUserIdentityChangeCause | undefined {
if (event === SESS_EVENT_LIFECYCLE_ADOPTED) return AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED;
if (event === SESS_EVENT_LIFECYCLE_ADOPTED_SERVER) return AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER;
if (event === SESS_EVENT_LIFECYCLE_REFRESHED) return AAPP_USER_IDENTITY_CAUSE_SESSION_REFRESHED;
if (event === SESS_EVENT_LIFECYCLE_REVOKED) return AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED;
if (event === SESS_EVENT_LIFECYCLE_EXPIRED) return AAPP_USER_IDENTITY_CAUSE_SESSION_EXPIRED;
if (event === SESS_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return AAPP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED;
return undefined;
}

@ -49,7 +49,7 @@ App.dispose();
Server-authoritative engines that have a browser reflector live under
`$svrs/*`: use `$svrs/auth` for `createEngineAuth()` and auth HTTP handlers,
`$svrs/perm` for `createEnginePermissions()` and authorization handlers, and
`$svrs/cach` for `createEngineCache()` in services, repositories or server
`$svrs/cache` for `createEngineCache()` in services, repositories or server
load code. `aapp` composes only the active/client side.
`Sium` is **not** a member of App. Validation is page-scoped — pages with
@ -119,7 +119,7 @@ module event -> aapp translator -> app event -> consumer opt-in reaction
Current identity flow:
```txt
sess emits SESS_EVENT_CHANGED
sess emits SESSION_EVENT_CHANGED
aapp's session translator publishes APP_EVENT_USER_IDENTITY_CHANGED
cach/perm/conn may react only when their own auto*On option opts in
```
@ -136,7 +136,7 @@ Current translator contract:
| Translator | Current source | Publishes |
| --- | --- | --- |
| `identity` | built-in: `SESS_EVENT_CHANGED` from `App.createActiveSession(...)` | `APP_EVENT_USER_IDENTITY_CHANGED` |
| `identity` | built-in: `SESSION_EVENT_CHANGED` from `App.createActiveSession(...)` | `APP_EVENT_USER_IDENTITY_CHANGED` |
| `dispose` | built-in: `App.dispose()` | `APP_EVENT_DISPOSE_STARTING` |
| `permissions-refresh` | typed public contract / explicit publish point | `APP_EVENT_PERMISSIONS_REFRESH_REQUESTED` |
| `tenant-switched` | typed public contract / explicit publish point | `APP_EVENT_TENANT_SWITCHED` |
@ -563,7 +563,7 @@ interface ActiveAppOptions<S extends LangNode> {
cache?: Omit<ActiveCacheOptions, 'logger' | 'bus'>;
connections?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session' | 'bus'>;
permissions?: Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>;
auth?: Omit<ActiveAuthOptions, 'http' | 'cach' | 'logger'>;
auth?: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'>;
orchestration?: 'standard' | 'silent' | false | readonly ActiveAppOrchestrationTranslator[];
}
@ -593,7 +593,7 @@ interface ActiveApp<S extends LangNode = LangNode> {
createActivePermissions(
options?: Partial<Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>>
): ActivePermissions;
createActiveAuth(options?: Omit<ActiveAuthOptions, 'http' | 'cach' | 'logger'>): ActiveAuth;
createActiveAuth(options?: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'>): ActiveAuth;
readonly Sess: ActiveSession<unknown, unknown, unknown> | undefined;
readonly Permissions: ActivePermissions | undefined;

@ -2,27 +2,27 @@ import { createActiveDom } from '$adom/active-dom.svelte';
import { createActiveAuth } from '$auth/active-auth.svelte';
import { createEngineHttpAuthClient } from '$auth/client';
import type { ActiveAuth, ActiveAuthOptions } from '$auth/types';
import { createSvelteEngineBus } from '$buss';
import { createActiveCache } from '$cach/active-cache.svelte';
import { createActiveConnections as createActiveConnectionsRegistry } from '$conn/active-connections.svelte';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$conn/types';
import { createActiveFrontend } from '$fend/active-frontend.svelte';
import { createActiveFormats } from '$fmts/active-formats.svelte';
import { createSvelteEngineBus } from '$bus';
import { createActiveCache } from '$cache/active-cache.svelte';
import { createActiveConnections as createActiveConnectionsRegistry } from '$connection/active-connections.svelte';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection/types';
import { createActiveFrontend } from '$frontend/active-frontend.svelte';
import { createActiveFormats } from '$formats/active-formats.svelte';
import { createEngineHttp } from '$http/engine-http';
import type { ActiveLang, LangNode } from '$libs/lang';
import { createActiveLang } from '$lang/active-lang.svelte';
import { createActiveMonoLang } from '$lang/mono-lang.svelte';
import { createEngineLogger } from '$logr/engine-logger';
import { createActivePermissions as createActivePermissionsClient } from '$perm/active-permissions.svelte';
import { PermInvalidEndpointError } from '$perm/errors';
import type { ActivePermissions, ActivePermissionsOptions } from '$perm/types';
import { createActiveSession } from '$sess/active-session.svelte';
import { SessAlreadyCreatedError } from '$sess/errors';
import type { ActiveSession, EngineSessionOptions } from '$sess/types';
import { createEngineLogger } from '$logger/engine-logger';
import { createActivePermissions as createActivePermissionsClient } from '$permissions/active-permissions.svelte';
import { PermInvalidEndpointError } from '$permissions/errors';
import type { ActivePermissions, ActivePermissionsOptions } from '$permissions/types';
import { createActiveSession } from '$session/active-session.svelte';
import { SessionAlreadyCreatedError } from '$session/errors';
import type { ActiveSession, EngineSessionOptions } from '$session/types';
import { createEngineSium } from '$sium/engine-sium';
import { createActiveStorage } from '$stor/active-storage.svelte';
import { createActiveTimers } from '$timr/active-timers.svelte';
import { publishAppDisposeStarting } from '$libs/aapp/events';
import { createActiveStorage } from '$storage/active-storage.svelte';
import { createActiveTimers } from '$timer/active-timers.svelte';
import { publishAppDisposeStarting } from '$libs/active-app/events';
import {
applyFrontendPreferenceSnapshot,
@ -32,18 +32,18 @@ import {
import { createAuthCacheInvalidator } from './integrations/auth-cache';
import { wireSessionTranslator } from './integrations/session-translator';
import {
AAPP_ERROR_ALREADY_CREATED_PERMISSIONS,
AAPP_ERROR_ALREADY_CREATED_AUTH,
AAPP_ERROR_ALREADY_CREATED_SESSION,
AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED,
AAPP_ORCHESTRATION_SILENT,
AAPP_ORCHESTRATION_STANDARD,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE,
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
AAPP_MODULE
APP_ERROR_ALREADY_CREATED_PERMISSIONS,
APP_ERROR_ALREADY_CREATED_AUTH,
APP_ERROR_ALREADY_CREATED_SESSION,
APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED,
APP_ORCHESTRATION_SILENT,
APP_ORCHESTRATION_STANDARD,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
APP_MODULE
} from './consts.ts';
import { AappAlreadyCreatedError } from './errors.ts';
import type {
@ -121,7 +121,7 @@ export function createActiveApp<S extends LangNode = LangNode>(
clock: Timers.clock
});
const orchestration = resolveActiveAppOrchestration(options.orchestration);
const detachSessionTranslator = orchestration.has(AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY)
const detachSessionTranslator = orchestration.has(APP_ORCHESTRATION_TRANSLATOR_IDENTITY)
? wireSessionTranslator(Bus)
: undefined;
@ -178,7 +178,7 @@ export function createActiveApp<S extends LangNode = LangNode>(
sessOptions: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger' | 'bus'> = {}
): ActiveSession<TUser, TCredential, TData> {
if (Sess !== undefined) {
throw new SessAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_SESSION);
throw new SessionAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_SESSION);
}
const built = createActiveSession<TUser, TCredential, TData>({
...sessOptions,
@ -210,14 +210,14 @@ export function createActiveApp<S extends LangNode = LangNode>(
permissionOptions: Partial<Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>> = {}
): ActivePermissions {
if (Permissions !== undefined) {
throw new AappAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_PERMISSIONS);
throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_PERMISSIONS);
}
const merged = {
...options.permissions,
...permissionOptions
};
if (!merged.endpoint) {
throw new PermInvalidEndpointError(AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED);
throw new PermInvalidEndpointError(APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED);
}
const built = createActivePermissionsClient({
...merged,
@ -231,17 +231,17 @@ export function createActiveApp<S extends LangNode = LangNode>(
},
createActiveAuth(
authOptions: Omit<ActiveAuthOptions, 'http' | 'cach' | 'logger'> = {}
authOptions: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'> = {}
): ActiveAuth {
if (Auth !== undefined) {
throw new AappAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_AUTH);
throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_AUTH);
}
const built = createActiveAuth({
...options.auth,
...authOptions,
logger: Logger,
http: createEngineHttpAuthClient(Http),
cach: createAuthCacheInvalidator({
cache: createAuthCacheInvalidator({
cache: Cache,
permissions: () => Permissions
})
@ -253,8 +253,8 @@ export function createActiveApp<S extends LangNode = LangNode>(
dispose() {
if (disposed) return;
disposed = true;
if (orchestration.has(AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE)) {
publishAppDisposeStarting(Bus, { cause: AAPP_MODULE });
if (orchestration.has(APP_ORCHESTRATION_TRANSLATOR_DISPOSE)) {
publishAppDisposeStarting(Bus, { cause: APP_MODULE });
}
Auth?.dispose();
Auth = undefined;
@ -284,19 +284,19 @@ export function createActiveApp<S extends LangNode = LangNode>(
function resolveActiveAppOrchestration(
orchestration: ActiveAppOrchestrationOptions | undefined
): ReadonlySet<ActiveAppOrchestrationTranslator> {
if (orchestration === false || orchestration === AAPP_ORCHESTRATION_SILENT) {
if (orchestration === false || orchestration === APP_ORCHESTRATION_SILENT) {
return new Set();
}
if (orchestration === undefined || orchestration === AAPP_ORCHESTRATION_STANDARD) {
if (orchestration === undefined || orchestration === APP_ORCHESTRATION_STANDARD) {
return new Set(STANDARD_ORCHESTRATION_TRANSLATORS);
}
return new Set(orchestration);
}
const STANDARD_ORCHESTRATION_TRANSLATORS = [
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE
] as const;

@ -0,0 +1,29 @@
export { APP_MODULE } from '$libs/active-app/consts';
import { APP_ERR, APP_MODULE } from '$libs/active-app/consts';
import { errCode, type ErrCode } from '$libs/errs';
export const APP_LOG_MSG_SESSION_CACHE_CLEAR_FAILED =
'session change cache clear failed';
export const APP_CONNECTION_CLOSE_REASON_IDENTITY_CLEARED = 'identity_cleared';
export const APP_ORCHESTRATION_STANDARD = 'standard';
export const APP_ORCHESTRATION_SILENT = 'silent';
export const APP_ORCHESTRATION_TRANSLATOR_IDENTITY = 'identity';
export const APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH = 'permissions-refresh';
export const APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED = 'tenant-switched';
export const APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY = 'connectivity';
export const APP_ORCHESTRATION_TRANSLATOR_DISPOSE = 'dispose';
export const APP_ERR_ALREADY_CREATED: ErrCode = errCode(APP_ERR, 'already_created');
export const APP_ERROR_ALREADY_CREATED_SESSION =
`[${APP_MODULE}] App.createActiveSession() called twice — only one session per App.`;
export const APP_ERROR_ALREADY_CREATED_PERMISSIONS =
`[${APP_MODULE}] App.createActivePermissions() called twice — only one permissions client per App.`;
export const APP_ERROR_ALREADY_CREATED_AUTH =
`[${APP_MODULE}] App.createActiveAuth() called twice — only one active auth client per App.`;
export const APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED =
`[${APP_MODULE}] App.createActivePermissions() requires endpoint via options.permissions or argument.`;

@ -1,9 +1,9 @@
import { CodeError } from '$libs/errs';
import { AAPP_ERR_ALREADY_CREATED } from './consts.ts';
import { APP_ERR_ALREADY_CREATED } from './consts.ts';
export class AappAlreadyCreatedError extends CodeError {
constructor(message: string) {
super(AAPP_ERR_ALREADY_CREATED, { message });
super(APP_ERR_ALREADY_CREATED, { message });
}
}

@ -1,13 +1,13 @@
export { createActiveApp } from './active-app.svelte';
export {
AAPP_ORCHESTRATION_SILENT,
AAPP_ORCHESTRATION_STANDARD,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE,
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
AAPP_MODULE
APP_ORCHESTRATION_SILENT,
APP_ORCHESTRATION_STANDARD,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
APP_MODULE
} from './consts';
export { AappAlreadyCreatedError, isAappAlreadyCreatedError } from './errors';
export type {
@ -23,18 +23,18 @@ export type {
FrontendPersistKeyOverride
} from './types';
// `createTestApp` lives at `$aapp/testing` so it does not ship with bundles
// `createTestApp` lives at `$active-app/testing` so it does not ship with bundles
// that import the production barrel. See `aapp/testing/index.ts`.
// Common storage adapters re-exported for ergonomic single import.
// `createMemoryAdapter` lives in `$stor` only — testing-specific.
export { localAdapter, sessionAdapter, cookieAdapter, withBroadcast } from '$stor';
// `createMemoryAdapter` lives in `$storage` only — testing-specific.
export { localAdapter, sessionAdapter, cookieAdapter, withBroadcast } from '$storage';
export type {
CookieAdapterOptions,
ServerCookiesLike,
BroadcastAdapter,
BroadcastOptions
} from '$stor';
} from '$storage';
// `createActiveMonoLang` is intentionally NOT re-exported — callers that
// compose without `createActiveApp` should import it directly from

@ -1,12 +1,12 @@
import { CACH_SCOPE_PUBLIC } from '$libs/cach';
import type { AuthClientCachPort } from '$libs/auth/contracts';
import type { ActiveCache } from '$cach';
import type { ActivePermissions } from '$perm';
import { CACHE_SCOPE_PUBLIC } from '$libs/cache';
import type { AuthClientCachePort } from '$libs/auth/contracts';
import type { ActiveCache } from '$cache';
import type { ActivePermissions } from '$permissions';
export function createAuthCacheInvalidator(input: {
readonly cache: ActiveCache;
readonly permissions: () => ActivePermissions | undefined;
}): AuthClientCachPort {
}): AuthClientCachePort {
return {
async invalidate(event) {
input.permissions()?.invalidate();
@ -14,7 +14,7 @@ export function createAuthCacheInvalidator(input: {
event.tags.map((tag) =>
input.cache.invalidate({
tag,
scope: CACH_SCOPE_PUBLIC
scope: CACHE_SCOPE_PUBLIC
})
)
);

@ -8,8 +8,8 @@ import {
type FrontendPreferenceKey,
type FrontendPreferenceSnapshot,
type FrontendPreferenceValue
} from '$fend';
import type { ActiveStorage, ActiveStorageEntry, SyncStorageAdapter } from '$stor';
} from '$frontend';
import type { ActiveStorage, ActiveStorageEntry, SyncStorageAdapter } from '$storage';
import type { FrontendPersist, FrontendPersistKeyOverride } from '../types';

@ -0,0 +1,64 @@
import type { AppEventMap, AppUserIdentityChangeCause } from '$libs/active-app/events';
import {
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER,
APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED,
APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged
} from '$libs/active-app/events';
import {
SESSION_EVENT_LIFECYCLE_ADOPTED,
SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER,
SESSION_EVENT_LIFECYCLE_EXPIRED,
SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED,
SESSION_EVENT_LIFECYCLE_REFRESHED,
SESSION_EVENT_LIFECYCLE_REVOKED,
SESSION_EVENT_CHANGED
} from '$session/consts';
import type { SessEventMap, SessLifecyclePayload, SessionEvent } from '$session/types';
import type { EngineBus } from '$libs/bus';
import { APP_MODULE } from '../consts.ts';
type SessionTranslatorBusEvents = AppEventMap & SessEventMap;
export function wireSessionTranslator(bus: EngineBus<SessionTranslatorBusEvents>): () => void {
const subscription = bus.on(SESSION_EVENT_CHANGED, (event) => {
publishAppIdentityFromSessionLifecycleEvent(bus, event.payload);
});
return () => subscription.unsubscribe();
}
export function publishAppIdentityFromSessionLifecycleEvent(
bus: EngineBus<SessionTranslatorBusEvents>,
payload: SessLifecyclePayload
): void {
const cause = resolveAppIdentityCause(payload.event);
if (cause === undefined) return;
publishAppUserIdentityChanged(
bus,
{
event: payload.event,
generation: payload.generation,
identity: {
from: payload.identity.from,
to: payload.identity.to
},
cause
},
{
source: APP_MODULE
}
);
}
function resolveAppIdentityCause(event: SessionEvent): AppUserIdentityChangeCause | undefined {
if (event === SESSION_EVENT_LIFECYCLE_ADOPTED) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED;
if (event === SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER;
if (event === SESSION_EVENT_LIFECYCLE_REFRESHED) return APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED;
if (event === SESSION_EVENT_LIFECYCLE_REVOKED) return APP_USER_IDENTITY_CAUSE_SESSION_REVOKED;
if (event === SESSION_EVENT_LIFECYCLE_EXPIRED) return APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED;
if (event === SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED;
return undefined;
}

@ -16,22 +16,22 @@
import { describe, it, expect, vi } from 'vitest';
import { createActiveApp } from '../active-app.svelte';
import { LogLevel, type LogEntry } from '$logr';
import { LogLevel, type LogEntry } from '$logger';
import type { LangNode } from '$lang';
import { LANG_MONO_LANG_CATEGORY } from '$lang/mono-lang.svelte';
import {
AAPP_EVENT_DISPOSE_STARTING,
AAPP_EVENT_USER_IDENTITY_CHANGED,
AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
APP_EVENT_DISPOSE_STARTING,
APP_EVENT_USER_IDENTITY_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged
} from '$libs/aapp/events';
import { CONN_STATE_CLOSED, CONN_STATE_OPEN, createMockTransport } from '$conn';
} from '$libs/active-app/events';
import { CONNECTION_STATE_CLOSED, CONNECTION_STATE_OPEN, createMockTransport } from '$connection';
import {
CACH_EVENT_INVALIDATE,
CACH_POLICY_INTERACTIVE,
CACH_SCOPE_PUBLIC,
CACHE_EVENT_INVALIDATE,
CACHE_POLICY_INTERACTIVE,
CACHE_SCOPE_PUBLIC,
memoryCacheAdapter
} from '$libs/cach';
} from '$libs/cache';
import {
AUTH_AAL,
AUTH_CACHE_TAGS,
@ -39,9 +39,9 @@ import {
AUTH_ROUTE_PATHS,
AUTH_SESSION_STATUSES
} from '$libs/auth';
import { PERM_EFFECT_ALLOW } from '$libs/perm';
import { PERM_AUTO_INVALIDATE_STANDARD } from '$perm';
import { SESS_EVENT_LIFECYCLE_REVOKED } from '$sess/consts';
import { PERMISSION_EFFECT_ALLOW } from '$libs/permissions';
import { PERMISSION_AUTO_INVALIDATE_STANDARD } from '$permissions';
import { SESSION_EVENT_LIFECYCLE_REVOKED } from '$session/consts';
const schema = {
greeting: { es: 'Hola', en: 'Hello', 'es-MX': 'Qué onda' },
@ -626,20 +626,20 @@ describe('createActiveApp — composition', () => {
});
const Permissions = App.createActivePermissions({
endpoint: 'https://active.test/permissions',
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
Permissions.hydrate({
decisions: {
'post.read:p1': { effect: PERM_EFFECT_ALLOW, policy: 'test.allow' }
'post.read:p1': { effect: PERMISSION_EFFECT_ALLOW, policy: 'test.allow' }
}
});
let sizeSeenDuringDispose = -1;
App.Bus.on(AAPP_EVENT_DISPOSE_STARTING, () => {
App.Bus.on(APP_EVENT_DISPOSE_STARTING, () => {
publishAppUserIdentityChanged(App.Bus, {
event: SESS_EVENT_LIFECYCLE_REVOKED,
event: SESSION_EVENT_LIFECYCLE_REVOKED,
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
sizeSeenDuringDispose = Permissions.size;
});
@ -657,7 +657,7 @@ describe('createActiveApp — composition', () => {
logger: { level: LogLevel.NONE, transports: [] }
});
let disposeStartingCount = 0;
App.Bus.on(AAPP_EVENT_DISPOSE_STARTING, () => {
App.Bus.on(APP_EVENT_DISPOSE_STARTING, () => {
disposeStartingCount += 1;
});
const Connections = App.createActiveConnections();
@ -666,18 +666,18 @@ describe('createActiveApp — composition', () => {
heartbeat: false,
reconnect: false
});
const handle = App.Timers.schedule('aapp:dispose:late-task', 1_000, timerTask);
const handle = App.Timers.schedule('app:dispose:late-task', 1_000, timerTask);
await Main.connect();
expect(Main.state).toBe(CONN_STATE_OPEN);
expect(App.Timers.has('aapp:dispose:late-task')).toBe(true);
expect(Main.state).toBe(CONNECTION_STATE_OPEN);
expect(App.Timers.has('app:dispose:late-task')).toBe(true);
App.dispose();
App.dispose();
vi.advanceTimersByTime(1_000);
expect(disposeStartingCount).toBe(1);
expect(Main.state).toBe(CONN_STATE_CLOSED);
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
expect(Connections.size).toBe(0);
expect(App.Timers.disposed).toBe(true);
expect(handle.active).toBe(false);
@ -685,10 +685,10 @@ describe('createActiveApp — composition', () => {
expect(App.Cache.disposed).toBe(true);
expect(() =>
publishAppUserIdentityChanged(App.Bus, {
event: SESS_EVENT_LIFECYCLE_REVOKED,
event: SESSION_EVENT_LIFECYCLE_REVOKED,
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
})
).toThrow();
} finally {
@ -712,16 +712,16 @@ describe('createActiveApp — composition', () => {
});
const Permissions = App.createActivePermissions({
endpoint: 'https://active.test/permissions',
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
App.Bus.on(AAPP_EVENT_DISPOSE_STARTING, () => {
App.Bus.on(APP_EVENT_DISPOSE_STARTING, () => {
void Promise.resolve().then(() => {
try {
publishAppUserIdentityChanged(App.Bus, {
event: SESS_EVENT_LIFECYCLE_REVOKED,
event: SESSION_EVENT_LIFECYCLE_REVOKED,
generation: 2,
identity: { from: 'identified', to: 'none' },
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
} catch (error) {
deferredPublishError = error;
@ -736,18 +736,18 @@ describe('createActiveApp — composition', () => {
});
const pendingCache = App.Cache.query({
key: ['dispose', 'pending'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
fetcher: async () => cacheReply.promise
});
await Promise.resolve();
App.dispose();
permissionReply.resolve({ effect: PERM_EFFECT_ALLOW, policy: 'test.allow' });
permissionReply.resolve({ effect: PERMISSION_EFFECT_ALLOW, policy: 'test.allow' });
cacheReply.resolve({ actorId: 'actor-ada' });
await expect(pendingPermission).resolves.toMatchObject({
effect: PERM_EFFECT_ALLOW
effect: PERMISSION_EFFECT_ALLOW
});
await expect(pendingCache).resolves.toEqual({ actorId: 'actor-ada' });
await Promise.resolve();
@ -782,7 +782,7 @@ describe('createActiveApp — composition', () => {
logger: { level: LogLevel.NONE, transports: [] }
});
const appIdentityEvents: unknown[] = [];
const off = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const off = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
appIdentityEvents.push(event.payload);
});
const Connections = App.createActiveConnections();
@ -803,7 +803,7 @@ describe('createActiveApp — composition', () => {
await Session.revoke();
expect(appIdentityEvents).toHaveLength(2);
expect(Main.state).toBe(CONN_STATE_OPEN);
expect(Main.state).toBe(CONNECTION_STATE_OPEN);
off.unsubscribe();
App.dispose();
@ -850,7 +850,7 @@ describe('createActiveApp — composition', () => {
});
}
if (path === '/permissions/check') {
return json({ effect: PERM_EFFECT_ALLOW, policy: 'remote.allow' });
return json({ effect: PERMISSION_EFFECT_ALLOW, policy: 'remote.allow' });
}
return new Response(null, { status: 404 });
}) as typeof fetch;
@ -859,7 +859,7 @@ describe('createActiveApp — composition', () => {
http: { baseUrl: 'https://active.test', fetch: fetcher },
cache: {
onEvent(event) {
if (event.type === CACH_EVENT_INVALIDATE) invalidatedTags.push(...(event.tags ?? []));
if (event.type === CACHE_EVENT_INVALIDATE) invalidatedTags.push(...(event.tags ?? []));
}
}
});

@ -11,7 +11,7 @@ import { describe, it, expect } from 'vitest';
import { createTestApp } from '../testing';
import { object, pipe, string, email, min, meta } from '$sium/core';
import { siumLangs } from '$sium';
import { LogLevel } from '$logr';
import { LogLevel } from '$logger';
import type { LangNode } from '$lang';
const langSchema = {

@ -1,28 +1,28 @@
import { describe, expect, it } from 'vitest';
import { createActiveApp } from '../active-app.svelte';
import {
CACH_EVENT_ALL,
CACH_EVENT_INVALIDATE,
CACH_AUTO_INVALIDATE_STANDARD,
CACH_POLICY_INTERACTIVE,
CACH_READ_MODE_STALE_WHILE_REVALIDATE,
CACH_SCOPE_PUBLIC,
CACH_SCOPE_TENANT,
CACHE_EVENT_ALL,
CACHE_EVENT_INVALIDATE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_POLICY_INTERACTIVE,
CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
CACHE_SCOPE_PUBLIC,
CACHE_SCOPE_TENANT,
memoryCacheAdapter,
type CacheEvent,
type ResolvedScopeValues
} from '$cach';
import { SESS_EVENT_CHANGED, type SessLifecyclePayload } from '$sess';
} from '$cache';
import { SESSION_EVENT_CHANGED, type SessLifecyclePayload } from '$session';
import {
CONN_FRAME_TYPE_AUTH,
CONN_STATE_CLOSED,
CONN_STATE_OPEN,
CONN_AUTO_REAUTH_STANDARD,
CONNECTION_FRAME_TYPE_AUTH,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_OPEN,
CONNECTION_AUTO_REAUTH_STANDARD,
createFrame,
createMockTransport,
type ConnectionFrame,
type MockConnectionTransport
} from '$conn';
} from '$connection';
import { HTTP_CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE } from '$libs/http';
import type { StandardSchemaV1 } from '$libs/standard-schema';
import {
@ -36,11 +36,11 @@ import {
type AuthSessionId,
type AuthTenantId
} from '$libs/auth';
import { LogLevel, type LogEntry } from '$logr';
import { LogLevel, type LogEntry } from '$logger';
import {
PERM_EFFECT_ALLOW,
PERM_EFFECT_NOT_APPLICABLE,
PERM_AUTO_INVALIDATE_STANDARD,
PERMISSION_EFFECT_ALLOW,
PERMISSION_EFFECT_NOT_APPLICABLE,
PERMISSION_AUTO_INVALIDATE_STANDARD,
actor,
allow,
and,
@ -50,22 +50,22 @@ import {
rel,
type ResourceRef,
type SubjectRef
} from '$perm';
import { createEnginePermissions, createPermissionHttpHandlers } from '$svrs/perm';
import { createMemoryAdapter } from '$stor';
import { SESS_EVENT_LIFECYCLE_ADOPTED } from '$sess/consts';
} from '$permissions';
import { createEnginePermissions, createPermissionHttpHandlers } from '$svrs/permissions';
import { createMemoryAdapter } from '$storage';
import { SESSION_EVENT_LIFECYCLE_ADOPTED } from '$session/consts';
import {
AAPP_EVENT_USER_IDENTITY_CHANGED,
AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
APP_EVENT_USER_IDENTITY_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
onAppPermissionsRefreshRequested,
publishAppPermissionsRefreshRequested,
publishAppTenantSwitched,
publishAppUserIdentityChanged
} from '$libs/aapp/events';
import { SESS_EVENT_LIFECYCLE_REVOKED } from '$sess/consts';
} from '$libs/active-app/events';
import { SESSION_EVENT_LIFECYCLE_REVOKED } from '$session/consts';
const PERM_ENDPOINT = 'https://ecosystem.test/api/permissions';
const PERMISSION_ENDPOINT = 'https://ecosystem.test/api/permissions';
const HTTP_PROJECT_PATH = '/api/demo/project';
const TENANT_ID = 'tenant-acme';
const SECOND_TENANT_ID = 'tenant-umbrella';
@ -78,8 +78,8 @@ const PROJECT_RELATION_MEMBER = 'project.member';
const PROJECT_SCHEMA_VERSION = 'ProjectPayload:v1';
const ROLE_ADMIN = 'admin';
const ROLE_VIEWER = 'viewer';
const TIMR_KEY = 'ecosystem:test';
const CONN_NAME = 'updates';
const TIMER_KEY = 'ecosystem:test';
const CONNECTION_NAME = 'updates';
const CHAT_CONNECTION_NAME = 'chat';
const LOG_CATEGORY = 'test.ecosystem';
const LOG_MESSAGE_BOOT = 'ecosystem.boot';
@ -127,7 +127,7 @@ interface DemoSessionData {
}
describe('ActiveApp — total ecosystem integration', () => {
it('wires auth, sess, perm, cach, http, stor, sium, fmts, fend, adom, timr, conn and logr', async () => {
it('wires auth, sess, perm, cache, http, stor, sium, fmts, fend, adom, timer, conn and logr', async () => {
const entries: LogEntry[] = [];
const authCurrent = createAuthCurrent();
let actorRole = ROLE_ADMIN;
@ -217,18 +217,18 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
}
});
const offCacheEvents = App.Cache.on(CACH_EVENT_ALL, (event) => {
const offCacheEvents = App.Cache.on(CACHE_EVENT_ALL, (event) => {
cacheEvents.push(event);
});
@ -269,7 +269,7 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(Sess.current?.user?.id).toBe(ACTOR_ID);
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${ACTOR_ID}:${actorRole}`
});
const decision = await Permissions.check({
@ -277,7 +277,7 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
});
expect(decision.effect).toBe(PERM_EFFECT_ALLOW);
expect(decision.effect).toBe(PERMISSION_EFFECT_ALLOW);
actorRole = ROLE_VIEWER;
Permissions.invalidate();
@ -286,7 +286,7 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
});
expect(viewerDecision.effect).toBe(PERM_EFFECT_NOT_APPLICABLE);
expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE);
expect(Permissions.size).toBeGreaterThan(0);
actorRole = ROLE_ADMIN;
@ -294,8 +294,8 @@ describe('ActiveApp — total ecosystem integration', () => {
const project = await App.Cache.query<ProjectPayload>({
key: ['project', PROJECT_ID],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'project', id: PROJECT_ID }],
fetcher: async () => {
@ -308,8 +308,8 @@ describe('ActiveApp — total ecosystem integration', () => {
});
const cached = await App.Cache.query<ProjectPayload>({
key: ['project', PROJECT_ID],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'project', id: PROJECT_ID }],
fetcher: async () => {
@ -323,8 +323,8 @@ describe('ActiveApp — total ecosystem integration', () => {
App.setLocale('es');
const localizedProject = await App.Cache.query<ProjectPayload>({
key: ['project', PROJECT_ID],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'project', id: PROJECT_ID }],
fetcher: async () => {
@ -339,7 +339,7 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(projectFetches).toBe(2);
let timerRan = false;
App.Timers.schedule(TIMR_KEY, 0, () => {
App.Timers.schedule(TIMER_KEY, 0, () => {
timerRan = true;
});
await new Promise((resolve) => setTimeout(resolve, 0));
@ -347,7 +347,7 @@ describe('ActiveApp — total ecosystem integration', () => {
const Connections = App.createActiveConnections();
const transport = createMockTransport();
const Updates = Connections.createConnection(CONN_NAME, {
const Updates = Connections.createConnection(CONNECTION_NAME, {
transport,
heartbeat: false,
reconnect: false
@ -360,7 +360,7 @@ describe('ActiveApp — total ecosystem integration', () => {
await Auth.signOut();
expect(Auth.authenticated).toBe(false);
expect(Permissions.size).toBe(0);
expect(cacheEvents.some((event) => event.type === CACH_EVENT_INVALIDATE)).toBe(true);
expect(cacheEvents.some((event) => event.type === CACHE_EVENT_INVALIDATE)).toBe(true);
} finally {
offCacheEvents();
App.dispose();
@ -376,10 +376,10 @@ describe('ActiveApp — total ecosystem integration', () => {
try {
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Updates = Connections.createConnection(CONN_NAME, {
const Updates = Connections.createConnection(CONNECTION_NAME, {
transport,
heartbeat: false,
reconnect: false,
@ -387,7 +387,7 @@ describe('ActiveApp — total ecosystem integration', () => {
});
await Updates.connect();
expect(Updates.state).toBe(CONN_STATE_OPEN);
expect(Updates.state).toBe(CONNECTION_STATE_OPEN);
const Sess = App.createActiveSession<DemoUser>();
Sess.adoptServer({
@ -397,7 +397,7 @@ describe('ActiveApp — total ecosystem integration', () => {
});
await Sess.revoke();
expect(Updates.state).toBe(CONN_STATE_CLOSED);
expect(Updates.state).toBe(CONNECTION_STATE_CLOSED);
} finally {
App.dispose();
}
@ -440,7 +440,7 @@ describe('ActiveApp — total ecosystem integration', () => {
retry: { limit: 0 }
},
cache: {
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: TENANT_ID,
actorId: activeActorId,
@ -450,7 +450,7 @@ describe('ActiveApp — total ecosystem integration', () => {
}
});
const appIdentityEvents: unknown[] = [];
const offAppIdentity = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const offAppIdentity = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
appIdentityEvents.push(event.payload);
});
@ -459,12 +459,12 @@ describe('ActiveApp — total ecosystem integration', () => {
storage: { adapter: createMemoryAdapter(), key: 'session' }
});
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => activeActorId,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -483,8 +483,8 @@ describe('ActiveApp — total ecosystem integration', () => {
});
const firstCached = await App.Cache.query({
key: ['silent', 'identity'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
fetcher: async () => {
fetches += 1;
@ -513,12 +513,12 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(appIdentityEvents).toHaveLength(0);
expect(Permissions.size).toBe(1);
expect(Chat.state).toBe(CONN_STATE_OPEN);
expect(Chat.state).toBe(CONNECTION_STATE_OPEN);
await expect(
App.Cache.query({
key: ['silent', 'identity'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
fetcher: async () => {
throw new Error('silent orchestration must not clear cache');
@ -527,21 +527,21 @@ describe('ActiveApp — total ecosystem integration', () => {
).resolves.toEqual(firstCached);
publishAppUserIdentityChanged(App.Bus, {
event: SESS_EVENT_LIFECYCLE_REVOKED,
event: SESSION_EVENT_LIFECYCLE_REVOKED,
generation: 3,
identity: { from: 'identified', to: 'none' },
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
await drainMicrotasks();
expect(appIdentityEvents).toHaveLength(1);
expect(Permissions.size).toBe(0);
expect(Chat.state).toBe(CONN_STATE_CLOSED);
expect(Chat.state).toBe(CONNECTION_STATE_CLOSED);
await expect(
App.Cache.query({
key: ['silent', 'identity'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
fetcher: async () => {
fetches += 1;
@ -611,7 +611,7 @@ describe('ActiveApp — total ecosystem integration', () => {
retry: { limit: 0 }
},
cache: {
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: TENANT_ID,
actorId: activeActorId,
@ -619,23 +619,23 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
}
});
const sessionEvents: SessLifecyclePayload[] = [];
const offSessionEvents = App.Bus.on(SESS_EVENT_CHANGED, (event) => {
const offSessionEvents = App.Bus.on(SESSION_EVENT_CHANGED, (event) => {
sessionEvents.push(event.payload);
});
const appIdentityEvents: unknown[] = [];
const offAppIdentityEvents = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const offAppIdentityEvents = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
appIdentityEvents.push(event.payload);
});
@ -652,18 +652,18 @@ describe('ActiveApp — total ecosystem integration', () => {
});
expect(appIdentityEvents).toHaveLength(1);
expect(appIdentityEvents[0]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_ADOPTED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
event: SESSION_EVENT_LIFECYCLE_ADOPTED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
});
expect(JSON.stringify(appIdentityEvents[0])).not.toContain(TOKEN_ADA);
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${activeActorId}:${activeRole}`,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -689,25 +689,25 @@ describe('ActiveApp — total ecosystem integration', () => {
await waitForSentCount(transport, 1);
const firstAuth = latestFrame(transport);
expect(firstAuth).toMatchObject({
type: CONN_FRAME_TYPE_AUTH,
type: CONNECTION_FRAME_TYPE_AUTH,
payload: { actorId: ACTOR_ID, token: TOKEN_ADA }
});
ackFrame(transport, firstAuth);
await expect(pendingConnect).resolves.toMatchObject({ ok: true });
expect(Chat.state).toBe(CONN_STATE_OPEN);
expect(Chat.state).toBe(CONNECTION_STATE_OPEN);
const adminDecision = await Permissions.check({
action: PROJECT_ACTION_UPDATE,
resource,
context: { risk: { mfa: true } }
});
expect(adminDecision.effect).toBe(PERM_EFFECT_ALLOW);
expect(adminDecision.effect).toBe(PERMISSION_EFFECT_ALLOW);
expect(Permissions.size).toBe(1);
const cachedPresence = await App.Cache.query({
key: ['chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -719,8 +719,8 @@ describe('ActiveApp — total ecosystem integration', () => {
const stalePresence = await App.Cache.query({
key: ['chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -741,26 +741,26 @@ describe('ActiveApp — total ecosystem integration', () => {
await drainMicrotasks();
expect(Permissions.size).toBe(0);
expect(sessionEvents.some((event) => event.event === SESS_EVENT_LIFECYCLE_ADOPTED)).toBe(true);
expect(sessionEvents.some((event) => event.event === SESSION_EVENT_LIFECYCLE_ADOPTED)).toBe(true);
expect(appIdentityEvents).toHaveLength(2);
expect(appIdentityEvents[1]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_ADOPTED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
event: SESSION_EVENT_LIFECYCLE_ADOPTED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
});
expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_ADA);
expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_LINUS);
await waitForSentCount(transport, 2);
const nextAuth = latestFrame(transport);
expect(nextAuth).toMatchObject({
type: CONN_FRAME_TYPE_AUTH,
type: CONNECTION_FRAME_TYPE_AUTH,
payload: { actorId: NEXT_ACTOR_ID, token: TOKEN_LINUS }
});
ackFrame(transport, nextAuth);
const refreshedPresence = await App.Cache.query({
key: ['chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -781,7 +781,7 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
});
expect(viewerDecision.effect).toBe(PERM_EFFECT_NOT_APPLICABLE);
expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE);
expect(Permissions.size).toBe(1);
await Sess.revoke();
@ -789,19 +789,19 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(Sess.current).toBeNull();
expect(Permissions.size).toBe(0);
expect(Chat.state).toBe(CONN_STATE_CLOSED);
expect(Chat.state).toBe(CONNECTION_STATE_CLOSED);
expect(appIdentityEvents).toHaveLength(3);
expect(appIdentityEvents[2]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_REVOKED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
event: SESSION_EVENT_LIFECYCLE_REVOKED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_ADA);
expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_LINUS);
await expect(
App.Cache.query({
key: ['chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -895,7 +895,7 @@ describe('ActiveApp — total ecosystem integration', () => {
retry: { limit: 0 }
},
cache: {
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: TENANT_ID,
actorId: activeActorId,
@ -903,19 +903,19 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
}
});
const appIdentityEvents: unknown[] = [];
const offAppIdentityEvents = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const offAppIdentityEvents = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
appIdentityEvents.push(event.payload);
});
@ -960,12 +960,12 @@ describe('ActiveApp — total ecosystem integration', () => {
});
});
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${activeActorId}:${activeRole}`,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -993,8 +993,8 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(Sess.current?.user?.id).toBe(ACTOR_ID);
expect(appIdentityEvents).toHaveLength(1);
expect(appIdentityEvents[0]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_ADOPTED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
event: SESSION_EVENT_LIFECYCLE_ADOPTED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
});
expect(JSON.stringify(appIdentityEvents[0])).not.toContain(TOKEN_ADA);
@ -1002,7 +1002,7 @@ describe('ActiveApp — total ecosystem integration', () => {
await waitForSentCount(transport, 1);
const firstAuth = latestFrame(transport);
expect(firstAuth).toMatchObject({
type: CONN_FRAME_TYPE_AUTH,
type: CONNECTION_FRAME_TYPE_AUTH,
payload: { actorId: ACTOR_ID, token: TOKEN_ADA }
});
ackFrame(transport, firstAuth);
@ -1014,13 +1014,13 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW });
expect(Permissions.size).toBe(1);
const adaPresence = await App.Cache.query({
key: ['auth', 'chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -1039,15 +1039,15 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(Permissions.size).toBe(0);
expect(appIdentityEvents).toHaveLength(2);
expect(appIdentityEvents[1]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_ADOPTED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
event: SESSION_EVENT_LIFECYCLE_ADOPTED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
});
expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_ADA);
expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_LINUS);
await waitForSentCount(transport, 2);
const nextAuth = latestFrame(transport);
expect(nextAuth).toMatchObject({
type: CONN_FRAME_TYPE_AUTH,
type: CONNECTION_FRAME_TYPE_AUTH,
payload: { actorId: NEXT_ACTOR_ID, token: TOKEN_LINUS }
});
ackFrame(transport, nextAuth);
@ -1055,8 +1055,8 @@ describe('ActiveApp — total ecosystem integration', () => {
await expect(
App.Cache.query({
key: ['auth', 'chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -1071,7 +1071,7 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_NOT_APPLICABLE });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_NOT_APPLICABLE });
expect(Permissions.size).toBe(1);
await Auth.signOut();
@ -1081,19 +1081,19 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(Auth.authenticated).toBe(false);
expect(Sess.current).toBeNull();
expect(Permissions.size).toBe(0);
expect(Chat.state).toBe(CONN_STATE_CLOSED);
expect(Chat.state).toBe(CONNECTION_STATE_CLOSED);
expect(appIdentityEvents).toHaveLength(3);
expect(appIdentityEvents[2]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_REVOKED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
event: SESSION_EVENT_LIFECYCLE_REVOKED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_ADA);
expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_LINUS);
await expect(
App.Cache.query({
key: ['auth', 'chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -1191,7 +1191,7 @@ describe('ActiveApp — total ecosystem integration', () => {
retry: { limit: 0 }
},
cache: {
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: TENANT_ID,
actorId: activeActorId,
@ -1199,12 +1199,12 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
@ -1244,12 +1244,12 @@ describe('ActiveApp — total ecosystem integration', () => {
});
});
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${activeActorId}:${activeRole}`,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -1286,8 +1286,8 @@ describe('ActiveApp — total ecosystem integration', () => {
});
const oldPresence = App.Cache.query({
key: ['race', 'chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => stalePresence.promise
@ -1303,20 +1303,20 @@ describe('ActiveApp — total ecosystem integration', () => {
await waitForSentCount(transport, 2);
const nextAuth = latestFrame(transport);
expect(nextAuth).toMatchObject({
type: CONN_FRAME_TYPE_AUTH,
type: CONNECTION_FRAME_TYPE_AUTH,
payload: { actorId: NEXT_ACTOR_ID, token: TOKEN_LINUS }
});
ackFrame(transport, nextAuth);
stalePermission.resolve({
effect: PERM_EFFECT_ALLOW,
effect: PERMISSION_EFFECT_ALLOW,
reason: 'stale-ada-response',
ttl: 60_000
});
stalePresence.resolve({ actorId: ACTOR_ID, fetch: 1 });
await expect(oldPermission).resolves.toMatchObject({
effect: PERM_EFFECT_ALLOW,
effect: PERMISSION_EFFECT_ALLOW,
reason: 'stale-ada-response'
});
await expect(oldPresence).resolves.toEqual({ actorId: ACTOR_ID, fetch: 1 });
@ -1328,14 +1328,14 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_NOT_APPLICABLE });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_NOT_APPLICABLE });
expect(Permissions.size).toBe(1);
await expect(
App.Cache.query({
key: ['race', 'chat', 'presence'],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }],
fetcher: async () => {
@ -1398,7 +1398,7 @@ describe('ActiveApp — total ecosystem integration', () => {
},
cache: {
adapter: cacheAdapter,
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: activeTenantId,
actorId: activeActorId,
@ -1406,12 +1406,12 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
@ -1430,12 +1430,12 @@ describe('ActiveApp — total ecosystem integration', () => {
expiresAt: Date.now() + 60_000
});
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${activeTenantId}:${activeActorId}:${activeRole}`,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -1464,13 +1464,13 @@ describe('ActiveApp — total ecosystem integration', () => {
resource: resourceForTenant(),
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW });
expect(Permissions.size).toBe(1);
const firstDashboard = await App.Cache.query({
key: ['tenant', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1501,8 +1501,8 @@ describe('ActiveApp — total ecosystem integration', () => {
await expect(
App.Cache.query({
key: ['tenant', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1517,7 +1517,7 @@ describe('ActiveApp — total ecosystem integration', () => {
resource: resourceForTenant(),
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW });
expect(Permissions.size).toBe(1);
activeTenantId = SECOND_TENANT_ID;
@ -1533,8 +1533,8 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(cacheAdapter.inspect().entries).toHaveLength(0);
const secondTenantDashboard = await App.Cache.query({
key: ['tenant', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1557,8 +1557,8 @@ describe('ActiveApp — total ecosystem integration', () => {
const frameCountAfterTenantSwitch = transport.sentMessages().length;
const arabicDashboard = await App.Cache.query({
key: ['tenant', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1639,7 +1639,7 @@ describe('ActiveApp — total ecosystem integration', () => {
},
cache: {
adapter: cacheAdapter,
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: activeTenantId,
actorId: activeActorId,
@ -1647,12 +1647,12 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
@ -1676,12 +1676,12 @@ describe('ActiveApp — total ecosystem integration', () => {
expiresAt: Date.now() + 60_000
});
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${activeTenantId}:${activeActorId}:${activeRole}`,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -1720,13 +1720,13 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW });
expect(Permissions.size).toBe(1);
const cachedDashboard = await App.Cache.query({
key: ['backend-webhook', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1765,7 +1765,7 @@ describe('ActiveApp — total ecosystem integration', () => {
);
await drainMicrotasks();
delayedPermission.resolve({
effect: PERM_EFFECT_ALLOW,
effect: PERMISSION_EFFECT_ALLOW,
reason: 'stale-pre-webhook-permission',
ttl: 60_000
});
@ -1779,8 +1779,8 @@ describe('ActiveApp — total ecosystem integration', () => {
await expect(
App.Cache.query({
key: ['backend-webhook', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1789,7 +1789,7 @@ describe('ActiveApp — total ecosystem integration', () => {
})
).resolves.toEqual(cachedDashboard);
await expect(stalePermission).resolves.toMatchObject({
effect: PERM_EFFECT_ALLOW,
effect: PERMISSION_EFFECT_ALLOW,
reason: 'stale-pre-webhook-permission'
});
expect(Permissions.size).toBe(0);
@ -1800,7 +1800,7 @@ describe('ActiveApp — total ecosystem integration', () => {
resource: delayedResource,
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_NOT_APPLICABLE });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_NOT_APPLICABLE });
expect(Permissions.size).toBe(1);
} finally {
offChatWebhook?.();
@ -1859,7 +1859,7 @@ describe('ActiveApp — total ecosystem integration', () => {
},
cache: {
adapter: cacheAdapter,
autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD,
autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD,
scopeResolver: (): ResolvedScopeValues => ({
tenantId: activeTenantId,
actorId: activeActorId,
@ -1867,18 +1867,18 @@ describe('ActiveApp — total ecosystem integration', () => {
locale: App.getLocale()
}),
policies: {
[CACH_POLICY_INTERACTIVE]: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
}
});
const offAppIdentityEvents = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const offAppIdentityEvents = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
appIdentityEvents.push(event.payload);
});
let offChatWebhook: (() => void) | undefined;
@ -1895,12 +1895,12 @@ describe('ActiveApp — total ecosystem integration', () => {
expiresAt: Date.now() + 60_000
});
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
endpoint: PERMISSION_ENDPOINT,
scopeKey: () => `${activeTenantId}:${activeActorId}:${activeRole}`,
autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD
autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD
});
const Connections = App.createActiveConnections({
autoReauthOn: CONN_AUTO_REAUTH_STANDARD
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD
});
const transport = createMockTransport();
const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, {
@ -1930,12 +1930,12 @@ describe('ActiveApp — total ecosystem integration', () => {
await waitForSentCount(transport, 1);
const firstAuth = latestFrame(transport);
expect(firstAuth).toMatchObject({
type: CONN_FRAME_TYPE_AUTH,
type: CONNECTION_FRAME_TYPE_AUTH,
payload: { actorId: ACTOR_ID, token: TOKEN_ADA }
});
ackFrame(transport, firstAuth);
await expect(pendingConnect).resolves.toMatchObject({ ok: true });
expect(Chat.state).toBe(CONN_STATE_OPEN);
expect(Chat.state).toBe(CONNECTION_STATE_OPEN);
await expect(
Permissions.check({
@ -1943,14 +1943,14 @@ describe('ActiveApp — total ecosystem integration', () => {
resource,
context: { risk: { mfa: true } }
})
).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW });
).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW });
expect(Permissions.size).toBe(1);
await expect(
App.Cache.query({
key: ['remote-revoke', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {
@ -1974,20 +1974,20 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(Sess.current).toBeNull();
expect(Permissions.size).toBe(0);
expect(Chat.state).toBe(CONN_STATE_CLOSED);
expect(Chat.state).toBe(CONNECTION_STATE_CLOSED);
expect(cacheAdapter.inspect().entries).toHaveLength(0);
expect(appIdentityEvents).toHaveLength(2);
expect(appIdentityEvents[1]).toMatchObject({
event: SESS_EVENT_LIFECYCLE_REVOKED,
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
event: SESSION_EVENT_LIFECYCLE_REVOKED,
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_ADA);
expect(transport.sentMessages()).toHaveLength(1);
await expect(
App.Cache.query({
key: ['remote-revoke', 'dashboard'],
scope: CACH_SCOPE_TENANT,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'tenant-dashboard', id: activeTenantId }],
fetcher: async () => {

@ -1,26 +1,26 @@
import { describe, expect, it } from 'vitest';
import { createEngineBus } from '$buss';
import { createEngineBus } from '$bus';
import {
AAPP_EVENT_USER_IDENTITY_CHANGED,
AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
APP_EVENT_USER_IDENTITY_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
type AppEventMap
} from '$libs/aapp/events';
import { SESS_EVENT_LIFECYCLE_ADOPTED } from '$sess/consts';
import type { SessEventMap, SessLifecyclePayload } from '$sess/types';
} from '$libs/active-app/events';
import { SESSION_EVENT_LIFECYCLE_ADOPTED } from '$session/consts';
import type { SessEventMap, SessLifecyclePayload } from '$session/types';
import { publishAppIdentityFromSessionLifecycleEvent } from '../integrations/session-translator.ts';
describe('aapp session translator', () => {
describe('app session translator', () => {
it('maps session lifecycle payloads explicitly without leaking extra sensitive fields', () => {
const Bus = createEngineBus<AppEventMap & SessEventMap>();
const appPayloads: unknown[] = [];
const off = Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const off = Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
appPayloads.push(event.payload);
});
publishAppIdentityFromSessionLifecycleEvent(
Bus,
{
event: SESS_EVENT_LIFECYCLE_ADOPTED,
event: SESSION_EVENT_LIFECYCLE_ADOPTED,
generation: 1,
identity: { from: 'none', to: 'identified' },
credential: { token: 'secret-token' },
@ -31,10 +31,10 @@ describe('aapp session translator', () => {
expect(appPayloads).toHaveLength(1);
expect(JSON.stringify(appPayloads[0])).not.toContain('secret-token');
expect(appPayloads[0]).toEqual({
event: SESS_EVENT_LIFECYCLE_ADOPTED,
event: SESSION_EVENT_LIFECYCLE_ADOPTED,
generation: 1,
identity: { from: 'none', to: 'identified' },
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
});
off.unsubscribe();

@ -12,8 +12,8 @@
import { describe, it, expect } from 'vitest';
import { createActiveApp } from '../active-app.svelte';
import { createMemoryAdapter, encodeEnvelope } from '$stor';
import { LogLevel } from '$logr';
import { createMemoryAdapter, encodeEnvelope } from '$storage';
import { LogLevel } from '$logger';
describe('App.Storage', () => {
it('is always present even without storage options', () => {
@ -61,7 +61,7 @@ describe('App.Storage', () => {
storage: { adapter: broken }
});
App.Storage.entry('x', 'd').set('v');
expect(captured.some((e) => e.category === 'stor' && /write/.test(e.message))).toBe(true);
expect(captured.some((e) => e.category === 'storage' && /write/.test(e.message))).toBe(true);
App.dispose();
});
});

@ -10,7 +10,7 @@
import { describe, it, expect } from 'vitest';
import { createTestApp } from '../testing';
import { LogLevel } from '$logr';
import { LogLevel } from '$logger';
import type { LangNode } from '$lang';
const schema = {

@ -1,4 +1,4 @@
import { LogLevel, type LogEntry, type LoggerOptions, type Transport } from '$logr';
import { LogLevel, type LogEntry, type LoggerOptions, type Transport } from '$logger';
import type { LangNode } from '$libs/lang';
import { createActiveApp } from '../active-app.svelte';
@ -40,8 +40,8 @@ export interface TestAppHandle<S extends LangNode = LangNode> extends ActiveApp<
* `createActiveApp()` with test-friendly defaults — the canonical way to
* build an `App` inside a unit/integration test.
*
* Lives under `$aapp/testing` so the helper does not ship with production
* bundles that import the main `$aapp` barrel.
* Lives under `$active-app/testing` so the helper does not ship with production
* bundles that import the main `$active-app` barrel.
*
* Defaults that differ from production:
*
@ -56,7 +56,7 @@ export interface TestAppHandle<S extends LangNode = LangNode> extends ActiveApp<
*
* @example
* // Smoke test — no logs needed
* import { createTestApp } from '$aapp/testing';
* import { createTestApp } from '$active-app/testing';
* const App = createTestApp({ lang: { schema } });
* expect(App.Lang.t('common.ok')).toBe('Aceptar');
* App.dispose();

@ -1,29 +1,29 @@
import type { ActiveDom, ActiveDomProps } from '$adom';
import type { ActiveAuth, ActiveAuthOptions } from '$auth';
import type { EngineBus, EngineBusOptions } from '$libs/buss';
import type { ActiveCache, ActiveCacheOptions } from '$cach';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$conn';
import type { ActiveFrontend, ActiveFrontendOptions } from '$fend';
import type { FrontendPreferenceKey } from '$fend';
import type { ActiveFormats, ActiveFormatsOptions } from '$fmts';
import type { EngineBus, EngineBusOptions } from '$libs/bus';
import type { ActiveCache, ActiveCacheOptions } from '$cache';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection';
import type { ActiveFrontend, ActiveFrontendOptions } from '$frontend';
import type { FrontendPreferenceKey } from '$frontend';
import type { ActiveFormats, ActiveFormatsOptions } from '$formats';
import type { EngineHttp, EngineHttpOptions } from '$http';
import type { AppEventMap } from '$libs/aapp/events';
import type { AppEventMap } from '$libs/active-app/events';
import type { ActiveLang, LangNode, SupportedLocale } from '$libs/lang';
import type { EngineLogger, LoggerOptions } from '$logr';
import type { ActivePermissions, ActivePermissionsOptions } from '$perm';
import type { ActiveSession, EngineSessionOptions, SessEventMap } from '$sess';
import type { EngineLogger, LoggerOptions } from '$logger';
import type { ActivePermissions, ActivePermissionsOptions } from '$permissions';
import type { ActiveSession, EngineSessionOptions, SessEventMap } from '$session';
import type { EngineSium } from '$sium';
import type { ActiveStorage, SyncStorageAdapter } from '$stor';
import type { ActiveTimers } from '$timr';
import type { EngineTimersOptions } from '$libs/timers';
import type { ActiveStorage, SyncStorageAdapter } from '$storage';
import type { ActiveTimers } from '$timer';
import type { EngineTimersOptions } from '$libs/timer';
import type {
AAPP_ORCHESTRATION_SILENT,
AAPP_ORCHESTRATION_STANDARD,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE,
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED
APP_ORCHESTRATION_SILENT,
APP_ORCHESTRATION_STANDARD,
APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED
} from './consts.ts';
/** Frontend preferences eligible for App-managed persistence. */
@ -73,15 +73,15 @@ export interface ActiveAppStorageOptions {
export type ActiveAppOrchestrationPreset =
| false
| typeof AAPP_ORCHESTRATION_SILENT
| typeof AAPP_ORCHESTRATION_STANDARD;
| typeof APP_ORCHESTRATION_SILENT
| typeof APP_ORCHESTRATION_STANDARD;
export type ActiveAppOrchestrationTranslator =
| typeof AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY
| typeof AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH
| typeof AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED
| typeof AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY
| typeof AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE;
| typeof APP_ORCHESTRATION_TRANSLATOR_IDENTITY
| typeof APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH
| typeof APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED
| typeof APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY
| typeof APP_ORCHESTRATION_TRANSLATOR_DISPOSE;
export type ActiveAppOrchestrationOptions =
| ActiveAppOrchestrationPreset
@ -96,7 +96,7 @@ export type ActiveAppOrchestrationOptions =
* - `lang` defaults to a mono lang when absent (single-language passthrough,
* reactive locale still owned so Formats/Frontend stay in sync).
* - `formats` is always built; sub-engines accept their own knobs (currency,
* date order, etc.). When absent the locale falls back to `FMTS_DEFAULT_LOCALE`.
* date order, etc.). When absent the locale falls back to `FORMATS_DEFAULT_LOCALE`.
*
* Sium is intentionally **not** part of App — validation is page-scoped.
* Pages that need it construct an `EngineSium` directly:
@ -174,7 +174,7 @@ export interface ActiveAppOptions<S extends LangNode = LangNode> {
* automatically; the authoritative auth engine still lives server-side
* under `$svrs/auth`.
*/
auth?: Omit<ActiveAuthOptions, 'http' | 'cach' | 'logger'>;
auth?: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'>;
/**
* Cross-artifact orchestration policy. App always exposes `App.Bus`;
* orchestration only controls translators from module events to stable
@ -197,7 +197,7 @@ export interface ActiveAppOptions<S extends LangNode = LangNode> {
* | -------- | ---------- | ------------------------------------------------ |
* | Logger | real | `level: WARN` + `consoleTransport()` (engine default; pass `{ level: NONE, transports: [] }` for silence) |
* | Lang | real | mono — returns paths and `\|fallback` literals; warns once per path in DEV via Logger under `lang.mono` |
* | Formats | real | real with locale = `FMTS_DEFAULT_LOCALE` (`'en-US'`) |
* | Formats | real | real with locale = `FORMATS_DEFAULT_LOCALE` (`'en-US'`) |
* | Frontend | real | real with default theme/mode/density |
* | Dom | real | real with default breakpoints |
*/
@ -240,7 +240,7 @@ export interface ActiveApp<S extends LangNode = LangNode> {
* `onRefresh`, `onRevoke`, `broadcastChannel`).
*
* Single session per App — the second call throws
* `SessAlreadyCreatedError`. Multi-account scenarios compose multiple
* `SessionAlreadyCreatedError`. Multi-account scenarios compose multiple
* App instances. The session is auto-disposed by `App.dispose()`.
*
* Generics let the caller declare the exact shape:
@ -286,7 +286,7 @@ export interface ActiveApp<S extends LangNode = LangNode> {
/**
* Build the App-scoped reactive permissions client. The authoritative
* runtime is `createEnginePermissions()` from `$svrs/perm`; this client
* runtime is `createEnginePermissions()` from `$svrs/permissions`; this client
* is only for UI/UX reflection, snapshots and cache.
*/
createActivePermissions(
@ -299,7 +299,7 @@ export interface ActiveApp<S extends LangNode = LangNode> {
* reflects `/api/auth/*` state, sends CSRF headers and exposes pending /
* error state for UI.
*/
createActiveAuth(options?: Omit<ActiveAuthOptions, 'http' | 'cach' | 'logger'>): ActiveAuth;
createActiveAuth(options?: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'>): ActiveAuth;
/**
* The active session, when one has been built via

@ -408,7 +408,7 @@ Antes de usarlo fuera de tests:
- Usar un `AuthStoreAdapter` transaccional.
- Conectar `AuthSessPort` al módulo real `sess`.
- Conectar `AuthLogrPort` a `logr`.
- Conectar `AuthCachPort` a `cach`.
- Conectar `AuthCachePort` a `cach`.
- Usar `createNodeScryptPasswordHasher()` o un adapter Argon2id propio.
- Resolver `tenantId` desde request, subdominio, organización o app config.
- Mantener `security.csrf.signingKey` fuera del repo.

@ -55,13 +55,13 @@ export async function invalidateAuthClientCache(
diagnostics: AuthClientDiagnostics,
reason: AuthEventName
): Promise<void> {
if (!options.cach) return;
if (!options.cache) return;
try {
await options.cach.invalidate({ tags: authCacheTagsForIdentity(), reason });
await options.cache.invalidate({ tags: authCacheTagsForIdentity(), reason });
} catch (error) {
emitAuthClientDiagnostic(
diagnostics,
AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACH_INVALIDATION_FAILED,
AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACHE_INVALIDATION_FAILED,
{ reason, error }
);
options.onCacheError?.(error);

@ -47,7 +47,7 @@ export const AUTH_ERR_INVALID_RESPONSE: ErrCode = errCode(AUTH_ERR, 'invalid_res
export const AUTH_CLIENT_DIAGNOSTIC_EVENTS = {
OPERATION_FAILED: 'auth.client.operation_failed',
CACH_INVALIDATION_FAILED: 'auth.client.cache_invalidation_failed'
CACHE_INVALIDATION_FAILED: 'auth.client.cache_invalidation_failed'
} as const;
export const AUTH_CLIENT_LOG_MESSAGE_OPERATION_FAILED = 'auth client operation failed';

@ -5,7 +5,7 @@ import {
type DiagnosticEvent,
type Diagnostics,
type Logger
} from '$libs/logr';
} from '$libs/logger';
import {
AUTH_CLIENT_DIAGNOSTIC_EVENTS,
AUTH_CLIENT_LOG_MESSAGE_CACHE_INVALIDATION_FAILED,
@ -34,7 +34,7 @@ const AUTH_CLIENT_DIAGNOSTIC_LOGS: DiagnosticCatalog<AuthClientDiagnosticEvent>
level: LogLevel.WARN,
message: AUTH_CLIENT_LOG_MESSAGE_OPERATION_FAILED
},
[AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACH_INVALIDATION_FAILED]: {
[AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACHE_INVALIDATION_FAILED]: {
level: LogLevel.WARN,
message: AUTH_CLIENT_LOG_MESSAGE_CACHE_INVALIDATION_FAILED
}

@ -86,8 +86,8 @@ export type {
AuthActiveRevokeDeviceInput
} from './types.ts';
export type {
AuthClientCachInvalidationInput,
AuthClientCachPort,
AuthClientCacheInvalidationInput,
AuthClientCachePort,
AuthClientHttpPort,
AuthClientRequestOptions,
AuthClientStoragePort

@ -103,7 +103,7 @@ describe('createActiveAuth', () => {
const Auth = createActiveAuth({
http: createFetchAuthClient(fetcher),
cach: {
cache: {
invalidate(input) {
invalidations.push(input);
}

@ -12,15 +12,15 @@ import type {
} from '$libs/auth/types';
import type { ActiveChangeListener, ActiveEngine } from '$libs/active';
import type {
AuthClientCachPort,
AuthClientCachePort,
AuthClientHttpPort,
AuthClientStoragePort
} from '$libs/auth/contracts';
import type { Logger } from '$libs/logr';
import type { Logger } from '$libs/logger';
export interface ActiveAuthOptions {
readonly http: AuthClientHttpPort;
readonly cach?: AuthClientCachPort;
readonly cache?: AuthClientCachePort;
readonly stor?: AuthClientStoragePort;
readonly initial?: AuthCurrentView;
readonly logger?: Logger;

@ -37,10 +37,10 @@ comes from each owner declaring constants, payload shapes, and typed
The framework distinguishes two layers of events that share a single
`App.Bus` instance:
- **Module events** (`SESS_EVENT_*`, `AUTH_EVENT_*`, `CACH_EVENT_*`, …)
- **Module events** (`SESSION_EVENT_*`, `AUTH_EVENT_*`, `CACHE_EVENT_*`, …)
— internal facts emitted by the artifact that owns them. They can
iterate; they are not part of the public contract.
- **App events** (`AAPP_EVENT_*`) — the **stable public contract**.
- **App events** (`APP_EVENT_*`) — the **stable public contract**.
Universal facts that consumers and external plugins listen to.
Renaming or removing one is a breaking change.
@ -54,8 +54,8 @@ The model is **Domain Events + Integration Events** with an
| Active term | DDD term |
| ------------------------------ | ----------------------------------- |
| Module events (`SESS_EVENT_*`, …) | Domain events (private, iterable) |
| App events (`AAPP_EVENT_*`) | Integration events (public, stable) |
| Module events (`SESSION_EVENT_*`, …) | Domain events (private, iterable) |
| App events (`APP_EVENT_*`) | Integration events (public, stable) |
| `aapp/integrations/*-translator.ts` | Anti-corruption layer + event mapper |
| Per-consumer auto-reactions | Stateless process managers |
@ -68,7 +68,7 @@ arts/buss/ ← engine, framework-agnostic
types.ts EngineBus, BusEnvelope, BusListener,
BusPublishOptions, BusPublishResult,
EventPublisher, BusAnyListener
consts.ts BUSS_*, listener error modes, diagnostics
consts.ts BUS_*, listener error modes, diagnostics
engine-bus.ts createEngineBus()
no Svelte imports
errors.ts / matching.ts / diagnostics.ts
@ -81,8 +81,8 @@ arts/buss/svelte/ ← Svelte adapter
arts/buss/active-bus.svelte.ts ← reactive wrappers (minimal surface)
createBusRecent() { lastEvent, count, clear, dispose }
libs/aapp/events.ts ← AAPP_EVENT_* constants + payloads
+ AAPP_EVENT_RUNTIMES metadata
libs/aapp/events.ts ← APP_EVENT_* constants + payloads
+ APP_EVENT_RUNTIMES metadata
libs/aapp/bus-context.svelte.ts ← getBus / setBus via createContext
@ -244,7 +244,7 @@ export type BusAnyListener<TEvents extends BusEventMap> = (
The convention remains conservative: `onAny` is for diagnostics,
devtools, event capture and tests. Business side effects should subscribe
to the exact `AAPP_EVENT_*` they need.
to the exact `APP_EVENT_*` they need.
### Re-entrancy
@ -275,11 +275,11 @@ Each module owns and emits its own facts. Constants live in the module's
```ts
// arts/sess/consts.ts
export const SESS_EVENT_CHANGED = 'sess.changed';
export const SESS_EVENT_IDENTITY_CHANGED = 'sess.identity.changed';
export const SESS_EVENT_REVOKED = 'sess.revoked';
export const SESS_EVENT_EXPIRED = 'sess.expired';
export const SESS_EVENT_REFRESHED = 'sess.refreshed';
export const SESSION_EVENT_CHANGED = 'session.changed';
export const SESSION_EVENT_IDENTITY_CHANGED = 'session.identity.changed';
export const SESSION_EVENT_REVOKED = 'session.revoked';
export const SESSION_EVENT_EXPIRED = 'session.expired';
export const SESSION_EVENT_REFRESHED = 'session.refreshed';
// arts/sess/types.ts
export interface SessLifecyclePayload {
@ -303,32 +303,32 @@ export function publishSessLifecycleEvent(
payload: SessLifecyclePayload,
options: BusPublishOptions = {}
): void {
if (payload.event === SESS_EVENT_LIFECYCLE_INITIAL) return;
const opts = { source: SESS_MODULE, ...options };
if (payload.event === SESSION_EVENT_LIFECYCLE_INITIAL) return;
const opts = { source: SESSION_MODULE, ...options };
bus.publish(SESS_EVENT_CHANGED, payload, opts);
bus.publish(SESSION_EVENT_CHANGED, payload, opts);
if (payload.identity.from !== payload.identity.to) {
bus.publish(SESS_EVENT_IDENTITY_CHANGED, payload, opts);
bus.publish(SESSION_EVENT_IDENTITY_CHANGED, payload, opts);
}
if (payload.event === SESS_EVENT_LIFECYCLE_REVOKED) {
bus.publish(SESS_EVENT_REVOKED, payload, opts);
if (payload.event === SESSION_EVENT_LIFECYCLE_REVOKED) {
bus.publish(SESSION_EVENT_REVOKED, payload, opts);
}
if (payload.event === SESS_EVENT_LIFECYCLE_EXPIRED) {
bus.publish(SESS_EVENT_EXPIRED, payload, opts);
if (payload.event === SESSION_EVENT_LIFECYCLE_EXPIRED) {
bus.publish(SESSION_EVENT_EXPIRED, payload, opts);
}
if (payload.event === SESS_EVENT_LIFECYCLE_REFRESHED) {
bus.publish(SESS_EVENT_REFRESHED, payload, opts);
if (payload.event === SESSION_EVENT_LIFECYCLE_REFRESHED) {
bus.publish(SESSION_EVENT_REFRESHED, payload, opts);
}
}
// Subscriber: one event, one listener. `onSessChanged` covers every
// lifecycle transition; subscribe to `SESS_EVENT_IDENTITY_CHANGED` /
// `SESS_EVENT_REVOKED` / etc directly when you only care about a slice.
// lifecycle transition; subscribe to `SESSION_EVENT_IDENTITY_CHANGED` /
// `SESSION_EVENT_REVOKED` / etc directly when you only care about a slice.
export function onSessChanged(
bus: EngineBus<SessEventMap>,
listener: (event: SessChangedEnvelope) => void | Promise<void>
): BusSubscription {
return bus.on(SESS_EVENT_CHANGED, (event) => listener(event as SessChangedEnvelope));
return bus.on(SESSION_EVENT_CHANGED, (event) => listener(event as SessChangedEnvelope));
}
```
@ -342,15 +342,15 @@ payloads — never `any`. No credentials.**
```ts
// libs/aapp/events.ts
export const AAPP_EVENT_USER_IDENTITY_CHANGED = 'aapp.user.identity.changed';
export const AAPP_EVENT_TENANT_SWITCHED = 'aapp.tenant.switched';
export const AAPP_EVENT_PERMISSIONS_REFRESH_REQUESTED = 'aapp.permissions.refresh.requested';
export const AAPP_EVENT_CONNECTIVITY_CHANGED = 'aapp.connectivity.changed';
export const AAPP_EVENT_CACHE_INVALIDATE_REQUESTED = 'aapp.cache.invalidate.requested';
export const AAPP_EVENT_DISPOSE_STARTING = 'aapp.dispose.starting';
export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed';
export const APP_EVENT_TENANT_SWITCHED = 'app.tenant.switched';
export const APP_EVENT_PERMISSIONS_REFRESH_REQUESTED = 'app.permissions.refresh.requested';
export const APP_EVENT_CONNECTIVITY_CHANGED = 'app.connectivity.changed';
export const APP_EVENT_CACHE_INVALIDATE_REQUESTED = 'app.cache.invalidate.requested';
export const APP_EVENT_DISPOSE_STARTING = 'app.dispose.starting';
```
Renaming any `AAPP_EVENT_*`, removing it, or changing its payload shape
Renaming any `APP_EVENT_*`, removing it, or changing its payload shape
non-additively is a **major bump**. Adding new app events is a minor.
## App.Bus is always-present per render scope
@ -441,11 +441,11 @@ function directly) inside `$effect`:
```svelte
<script lang="ts">
import { AAPP_EVENT_USER_IDENTITY_CHANGED } from '$libs/aapp/events';
import { APP_EVENT_USER_IDENTITY_CHANGED } from '$libs/aapp/events';
const Bus = getBus();
$effect(() =>
Bus.subscribe(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
Bus.subscribe(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
// react
})
);
@ -516,25 +516,25 @@ component's lifecycle.
### Rule 7 — Server / client / both-runtime events
Each `AAPP_EVENT_*` declares where it is allowed to fire:
Each `APP_EVENT_*` declares where it is allowed to fire:
```ts
// libs/aapp/events.ts
export const AAPP_EVENT_RUNTIMES: Readonly<Record<string, AppEventRuntime>> = {
[AAPP_EVENT_USER_IDENTITY_CHANGED]: AAPP_EVENT_RUNTIME_BOTH,
[AAPP_EVENT_TENANT_SWITCHED]: AAPP_EVENT_RUNTIME_BOTH,
[AAPP_EVENT_PERMISSIONS_REFRESH_REQUESTED]: AAPP_EVENT_RUNTIME_BOTH,
[AAPP_EVENT_CONNECTIVITY_CHANGED]: AAPP_EVENT_RUNTIME_CLIENT,
[AAPP_EVENT_CACHE_INVALIDATE_REQUESTED]: AAPP_EVENT_RUNTIME_BOTH,
[AAPP_EVENT_DISPOSE_STARTING]: AAPP_EVENT_RUNTIME_BOTH
export const APP_EVENT_RUNTIMES: Readonly<Record<string, AppEventRuntime>> = {
[APP_EVENT_USER_IDENTITY_CHANGED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_TENANT_SWITCHED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_PERMISSIONS_REFRESH_REQUESTED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_CONNECTIVITY_CHANGED]: APP_EVENT_RUNTIME_CLIENT,
[APP_EVENT_CACHE_INVALIDATE_REQUESTED]: APP_EVENT_RUNTIME_BOTH,
[APP_EVENT_DISPOSE_STARTING]: APP_EVENT_RUNTIME_BOTH
};
export type AppEventRuntime =
| typeof AAPP_EVENT_RUNTIME_BOTH
| typeof AAPP_EVENT_RUNTIME_CLIENT;
| typeof APP_EVENT_RUNTIME_BOTH
| typeof APP_EVENT_RUNTIME_CLIENT;
export function assertEventCanFire(type: string, where: 'server' | 'client'): void {
const runtime = AAPP_EVENT_RUNTIMES[type];
const runtime = APP_EVENT_RUNTIMES[type];
if (runtime && runtime !== 'both' && runtime !== where) {
throw new AappInvalidEventRuntimeError(type, runtime, where);
}
@ -572,39 +572,39 @@ execute domain logic.
```ts
// arts/aapp/integrations/session-translator.ts
import {
AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER,
AAPP_USER_IDENTITY_CAUSE_SESSION_EXPIRED,
AAPP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED,
AAPP_USER_IDENTITY_CAUSE_SESSION_REFRESHED,
AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED,
APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER,
APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED,
APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED,
APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged
} from '$libs/aapp/events';
import {
SESS_EVENT_CHANGED,
SESS_EVENT_LIFECYCLE_ADOPTED,
SESS_EVENT_LIFECYCLE_ADOPTED_SERVER,
SESS_EVENT_LIFECYCLE_EXPIRED,
SESS_EVENT_LIFECYCLE_EXTERNAL_CHANGED,
SESS_EVENT_LIFECYCLE_REFRESHED,
SESS_EVENT_LIFECYCLE_REVOKED
SESSION_EVENT_CHANGED,
SESSION_EVENT_LIFECYCLE_ADOPTED,
SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER,
SESSION_EVENT_LIFECYCLE_EXPIRED,
SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED,
SESSION_EVENT_LIFECYCLE_REFRESHED,
SESSION_EVENT_LIFECYCLE_REVOKED
} from '$sess/consts';
import type { EngineBus } from '$libs/buss';
// Each lifecycle event maps to its own app-level cause. Hardcoding a
// single cause would mislabel five out of six transitions.
function resolveAppIdentityCause(event) {
if (event === SESS_EVENT_LIFECYCLE_ADOPTED) return AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED;
if (event === SESS_EVENT_LIFECYCLE_ADOPTED_SERVER) return AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER;
if (event === SESS_EVENT_LIFECYCLE_REFRESHED) return AAPP_USER_IDENTITY_CAUSE_SESSION_REFRESHED;
if (event === SESS_EVENT_LIFECYCLE_REVOKED) return AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED;
if (event === SESS_EVENT_LIFECYCLE_EXPIRED) return AAPP_USER_IDENTITY_CAUSE_SESSION_EXPIRED;
if (event === SESS_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return AAPP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED;
if (event === SESSION_EVENT_LIFECYCLE_ADOPTED) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED;
if (event === SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER;
if (event === SESSION_EVENT_LIFECYCLE_REFRESHED) return APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED;
if (event === SESSION_EVENT_LIFECYCLE_REVOKED) return APP_USER_IDENTITY_CAUSE_SESSION_REVOKED;
if (event === SESSION_EVENT_LIFECYCLE_EXPIRED) return APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED;
if (event === SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED;
return undefined;
}
export function wireSessionTranslator(bus: EngineBus): () => void {
const sub = bus.on(SESS_EVENT_CHANGED, (event) => {
const sub = bus.on(SESSION_EVENT_CHANGED, (event) => {
const cause = resolveAppIdentityCause(event.payload.event);
if (cause === undefined) return;
publishAppUserIdentityChanged(bus, {
@ -616,7 +616,7 @@ export function wireSessionTranslator(bus: EngineBus): () => void {
},
cause
}, {
source: AAPP_MODULE
source: APP_MODULE
});
});
return () => sub.unsubscribe();
@ -663,11 +663,11 @@ Translators wired by `'standard'`:
| Translator | Source events | Publishes |
| --------------- | -------------------------------------------------------------------------- | ------------------------------------ |
| `identity` | `SESS_EVENT_CHANGED` from `App.createActiveSession(...)` | `AAPP_EVENT_USER_IDENTITY_CHANGED` |
| `dispose` | `App.dispose()` entry | `AAPP_EVENT_DISPOSE_STARTING` |
| `tenant-switched` | typed public contract / explicit publish point | `AAPP_EVENT_TENANT_SWITCHED` |
| `permissions-refresh` | typed public contract / explicit publish point | `AAPP_EVENT_PERMISSIONS_REFRESH_REQUESTED` |
| `connectivity` | typed public contract / explicit publish point | `AAPP_EVENT_CONNECTIVITY_CHANGED` |
| `identity` | `SESSION_EVENT_CHANGED` from `App.createActiveSession(...)` | `APP_EVENT_USER_IDENTITY_CHANGED` |
| `dispose` | `App.dispose()` entry | `APP_EVENT_DISPOSE_STARTING` |
| `tenant-switched` | typed public contract / explicit publish point | `APP_EVENT_TENANT_SWITCHED` |
| `permissions-refresh` | typed public contract / explicit publish point | `APP_EVENT_PERMISSIONS_REFRESH_REQUESTED` |
| `connectivity` | typed public contract / explicit publish point | `APP_EVENT_CONNECTIVITY_CHANGED` |
### Per consumer: which app events auto-trigger side-effects
@ -703,10 +703,10 @@ Consumers (`cach`, `perm`, `conn`, third-party plugins) listen **only**
to `app.*` events.
```ts
import { AAPP_EVENT_USER_IDENTITY_CHANGED } from '$libs/aapp/events';
import { APP_EVENT_USER_IDENTITY_CHANGED } from '$libs/aapp/events';
$effect(() =>
App.Bus.subscribe(AAPP_EVENT_USER_IDENTITY_CHANGED, () => {
App.Bus.subscribe(APP_EVENT_USER_IDENTITY_CHANGED, () => {
Cache.invalidate();
})
);
@ -754,12 +754,12 @@ Start small. Six facts cover the universal needs:
| Constant | Meaning |
| ------------------------------------- | ----------------------------------------------------------- |
| `AAPP_EVENT_USER_IDENTITY_CHANGED` | Sign-in, sign-out, refresh, impersonation, … |
| `AAPP_EVENT_TENANT_SWITCHED` | Multi-tenant switch |
| `AAPP_EVENT_PERMISSIONS_REFRESH_REQUESTED` | Policies changed, consumers may drop permission caches |
| `AAPP_EVENT_CONNECTIVITY_CHANGED` | Online ↔ offline (client-only) |
| `AAPP_EVENT_CACHE_INVALIDATE_REQUESTED` | Broad invalidation requested |
| `AAPP_EVENT_DISPOSE_STARTING` | App.dispose() entered, last chance to flush |
| `APP_EVENT_USER_IDENTITY_CHANGED` | Sign-in, sign-out, refresh, impersonation, … |
| `APP_EVENT_TENANT_SWITCHED` | Multi-tenant switch |
| `APP_EVENT_PERMISSIONS_REFRESH_REQUESTED` | Policies changed, consumers may drop permission caches |
| `APP_EVENT_CONNECTIVITY_CHANGED` | Online ↔ offline (client-only) |
| `APP_EVENT_CACHE_INVALIDATE_REQUESTED` | Broad invalidation requested |
| `APP_EVENT_DISPOSE_STARTING` | App.dispose() entered, last chance to flush |
Grow this list deliberately. A bus with 50 app events is the same
god-object problem at a different layer.
@ -782,7 +782,7 @@ Three protections, applied together:
part such as `token`, `secret`, `password`, `hash`, `authorization`,
`cookie`, `credential`, `csrf` or `header`.
3. **Documented invariant.** Every `AAPP_EVENT_*` declaration carries the
3. **Documented invariant.** Every `APP_EVENT_*` declaration carries the
comment: _"App events never contain credentials. To pass sensitive
data, use `correlationId` and let the consumer resolve it against
`App.Sess` directly."_
@ -816,7 +816,7 @@ import {
flushSync(() => {
publishAppUserIdentityChanged(Bus, {
event: 'sess.lifecycle.adopted',
event: 'session.lifecycle.adopted',
generation: 2,
identity: { from: 'ada', to: 'linus' },
cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED
@ -851,15 +851,15 @@ Frozen for `0.1.x`:
- `EngineBus`, `EventPublisher`, `BusEnvelope`, `BusPublishResult`,
`BusSubscription`, `BusListener`, `BusListenerContext`,
`BusListenerErrorMode`, `BusAnyListener`, `BusEventUnion`.
- Generic `BUSS_*` constants (modes, defaults, `BUSS_EVENT_ALL`,
- Generic `BUS_*` constants (modes, defaults, `BUS_EVENT_ALL`,
`BusInvalidPayloadError`, `BusReentrancyLimitError`).
- All `AAPP_EVENT_*` constants and their payload types.
- All `APP_EVENT_*` constants and their payload types.
- Sync-vs-async semantics, listener order, error handling contract.
- Re-entrancy semantics (DFS + maxReentrancyDepth).
Free to iterate inside `0.1.x`:
- Module events (`SESS_EVENT_*`, `AUTH_EVENT_*`, …) and their payloads.
- Module events (`SESSION_EVENT_*`, `AUTH_EVENT_*`, …) and their payloads.
- Internal diagnostics constants.
- Translator implementations under `arts/aapp/integrations/`.
@ -912,20 +912,20 @@ Out of scope for the current cut:
## Naming convention reminder
- `BUSS_*` — generic bus constants (live in `arts/buss`).
- `AAPP_EVENT_*` — public app event names (live in `libs/aapp/events.ts`).
- `<MOD>_EVENT_*` (`SESS_EVENT_*`, `AUTH_EVENT_*`, `CACH_EVENT_*`,
`PERM_EVENT_*`, `CONN_EVENT_*`) — module event names, live in their
- `BUS_*` — generic bus constants (live in `arts/buss`).
- `APP_EVENT_*` — public app event names (live in `libs/aapp/events.ts`).
- `<MOD>_EVENT_*` (`SESSION_EVENT_*`, `AUTH_EVENT_*`, `CACHE_EVENT_*`,
`PERMISSION_EVENT_*`, `CONNECTION_EVENT_*`) — module event names, live in their
owning artifact's `consts.ts`.
- Diagnostic constants are **full-prefixed strings**, never bare names:
`'buss.event.published'`, not `'event_published'`.
`'bus.event.published'`, not `'event_published'`.
- Helpers follow `publish<Domain><Verb>` / `on<Domain><Verb>`.
## Invariants
- `arts/buss` does not import other artifacts.
- `arts/buss` does not export domain event constants. Domain events
belong to their owning artifact; `AAPP_EVENT_*` belongs to `libs/aapp`.
belong to their owning artifact; `APP_EVENT_*` belongs to `libs/aapp`.
- Events describe what happened; they never command another module to
do something.
- Automatic side-effects are opt-in per consumer.

@ -8,11 +8,11 @@
* intentionally tiny — anything richer (history, derived projections,
* windowing) waits until real usage patterns emerge.
*
* Lives in `arts/buss` (not `libs/buss`) because it depends on Svelte's
* Lives in `arts/buss` (not `libs/bus`) because it depends on Svelte's
* `$state` rune.
*/
import type { BusEnvelope, BusListener, EngineBus } from '$libs/buss';
import type { BusEnvelope, BusListener, EngineBus } from '$libs/bus';
export interface BusRecent<TPayload> {
readonly lastEvent: BusEnvelope<string, TPayload> | undefined;

@ -1,13 +1,13 @@
import { SILENT_LOGGER } from '$libs/logr';
import { SILENT_LOGGER } from '$libs/logger';
import {
BUSS_DEFAULT_MAX_LISTENERS_PER_EVENT,
BUSS_DEFAULT_MAX_REENTRANCY_DEPTH,
BUSS_DEFAULT_SOURCE,
BUSS_EVENT_ALL,
BUSS_ID_PREFIX,
BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE,
BUSS_LISTENER_ERROR_MODE_THROW,
BUSS_LISTENER_ID_PREFIX,
BUS_DEFAULT_MAX_LISTENERS_PER_EVENT,
BUS_DEFAULT_MAX_REENTRANCY_DEPTH,
BUS_DEFAULT_SOURCE,
BUS_EVENT_ALL,
BUS_ID_PREFIX,
BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE,
BUS_LISTENER_ERROR_MODE_THROW,
BUS_LISTENER_ID_PREFIX,
BusAggregateListenerError,
BusDisposedError,
BusInvalidPayloadError,
@ -15,7 +15,7 @@ import {
createBusDiagnostics,
emitBusListenerFailureDiagnostic,
emitBusListenerLeakWarningDiagnostic
} from '$libs/buss';
} from '$libs/bus';
import type {
BusEnvelope,
BusEventMap,
@ -29,11 +29,11 @@ import type {
BusSubscription,
EngineBus,
EngineBusOptions
} from '$libs/buss';
} from '$libs/bus';
interface ListenerRecord {
readonly id: string;
readonly type: string | typeof BUSS_EVENT_ALL;
readonly type: string | typeof BUS_EVENT_ALL;
readonly listener: BusListener<unknown>;
readonly controller: AbortController;
readonly externalSignal?: AbortSignal;
@ -55,13 +55,13 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
const logger = options.logger ?? SILENT_LOGGER;
const diagnostics = createBusDiagnostics(logger);
const clock = options.clock ?? DEFAULT_CLOCK;
const idFactory = options.idFactory ?? createIncrementalIdFactory(BUSS_ID_PREFIX);
const idFactory = options.idFactory ?? createIncrementalIdFactory(BUS_ID_PREFIX);
const maxListenersPerEvent =
options.maxListenersPerEvent ?? BUSS_DEFAULT_MAX_LISTENERS_PER_EVENT;
options.maxListenersPerEvent ?? BUS_DEFAULT_MAX_LISTENERS_PER_EVENT;
const maxReentrancyDepth =
options.maxReentrancyDepth ?? BUSS_DEFAULT_MAX_REENTRANCY_DEPTH;
options.maxReentrancyDepth ?? BUS_DEFAULT_MAX_REENTRANCY_DEPTH;
const defaultErrorMode =
options.listenerErrorMode ?? BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE;
options.listenerErrorMode ?? BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE;
const invokeListener = options.invokeListener ?? defaultInvokeListener;
const listeners = new Map<string, ListenerRecord[]>();
const anyListeners: ListenerRecord[] = [];
@ -75,7 +75,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
function nextListenerId(): string {
listenerSequence += 1;
return `${BUSS_LISTENER_ID_PREFIX}${listenerSequence}`;
return `${BUS_LISTENER_ID_PREFIX}${listenerSequence}`;
}
function assertPayloadCloneable<TType extends string>(
@ -100,7 +100,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
type,
payload,
at: clock.now(),
source: publishOptions?.source ?? BUSS_DEFAULT_SOURCE
source: publishOptions?.source ?? BUS_DEFAULT_SOURCE
};
return withOptionalEnvelopeFields(envelope, publishOptions);
}
@ -110,7 +110,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
}
function subscribe(
type: string | typeof BUSS_EVENT_ALL,
type: string | typeof BUS_EVENT_ALL,
listener: BusListener<unknown>,
listenOptions: BusListenOptions = {}
): BusSubscription {
@ -139,8 +139,8 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
return createSubscription(record, remove);
}
function listenerBucket(type: string | typeof BUSS_EVENT_ALL): ListenerRecord[] {
if (type === BUSS_EVENT_ALL) return anyListeners;
function listenerBucket(type: string | typeof BUS_EVENT_ALL): ListenerRecord[] {
if (type === BUS_EVENT_ALL) return anyListeners;
let bucket = listeners.get(type);
if (bucket === undefined) {
bucket = [];
@ -149,7 +149,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
return bucket;
}
function warnIfListenerLeak(type: string | typeof BUSS_EVENT_ALL): void {
function warnIfListenerLeak(type: string | typeof BUS_EVENT_ALL): void {
const count = listenerBucket(type).length;
if (count <= maxListenersPerEvent) return;
emitBusListenerLeakWarningDiagnostic({
@ -177,11 +177,11 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
if (!record.active) return;
record.active = false;
record.controller.abort();
const bucket = record.type === BUSS_EVENT_ALL ? anyListeners : listeners.get(record.type);
const bucket = record.type === BUS_EVENT_ALL ? anyListeners : listeners.get(record.type);
if (bucket === undefined) return;
const index = bucket.indexOf(record);
if (index >= 0) bucket.splice(index, 1);
if (record.type !== BUSS_EVENT_ALL && bucket.length === 0) listeners.delete(record.type);
if (record.type !== BUS_EVENT_ALL && bucket.length === 0) listeners.delete(record.type);
}
function publish<TType extends keyof TEvents & string>(
@ -317,7 +317,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
error
};
errors.push(failure);
if (mode === BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE) {
if (mode === BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE) {
emitBusListenerFailureDiagnostic({ diagnostics, envelope, failure });
}
}
@ -327,7 +327,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
errors: readonly BusListenerFailure[],
envelope: BusEnvelope
): void {
if (mode !== BUSS_LISTENER_ERROR_MODE_THROW || errors.length === 0) return;
if (mode !== BUS_LISTENER_ERROR_MODE_THROW || errors.length === 0) return;
throw new BusAggregateListenerError(errors, envelope);
}
@ -338,8 +338,8 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
return count;
}
function clearForTesting(type?: keyof TEvents & string | typeof BUSS_EVENT_ALL): void {
if (type === BUSS_EVENT_ALL) {
function clearForTesting(type?: keyof TEvents & string | typeof BUS_EVENT_ALL): void {
if (type === BUS_EVENT_ALL) {
for (const record of [...anyListeners]) removeRecord(record);
return;
}
@ -365,7 +365,7 @@ export function createEngineBus<TEvents extends BusEventMap = BusEventMap>(
},
onAny(listener, listenOptions) {
return subscribe(
BUSS_EVENT_ALL,
BUS_EVENT_ALL,
listener as unknown as BusListener<unknown>,
listenOptions
);

@ -3,10 +3,10 @@
*
* Only the runtime factory lives here. Interfaces, generic constants,
* error classes, the `SILENT_BUS` no-op, and pure helpers live in
* `$libs/buss`.
* `$libs/bus`.
*
* Modules that need to publish or subscribe MUST import their types
* from `$libs/buss`. They MUST NOT import from `$buss` — that is
* from `$libs/bus`. They MUST NOT import from `$bus` — that is
* reserved for the composition root (`aapp`) and tests that need the
* concrete engine.
*/

@ -18,7 +18,7 @@
*/
import { untrack } from 'svelte';
import type { BusEventMap, EngineBus, EngineBusOptions } from '$libs/buss';
import type { BusEventMap, EngineBus, EngineBusOptions } from '$libs/bus';
import { createEngineBus } from '../engine-bus.ts';
export function createSvelteEngineBus<TEvents extends BusEventMap = BusEventMap>(

@ -1,18 +1,18 @@
import { describe, expect, it, vi } from 'vitest';
import {
BUSS_EVENT_ALL,
BUSS_LISTENER_ERROR_MODE_COLLECT,
BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE,
BUSS_LISTENER_ERROR_MODE_THROW,
BUS_EVENT_ALL,
BUS_LISTENER_ERROR_MODE_COLLECT,
BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE,
BUS_LISTENER_ERROR_MODE_THROW,
BusAggregateListenerError,
BusDisposedError,
BusInvalidPayloadError,
BusReentrancyLimitError
} from '$libs/buss';
} from '$libs/bus';
import { createEngineBus } from '../index.ts';
import type { LogFn, Logger } from '$libs/logr';
import type { LogFn, Logger } from '$libs/logger';
const EVENT_IDENTITY_CHANGED = 'sess.identity.changed';
const EVENT_IDENTITY_CHANGED = 'session.identity.changed';
const EVENT_AUTH_SIGNED_OUT = 'auth.signed_out';
interface TestEvents {
@ -84,7 +84,7 @@ describe('createEngineBus', () => {
id: 'event-1',
type: EVENT_IDENTITY_CHANGED,
at: 42,
source: 'buss',
source: 'bus',
payload: {
previousActorId: 'actor-ada',
nextActorId: 'actor-linus'
@ -171,7 +171,7 @@ describe('createEngineBus', () => {
const { logger, entries } = createCapturedLogger();
const Bus = createEngineBus<TestEvents>({
logger,
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT
listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT
});
const afterFailure = vi.fn();
const error = new Error('boom');
@ -197,7 +197,7 @@ describe('createEngineBus', () => {
const { logger, entries } = createCapturedLogger();
const Bus = createEngineBus<TestEvents>({
logger,
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE
listenerErrorMode: BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE
});
Bus.on(EVENT_AUTH_SIGNED_OUT, () => {
@ -207,7 +207,7 @@ describe('createEngineBus', () => {
const result = Bus.publish(
EVENT_AUTH_SIGNED_OUT,
{ actorId: 'actor-ada' },
{ listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT }
{ listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT }
);
expect(result.errors).toHaveLength(1);
@ -218,7 +218,7 @@ describe('createEngineBus', () => {
const { logger, entries } = createCapturedLogger();
const Bus = createEngineBus<TestEvents>({
logger,
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE
listenerErrorMode: BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE
});
const afterFailure = vi.fn();
@ -234,15 +234,15 @@ describe('createEngineBus', () => {
expect(entries).toEqual([
expect.objectContaining({
level: 'error',
category: 'buss',
message: 'buss.listener.failed'
category: 'bus',
message: 'bus.listener.failed'
})
]);
});
it('throws an aggregate listener error in throw mode after collecting failures', () => {
const Bus = createEngineBus<TestEvents>({
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_THROW
listenerErrorMode: BUS_LISTENER_ERROR_MODE_THROW
});
const afterFailure = vi.fn(() => {
throw new Error('second');
@ -261,7 +261,7 @@ describe('createEngineBus', () => {
it('awaits async listeners and collects async failures', async () => {
const Bus = createEngineBus<TestEvents>({
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT
listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT
});
const calls: string[] = [];
@ -294,8 +294,8 @@ describe('createEngineBus', () => {
expect(entries).toEqual([
expect.objectContaining({
level: 'warn',
category: 'buss',
message: 'buss.listener.leak_warning'
category: 'bus',
message: 'bus.listener.leak_warning'
})
]);
});
@ -368,7 +368,7 @@ describe('createEngineBus', () => {
it('listener failures carry envelopeId, correlationId, and causationId', () => {
const Bus = createEngineBus<TestEvents>({
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT
listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT
});
Bus.on(EVENT_AUTH_SIGNED_OUT, () => {
throw new Error('boom');
@ -423,7 +423,7 @@ describe('createEngineBus', () => {
Bus.onAny(listener);
expect(Bus.listenerCount()).toBe(2);
Bus._clearForTesting(BUSS_EVENT_ALL);
Bus._clearForTesting(BUS_EVENT_ALL);
expect(Bus.listenerCount()).toBe(1);
Bus._clearForTesting();
expect(Bus.listenerCount()).toBe(0);

@ -1,80 +0,0 @@
import { CACH_MODULE } from '$svrs/cach';
export {
CACH_ERROR_MSG_DISPOSED_SUFFIX,
CACH_LOG_MESSAGE_ADAPTER_ERROR,
CACH_LOG_MESSAGE_BY_EVENT,
CACH_LOG_MESSAGE_DELETE,
CACH_LOG_MESSAGE_EVICTION,
CACH_LOG_MESSAGE_HIT,
CACH_LOG_MESSAGE_INVALIDATE,
CACH_LOG_MESSAGE_MISS,
CACH_LOG_MESSAGE_REFRESH_ERROR,
CACH_LOG_MESSAGE_REFRESH_START,
CACH_LOG_MESSAGE_REFRESH_SUCCESS,
CACH_LOG_MESSAGE_SCHEMA_MISMATCH,
CACH_LOG_MESSAGE_SCOPE_ERROR,
CACH_LOG_MESSAGE_SET,
CACH_LOG_MESSAGE_SINGLEFLIGHT_JOIN,
CACH_LOG_MESSAGE_STALE_HIT,
CACH_LOG_MESSAGE_STALE_IF_ERROR,
CACH_METHOD_CLEAR,
CACH_METHOD_EXPLAIN,
CACH_METHOD_GET,
CACH_METHOD_INVALIDATE,
CACH_METHOD_MUTATE,
CACH_METHOD_ON,
CACH_METHOD_QUERY,
CACH_METHOD_SET,
CACH_METHOD_STATS,
CACH_MODULE
} from '$svrs/cach';
export const CACH_ACTIVE_STATUS_IDLE = 'idle';
export const CACH_ACTIVE_STATUS_LOADING = 'loading';
export const CACH_ACTIVE_STATUS_SUCCESS = 'success';
export const CACH_ACTIVE_STATUS_STALE = 'stale';
export const CACH_ACTIVE_STATUS_REFRESHING = 'refreshing';
export const CACH_ACTIVE_STATUS_ERROR = 'error';
export const CACH_ACTIVE_STATUS_DEGRADED = 'degraded';
export const CACH_ACTIVE_STATUSES = [
CACH_ACTIVE_STATUS_IDLE,
CACH_ACTIVE_STATUS_LOADING,
CACH_ACTIVE_STATUS_SUCCESS,
CACH_ACTIVE_STATUS_STALE,
CACH_ACTIVE_STATUS_REFRESHING,
CACH_ACTIVE_STATUS_ERROR,
CACH_ACTIVE_STATUS_DEGRADED
] as const;
// Active cache entry event names. Scoped with `cach.entry.` so they do
// not collide with the engine-level `cach.*` events nor with any other
// artifact's events.
export const CACH_ACTIVE_ENTRY_EVENT_LOAD = 'cach.entry.load';
export const CACH_ACTIVE_ENTRY_EVENT_REFRESH = 'cach.entry.refresh';
export const CACH_ACTIVE_ENTRY_EVENT_INVALIDATE = 'cach.entry.invalidate';
export const CACH_ACTIVE_ENTRY_EVENT_SET = 'cach.entry.set';
export const CACH_ACTIVE_DIAGNOSTIC_EVENTS = {
OPERATION_FAILED: 'cach.active.operation_failed'
} as const;
export const CACH_ACTIVE_LOG_MESSAGE_OPERATION_FAILED = 'active cache operation failed';
export const CACH_ERROR_MSG_ACTIVE_ENTRY_DISPOSED =
`[${CACH_MODULE}] ActiveCacheEntry used after dispose().`;
export const CACH_AUTO_INVALIDATE_NONE = 'none';
export const CACH_AUTO_INVALIDATE_STANDARD = 'standard';
export const CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE = 'userIdentityChange';
export const CACH_AUTO_INVALIDATE_TENANT_SWITCHED = 'tenantSwitched';
export const CACH_METHOD_ENTRY = 'cach.entry';
export const CACH_ACTIVE_ENTRY_EVENTS = [
CACH_ACTIVE_ENTRY_EVENT_LOAD,
CACH_ACTIVE_ENTRY_EVENT_REFRESH,
CACH_ACTIVE_ENTRY_EVENT_INVALIDATE,
CACH_ACTIVE_ENTRY_EVENT_SET
] as const;

@ -1,17 +0,0 @@
import { CodeError } from '$libs/errs';
import { CACH_ERR_ACTIVE_ENTRY_DISPOSED } from '$libs/cach';
import { CACH_ERROR_MSG_ACTIVE_ENTRY_DISPOSED } from './consts.ts';
export { CachDisposedError, isCachDisposedError } from '$svrs/cach';
export class CachActiveEntryDisposedError extends CodeError {
constructor(message = CACH_ERROR_MSG_ACTIVE_ENTRY_DISPOSED) {
super(CACH_ERR_ACTIVE_ENTRY_DISPOSED, { message });
}
}
export function isCachActiveEntryDisposedError(
error: unknown
): error is CachActiveEntryDisposedError {
return error instanceof CachActiveEntryDisposedError;
}

@ -1,196 +0,0 @@
export { createActiveCache } from './active-cache.svelte.ts';
export { CACH_ERR, CACH_ERR_ACTIVE_ENTRY_DISPOSED, CACH_ERR_DISPOSED } from '$libs/cach';
export {
CACH_ACTIVE_ENTRY_EVENT_INVALIDATE,
CACH_ACTIVE_ENTRY_EVENT_LOAD,
CACH_ACTIVE_ENTRY_EVENT_REFRESH,
CACH_ACTIVE_ENTRY_EVENT_SET,
CACH_ACTIVE_ENTRY_EVENTS,
CACH_ACTIVE_DIAGNOSTIC_EVENTS,
CACH_ACTIVE_LOG_MESSAGE_OPERATION_FAILED,
CACH_ACTIVE_STATUS_DEGRADED,
CACH_ACTIVE_STATUS_ERROR,
CACH_ACTIVE_STATUS_IDLE,
CACH_ACTIVE_STATUS_LOADING,
CACH_ACTIVE_STATUS_REFRESHING,
CACH_ACTIVE_STATUS_STALE,
CACH_ACTIVE_STATUS_SUCCESS,
CACH_ACTIVE_STATUSES,
CACH_AUTO_INVALIDATE_NONE,
CACH_AUTO_INVALIDATE_STANDARD,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED,
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACH_ERROR_MSG_ACTIVE_ENTRY_DISPOSED,
CACH_ERROR_MSG_DISPOSED_SUFFIX,
CACH_LOG_MESSAGE_ADAPTER_ERROR,
CACH_LOG_MESSAGE_BY_EVENT,
CACH_LOG_MESSAGE_DELETE,
CACH_LOG_MESSAGE_EVICTION,
CACH_LOG_MESSAGE_HIT,
CACH_LOG_MESSAGE_INVALIDATE,
CACH_LOG_MESSAGE_MISS,
CACH_LOG_MESSAGE_REFRESH_ERROR,
CACH_LOG_MESSAGE_REFRESH_START,
CACH_LOG_MESSAGE_REFRESH_SUCCESS,
CACH_LOG_MESSAGE_SCHEMA_MISMATCH,
CACH_LOG_MESSAGE_SCOPE_ERROR,
CACH_LOG_MESSAGE_SET,
CACH_LOG_MESSAGE_SINGLEFLIGHT_JOIN,
CACH_LOG_MESSAGE_STALE_HIT,
CACH_LOG_MESSAGE_STALE_IF_ERROR,
CACH_METHOD_CLEAR,
CACH_METHOD_ENTRY,
CACH_METHOD_EXPLAIN,
CACH_METHOD_GET,
CACH_METHOD_INVALIDATE,
CACH_METHOD_MUTATE,
CACH_METHOD_ON,
CACH_METHOD_QUERY,
CACH_METHOD_SET,
CACH_METHOD_STATS,
CACH_MODULE
} from './consts.ts';
export { createActiveCacheDiagnostics, emitActiveCacheDiagnostic } from './diagnostics.ts';
export {
CachActiveEntryDisposedError,
CachDisposedError,
isCachActiveEntryDisposedError,
isCachDisposedError
} from './errors.ts';
export { disposedCacheMessage } from '$svrs/cach';
export type {
ActiveCacheDiagnosticEvent,
ActiveCacheDiagnosticMeta,
ActiveCacheDiagnostics,
ActiveCacheDiagnosticType
} from './diagnostics.ts';
export type {
ActiveCache,
ActiveCacheEntry,
ActiveCacheEntryEvent,
ActiveCacheEntryListener,
ActiveCacheEntryOptions,
ActiveCacheEntrySnapshot,
ActiveCacheEntryStatus,
ActiveCacheAutoInvalidateOn,
ActiveCacheAutoInvalidateTarget,
ActiveCacheOptions,
EngineCache,
EngineCacheOptions
} from './types.ts';
export {
CACH_ADAPTER_MEMORY,
CACH_ADAPTER_STORAGE,
CACH_DECISION_ACTION_DELETE_AND_FETCH,
CACH_DECISION_ACTION_FETCH,
CACH_DECISION_ACTION_SERVE,
CACH_DECISION_ACTION_SERVE_AND_REFRESH,
CACH_DECISION_ACTION_SKIP_CACHE,
CACH_DECISION_REASON_BYPASS_CACHE,
CACH_DECISION_REASON_CACHE_MISS,
CACH_DECISION_REASON_EXPIRED,
CACH_DECISION_REASON_FRESH,
CACH_DECISION_REASON_NO_STORE,
CACH_DECISION_REASON_PREFIX_EPOCH_CHANGED,
CACH_DECISION_REASON_SCHEMA_VERSION_MISMATCH,
CACH_DECISION_REASON_SCOPE_MISMATCH,
CACH_DECISION_REASON_STALE_WINDOW_VALID,
CACH_DECISION_REASON_TAG_EPOCH_CHANGED,
CACH_ENVELOPE_STATE_DEGRADED,
CACH_ENVELOPE_STATE_EXPIRED,
CACH_ENVELOPE_STATE_FRESH,
CACH_ENVELOPE_STATE_INVALIDATED,
CACH_ENVELOPE_STATE_STALE,
CACH_EVENT_ADAPTER_ERROR,
CACH_EVENT_ALL,
CACH_EVENT_DELETE,
CACH_EVENT_HIT,
CACH_EVENT_INVALIDATE,
CACH_EVENT_MISS,
CACH_EVENT_REFRESH_ERROR,
CACH_EVENT_REFRESH_START,
CACH_EVENT_REFRESH_SUCCESS,
CACH_EVENT_SCHEMA_MISMATCH,
CACH_EVENT_SCOPE_ERROR,
CACH_EVENT_SET,
CACH_EVENT_SINGLEFLIGHT_JOIN,
CACH_EVENT_STALE_HIT,
CACH_EVENT_STALE_IF_ERROR,
CACH_EVENT_EVICTION,
CACH_POLICY_CATALOG,
CACH_POLICY_IMMUTABLE,
CACH_POLICY_INTERACTIVE,
CACH_POLICY_PRIVATE_SESSION,
CACH_POLICY_REALTIME,
CACH_READ_MODE_BYPASS_CACHE,
CACH_READ_MODE_CACHE_FIRST,
CACH_READ_MODE_MUST_REVALIDATE,
CACH_READ_MODE_NO_STORE,
CACH_READ_MODE_STALE_WHILE_REVALIDATE,
CACH_SCOPE_ACTOR,
CACH_SCOPE_CUSTOM,
CACH_SCOPE_PERMISSION,
CACH_SCOPE_PUBLIC,
CACH_SCOPE_TENANT,
CacheKeyError,
CachePolicyError,
CacheScopeError,
createMapStorage,
defaultCachePolicies,
durationToMs,
isCacheKeyError,
isCachePolicyError,
isCacheScopeError,
memoryCacheAdapter,
normalizeKey,
normalizeKeyPrefixes,
normalizeTag,
normalizeTags,
resolveScope,
stableHash,
stableStringify,
storageCacheAdapter
} from '$svrs/cach';
export type {
CacheAdapter,
CacheAdapterSetOptions,
CacheClock,
CacheDecision,
CacheDecisionAction,
CacheDecisionReason,
CacheEnvelope,
CacheEnvelopeState,
CacheEvent,
CacheEventHandler,
CacheEventSelector,
CacheEventType,
CacheExplain,
CacheKey,
CachePolicy,
CacheReadMode,
CacheRuntime,
CacheRuntimeConfig,
CacheScopeInput,
CacheScopeMode,
CacheStats,
CacheTagLike,
CacheTagObject,
CustomCacheScope,
ExplainOptions,
GetOptions,
InvalidateOptions,
MemoryCacheAdapter,
MemoryCacheAdapterOptions,
MemoryCacheEvictReason,
MutateOptions,
QueryOptions,
ResolvedCachePolicy,
ResolvedCacheScope,
ResolvedScopeValues,
ScopeResolver,
SetOptions,
StorageCacheAdapterOptions,
StorageLike
} from '$svrs/cach';

@ -8,11 +8,11 @@ afectan.
El patrón sigue el resto de artefactos:
```ts
import { createEngineCache } from '$svrs/cach';
import { createEngineCache } from '$svrs/cache';
import { createActiveCache } from '$cach';
```
- `createEngineCache()` vive en `$svrs/cach` y es la API imperativa para server, servicios, repositorios, workers y tests.
- `createEngineCache()` vive en `$svrs/cache` y es la API imperativa para server, servicios, repositorios, workers y tests.
- `createActiveCache()` vive en `$cach` y añade estado reactivo para Svelte.
- `App.Cache` está siempre presente cuando usas `createActiveApp()`.
- El core puro vive en `$libs/cach` como `createCacheRuntime()`.
@ -32,11 +32,11 @@ import { createActiveCache } from '$cach';
## Uso Mínimo
```ts
import { createEngineCache, memoryCacheAdapter, CACH_POLICY_INTERACTIVE } from '$svrs/cach';
import { createEngineCache, memoryCacheAdapter, CACHE_POLICY_INTERACTIVE } from '$svrs/cache';
const Cache = createEngineCache({
adapter: memoryCacheAdapter(),
defaultPolicy: CACH_POLICY_INTERACTIVE
defaultPolicy: CACHE_POLICY_INTERACTIVE
});
const project = await Cache.query({
@ -178,10 +178,10 @@ Las políticas expresan intención:
```ts
import {
CACH_POLICY_INTERACTIVE,
CACH_POLICY_CATALOG,
CACH_POLICY_PRIVATE_SESSION
} from '$svrs/cach';
CACHE_POLICY_INTERACTIVE,
CACHE_POLICY_CATALOG,
CACHE_POLICY_PRIVATE_SESSION
} from '$svrs/cache';
```
Incluidas:
@ -195,7 +195,7 @@ Incluidas:
Puedes definir las tuyas:
```ts
import { createEngineCache } from '$svrs/cach';
import { createEngineCache } from '$svrs/cache';
const Cache = createEngineCache({
policies: {
@ -204,7 +204,7 @@ const Cache = createEngineCache({
staleFor: '2m',
staleIfErrorFor: '10m',
gcAfter: '30m',
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
@ -214,11 +214,11 @@ const Cache = createEngineCache({
## Modos
```ts
CACH_READ_MODE_CACHE_FIRST;
CACH_READ_MODE_STALE_WHILE_REVALIDATE;
CACH_READ_MODE_MUST_REVALIDATE;
CACH_READ_MODE_BYPASS_CACHE;
CACH_READ_MODE_NO_STORE;
CACHE_READ_MODE_CACHE_FIRST;
CACHE_READ_MODE_STALE_WHILE_REVALIDATE;
CACHE_READ_MODE_MUST_REVALIDATE;
CACHE_READ_MODE_BYPASS_CACHE;
CACHE_READ_MODE_NO_STORE;
```
- `cache-first`: sirve caché mientras esté fresh.
@ -336,7 +336,7 @@ const adapter = memoryCacheAdapter({
```
Soporta TTL, LRU aproximado, epochs e introspección.
Emite el warning constante `CACH_MEMORY_ADAPTER_PRODUCTION_WARNING` si se crea
Emite el warning constante `CACHE_MEMORY_ADAPTER_PRODUCTION_WARNING` si se crea
en runtime de producción. Es correcto para tests, demos, L1 por proceso y
desarrollo local; para producción multi-instancia usa un adapter intencional
o una composición tiered cuando esté disponible.
@ -363,7 +363,7 @@ El contrato está preparado para que Redis se integre sin dependencia dura.
El engine emite eventos:
```ts
Cache.on(CACH_EVENT_ALL, (event) => {
Cache.on(CACHE_EVENT_ALL, (event) => {
console.log(event.type, event.reason);
});
```

@ -1,16 +1,16 @@
import {
CACH_ACTIVE_ENTRY_EVENT_INVALIDATE,
CACH_ACTIVE_ENTRY_EVENT_LOAD,
CACH_ACTIVE_ENTRY_EVENT_REFRESH,
CACH_ACTIVE_ENTRY_EVENT_SET,
CACH_ACTIVE_STATUS_ERROR,
CACH_ACTIVE_STATUS_IDLE,
CACH_ACTIVE_STATUS_LOADING,
CACH_ACTIVE_STATUS_REFRESHING,
CACH_ACTIVE_STATUS_STALE,
CACH_ACTIVE_STATUS_SUCCESS
CACHE_ACTIVE_ENTRY_EVENT_INVALIDATE,
CACHE_ACTIVE_ENTRY_EVENT_LOAD,
CACHE_ACTIVE_ENTRY_EVENT_REFRESH,
CACHE_ACTIVE_ENTRY_EVENT_SET,
CACHE_ACTIVE_STATUS_ERROR,
CACHE_ACTIVE_STATUS_IDLE,
CACHE_ACTIVE_STATUS_LOADING,
CACHE_ACTIVE_STATUS_REFRESHING,
CACHE_ACTIVE_STATUS_STALE,
CACHE_ACTIVE_STATUS_SUCCESS
} from './consts.ts';
import { CachActiveEntryDisposedError } from './errors.ts';
import { CacheActiveEntryDisposedError } from './errors.ts';
import { normalizeActiveCacheError } from './active-cache-helpers.ts';
import type {
ActiveCache,
@ -21,7 +21,7 @@ import type {
ActiveCacheEntrySnapshot,
ActiveCacheEntryStatus
} from './types.ts';
import type { CacheClock } from '$libs/cach';
import type { CacheClock } from '$libs/cache';
export function createActiveCacheEntry<T>(
options: ActiveCacheEntryOptions<T>,
@ -31,7 +31,7 @@ export function createActiveCacheEntry<T>(
): ActiveCacheEntry<T> {
let dataCell = $state<T | undefined>(undefined);
let errorCell = $state<Error | null>(null);
let statusCell = $state<ActiveCacheEntryStatus>(CACH_ACTIVE_STATUS_IDLE);
let statusCell = $state<ActiveCacheEntryStatus>(CACHE_ACTIVE_STATUS_IDLE);
let updatedAtCell = $state<number | null>(null);
let eventCell = $state<ActiveCacheEntryEvent | null>(null);
let disposed = false;
@ -56,7 +56,7 @@ export function createActiveCacheEntry<T>(
function assertActive(): void {
if (disposed) {
throw new CachActiveEntryDisposedError();
throw new CacheActiveEntryDisposedError();
}
}
@ -64,7 +64,7 @@ export function createActiveCacheEntry<T>(
assertActive();
eventCell = event;
statusCell =
dataCell === undefined ? CACH_ACTIVE_STATUS_LOADING : CACH_ACTIVE_STATUS_REFRESHING;
dataCell === undefined ? CACHE_ACTIVE_STATUS_LOADING : CACHE_ACTIVE_STATUS_REFRESHING;
errorCell = null;
notify();
@ -72,12 +72,12 @@ export function createActiveCacheEntry<T>(
const value = await engine.query(options);
dataCell = value;
updatedAtCell = clock.now();
statusCell = CACH_ACTIVE_STATUS_SUCCESS;
statusCell = CACHE_ACTIVE_STATUS_SUCCESS;
notify();
return value;
} catch (error) {
errorCell = normalizeActiveCacheError(error);
statusCell = CACH_ACTIVE_STATUS_ERROR;
statusCell = CACHE_ACTIVE_STATUS_ERROR;
notify();
throw error;
}
@ -98,17 +98,17 @@ export function createActiveCacheEntry<T>(
},
get loading() {
return (
statusCell === CACH_ACTIVE_STATUS_LOADING || statusCell === CACH_ACTIVE_STATUS_REFRESHING
statusCell === CACHE_ACTIVE_STATUS_LOADING || statusCell === CACHE_ACTIVE_STATUS_REFRESHING
);
},
get updatedAt() {
return updatedAtCell;
},
load: () => runLoad(CACH_ACTIVE_ENTRY_EVENT_LOAD),
refresh: () => runLoad(CACH_ACTIVE_ENTRY_EVENT_REFRESH),
load: () => runLoad(CACHE_ACTIVE_ENTRY_EVENT_LOAD),
refresh: () => runLoad(CACHE_ACTIVE_ENTRY_EVENT_REFRESH),
async set(value, setOptions = {}) {
assertActive();
eventCell = CACH_ACTIVE_ENTRY_EVENT_SET;
eventCell = CACHE_ACTIVE_ENTRY_EVENT_SET;
await engine.set(options.key, value, {
policy: options.policy,
mode: options.mode,
@ -120,15 +120,15 @@ export function createActiveCacheEntry<T>(
});
dataCell = value;
errorCell = null;
statusCell = CACH_ACTIVE_STATUS_SUCCESS;
statusCell = CACHE_ACTIVE_STATUS_SUCCESS;
updatedAtCell = clock.now();
notify();
},
async invalidate() {
assertActive();
eventCell = CACH_ACTIVE_ENTRY_EVENT_INVALIDATE;
eventCell = CACHE_ACTIVE_ENTRY_EVENT_INVALIDATE;
await engine.invalidate({ key: options.key, scope: options.scope });
statusCell = dataCell === undefined ? CACH_ACTIVE_STATUS_IDLE : CACH_ACTIVE_STATUS_STALE;
statusCell = dataCell === undefined ? CACHE_ACTIVE_STATUS_IDLE : CACHE_ACTIVE_STATUS_STALE;
notify();
},
snapshot,

@ -1,32 +1,32 @@
import type { BusSubscription } from '$libs/buss';
import type { BusSubscription } from '$libs/bus';
import {
AAPP_EVENT_TENANT_SWITCHED,
AAPP_EVENT_USER_IDENTITY_CHANGED
} from '$libs/aapp/events';
import { CACH_EVENT_ALL, type CacheClock, type CacheEvent } from '$libs/cach';
import { createEngineCache } from '$svrs/cach';
APP_EVENT_TENANT_SWITCHED,
APP_EVENT_USER_IDENTITY_CHANGED
} from '$libs/active-app/events';
import { CACHE_EVENT_ALL, type CacheClock, type CacheEvent } from '$libs/cache';
import { createEngineCache } from '$svrs/cache';
import {
CACH_AUTO_INVALIDATE_NONE,
CACH_AUTO_INVALIDATE_STANDARD,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED,
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACH_ACTIVE_DIAGNOSTIC_EVENTS,
CACH_METHOD_CLEAR,
CACH_METHOD_ENTRY,
CACH_METHOD_EXPLAIN,
CACH_METHOD_GET,
CACH_METHOD_INVALIDATE,
CACH_METHOD_MUTATE,
CACH_METHOD_ON,
CACH_METHOD_QUERY,
CACH_METHOD_SET,
CACH_METHOD_STATS
CACHE_AUTO_INVALIDATE_NONE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACHE_ACTIVE_DIAGNOSTIC_EVENTS,
CACHE_METHOD_CLEAR,
CACHE_METHOD_ENTRY,
CACHE_METHOD_EXPLAIN,
CACHE_METHOD_GET,
CACHE_METHOD_INVALIDATE,
CACHE_METHOD_MUTATE,
CACHE_METHOD_ON,
CACHE_METHOD_QUERY,
CACHE_METHOD_SET,
CACHE_METHOD_STATS
} from './consts.ts';
import { disposedCacheMessage } from '$svrs/cach';
import { disposedCacheMessage } from '$svrs/cache';
import { createActiveCacheEntry } from './active-cache-entry.svelte.ts';
import { normalizeActiveCacheError } from './active-cache-helpers.ts';
import { createActiveCacheDiagnostics, emitActiveCacheDiagnostic } from './diagnostics.ts';
import { CachDisposedError } from './errors.ts';
import { CacheDisposedError } from './errors.ts';
import type {
ActiveCache,
ActiveCacheEntry,
@ -51,7 +51,7 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
(snapshot: ActiveCache['snapshot'] extends () => infer T ? T : never) => void
>();
const offEvents = engine.on(CACH_EVENT_ALL, (event) => {
const offEvents = engine.on(CACHE_EVENT_ALL, (event) => {
lastEventCell = event;
eventCountCell += 1;
notify();
@ -72,7 +72,7 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
return value;
} catch (error) {
lastErrorCell = normalizeActiveCacheError(error);
emitActiveCacheDiagnostic(diagnostics, CACH_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED, {
emitActiveCacheDiagnostic(diagnostics, CACHE_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED, {
method,
error
});
@ -84,16 +84,16 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
}
function entry<T>(entryOptions: ActiveCacheEntryOptions<T>): ActiveCacheEntry<T> {
ensureLive(CACH_METHOD_ENTRY);
ensureLive(CACHE_METHOD_ENTRY);
const built = createActiveCacheEntry(
entryOptions,
clock,
{
query: (queryOptions) => track(CACH_METHOD_QUERY, () => engine.query(queryOptions)),
query: (queryOptions) => track(CACHE_METHOD_QUERY, () => engine.query(queryOptions)),
set: (key, value, setOptions) =>
track(CACH_METHOD_SET, () => engine.set(key, value, setOptions)),
track(CACHE_METHOD_SET, () => engine.set(key, value, setOptions)),
invalidate: (invalidateOptions) =>
track(CACH_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions))
track(CACHE_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions))
},
() => {
entries.delete(built as ActiveCacheEntry<unknown>);
@ -104,7 +104,7 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
}
function ensureLive(method: string): void {
if (disposed) throw new CachDisposedError(disposedCacheMessage(method));
if (disposed) throw new CacheDisposedError(disposedCacheMessage(method));
}
function snapshot() {
@ -139,50 +139,50 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
return disposed;
},
query(queryOptions) {
ensureLive(CACH_METHOD_QUERY);
return track(CACH_METHOD_QUERY, () => engine.query(queryOptions));
ensureLive(CACHE_METHOD_QUERY);
return track(CACHE_METHOD_QUERY, () => engine.query(queryOptions));
},
get(key, getOptions) {
ensureLive(CACH_METHOD_GET);
return track(CACH_METHOD_GET, () => engine.get(key, getOptions));
ensureLive(CACHE_METHOD_GET);
return track(CACHE_METHOD_GET, () => engine.get(key, getOptions));
},
set(key, value, setOptions) {
ensureLive(CACH_METHOD_SET);
return track(CACH_METHOD_SET, () => engine.set(key, value, setOptions));
ensureLive(CACHE_METHOD_SET);
return track(CACHE_METHOD_SET, () => engine.set(key, value, setOptions));
},
invalidate(invalidateOptions) {
ensureLive(CACH_METHOD_INVALIDATE);
return track(CACH_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions));
ensureLive(CACHE_METHOD_INVALIDATE);
return track(CACHE_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions));
},
mutate(mutateOptions) {
ensureLive(CACH_METHOD_MUTATE);
return track(CACH_METHOD_MUTATE, () => engine.mutate(mutateOptions));
ensureLive(CACHE_METHOD_MUTATE);
return track(CACHE_METHOD_MUTATE, () => engine.mutate(mutateOptions));
},
explain(key, explainOptions) {
ensureLive(CACH_METHOD_EXPLAIN);
return track(CACH_METHOD_EXPLAIN, () => engine.explain(key, explainOptions));
ensureLive(CACHE_METHOD_EXPLAIN);
return track(CACHE_METHOD_EXPLAIN, () => engine.explain(key, explainOptions));
},
stats() {
ensureLive(CACH_METHOD_STATS);
ensureLive(CACHE_METHOD_STATS);
return engine.stats();
},
on(type, handler) {
ensureLive(CACH_METHOD_ON);
ensureLive(CACHE_METHOD_ON);
return engine.on(type, handler);
},
clear() {
ensureLive(CACH_METHOD_CLEAR);
return track(CACH_METHOD_CLEAR, () => engine.clear());
ensureLive(CACHE_METHOD_CLEAR);
return track(CACHE_METHOD_CLEAR, () => engine.clear());
},
entry,
snapshot,
clearError() {
ensureLive(CACH_METHOD_CLEAR);
ensureLive(CACHE_METHOD_CLEAR);
lastErrorCell = null;
notify();
},
onChange(listener) {
ensureLive(CACH_METHOD_ON);
ensureLive(CACHE_METHOD_ON);
listeners.add(listener);
listener(snapshot());
return () => {
@ -206,19 +206,19 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
if (bus === undefined) return [];
const targets = resolveAutoInvalidateTargets(options.autoInvalidateOn);
const subscriptions: BusSubscription[] = [];
if (targets.has(CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE)) {
if (targets.has(CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE)) {
subscriptions.push(
bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, () => {
bus.on(APP_EVENT_USER_IDENTITY_CHANGED, () => {
if (disposed) return;
void track(CACH_METHOD_CLEAR, () => engine.clear()).catch(() => undefined);
void track(CACHE_METHOD_CLEAR, () => engine.clear()).catch(() => undefined);
})
);
}
if (targets.has(CACH_AUTO_INVALIDATE_TENANT_SWITCHED)) {
if (targets.has(CACHE_AUTO_INVALIDATE_TENANT_SWITCHED)) {
subscriptions.push(
bus.on(AAPP_EVENT_TENANT_SWITCHED, () => {
bus.on(APP_EVENT_TENANT_SWITCHED, () => {
if (disposed) return;
void track(CACH_METHOD_CLEAR, () => engine.clear()).catch(() => undefined);
void track(CACHE_METHOD_CLEAR, () => engine.clear()).catch(() => undefined);
})
);
}
@ -233,13 +233,13 @@ const systemClock: CacheClock = {
function resolveAutoInvalidateTargets(
autoInvalidateOn: ActiveCacheOptions['autoInvalidateOn']
): ReadonlySet<string> {
if (autoInvalidateOn === undefined || autoInvalidateOn === CACH_AUTO_INVALIDATE_NONE) {
if (autoInvalidateOn === undefined || autoInvalidateOn === CACHE_AUTO_INVALIDATE_NONE) {
return new Set();
}
if (autoInvalidateOn === CACH_AUTO_INVALIDATE_STANDARD) {
if (autoInvalidateOn === CACHE_AUTO_INVALIDATE_STANDARD) {
return new Set([
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED
]);
}
return new Set(autoInvalidateOn);

@ -0,0 +1,80 @@
import { CACHE_MODULE } from '$svrs/cache';
export {
CACHE_ERROR_MSG_DISPOSED_SUFFIX,
CACHE_LOG_MESSAGE_ADAPTER_ERROR,
CACHE_LOG_MESSAGE_BY_EVENT,
CACHE_LOG_MESSAGE_DELETE,
CACHE_LOG_MESSAGE_EVICTION,
CACHE_LOG_MESSAGE_HIT,
CACHE_LOG_MESSAGE_INVALIDATE,
CACHE_LOG_MESSAGE_MISS,
CACHE_LOG_MESSAGE_REFRESH_ERROR,
CACHE_LOG_MESSAGE_REFRESH_START,
CACHE_LOG_MESSAGE_REFRESH_SUCCESS,
CACHE_LOG_MESSAGE_SCHEMA_MISMATCH,
CACHE_LOG_MESSAGE_SCOPE_ERROR,
CACHE_LOG_MESSAGE_SET,
CACHE_LOG_MESSAGE_SINGLEFLIGHT_JOIN,
CACHE_LOG_MESSAGE_STALE_HIT,
CACHE_LOG_MESSAGE_STALE_IF_ERROR,
CACHE_METHOD_CLEAR,
CACHE_METHOD_EXPLAIN,
CACHE_METHOD_GET,
CACHE_METHOD_INVALIDATE,
CACHE_METHOD_MUTATE,
CACHE_METHOD_ON,
CACHE_METHOD_QUERY,
CACHE_METHOD_SET,
CACHE_METHOD_STATS,
CACHE_MODULE
} from '$svrs/cache';
export const CACHE_ACTIVE_STATUS_IDLE = 'idle';
export const CACHE_ACTIVE_STATUS_LOADING = 'loading';
export const CACHE_ACTIVE_STATUS_SUCCESS = 'success';
export const CACHE_ACTIVE_STATUS_STALE = 'stale';
export const CACHE_ACTIVE_STATUS_REFRESHING = 'refreshing';
export const CACHE_ACTIVE_STATUS_ERROR = 'error';
export const CACHE_ACTIVE_STATUS_DEGRADED = 'degraded';
export const CACHE_ACTIVE_STATUSES = [
CACHE_ACTIVE_STATUS_IDLE,
CACHE_ACTIVE_STATUS_LOADING,
CACHE_ACTIVE_STATUS_SUCCESS,
CACHE_ACTIVE_STATUS_STALE,
CACHE_ACTIVE_STATUS_REFRESHING,
CACHE_ACTIVE_STATUS_ERROR,
CACHE_ACTIVE_STATUS_DEGRADED
] as const;
// Active cache entry event names. Scoped with `cache.entry.` so they do
// not collide with the engine-level `cach.*` events nor with any other
// artifact's events.
export const CACHE_ACTIVE_ENTRY_EVENT_LOAD = 'cache.entry.load';
export const CACHE_ACTIVE_ENTRY_EVENT_REFRESH = 'cache.entry.refresh';
export const CACHE_ACTIVE_ENTRY_EVENT_INVALIDATE = 'cache.entry.invalidate';
export const CACHE_ACTIVE_ENTRY_EVENT_SET = 'cache.entry.set';
export const CACHE_ACTIVE_DIAGNOSTIC_EVENTS = {
OPERATION_FAILED: 'cache.active.operation_failed'
} as const;
export const CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED = 'active cache operation failed';
export const CACHE_ERROR_MSG_ACTIVE_ENTRY_DISPOSED =
`[${CACHE_MODULE}] ActiveCacheEntry used after dispose().`;
export const CACHE_AUTO_INVALIDATE_NONE = 'none';
export const CACHE_AUTO_INVALIDATE_STANDARD = 'standard';
export const CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE = 'userIdentityChange';
export const CACHE_AUTO_INVALIDATE_TENANT_SWITCHED = 'tenantSwitched';
export const CACHE_METHOD_ENTRY = 'cache.entry';
export const CACHE_ACTIVE_ENTRY_EVENTS = [
CACHE_ACTIVE_ENTRY_EVENT_LOAD,
CACHE_ACTIVE_ENTRY_EVENT_REFRESH,
CACHE_ACTIVE_ENTRY_EVENT_INVALIDATE,
CACHE_ACTIVE_ENTRY_EVENT_SET
] as const;

@ -5,16 +5,16 @@ import {
type DiagnosticEvent,
type Diagnostics,
type Logger
} from '$libs/logr';
import type { CacheKey } from '$libs/cach';
} from '$libs/logger';
import type { CacheKey } from '$libs/cache';
import {
CACH_ACTIVE_DIAGNOSTIC_EVENTS,
CACH_ACTIVE_LOG_MESSAGE_OPERATION_FAILED,
CACH_MODULE
CACHE_ACTIVE_DIAGNOSTIC_EVENTS,
CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED,
CACHE_MODULE
} from './consts.ts';
export type ActiveCacheDiagnosticType =
(typeof CACH_ACTIVE_DIAGNOSTIC_EVENTS)[keyof typeof CACH_ACTIVE_DIAGNOSTIC_EVENTS];
(typeof CACHE_ACTIVE_DIAGNOSTIC_EVENTS)[keyof typeof CACHE_ACTIVE_DIAGNOSTIC_EVENTS];
export interface ActiveCacheDiagnosticMeta {
readonly method: string;
@ -29,16 +29,16 @@ export type ActiveCacheDiagnosticEvent = DiagnosticEvent<
export type ActiveCacheDiagnostics = Diagnostics<ActiveCacheDiagnosticEvent>;
const ACTIVE_CACHE_DIAGNOSTIC_LOGS: DiagnosticCatalog<ActiveCacheDiagnosticEvent> = {
[CACH_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED]: {
[CACHE_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED]: {
level: LogLevel.WARN,
message: CACH_ACTIVE_LOG_MESSAGE_OPERATION_FAILED
message: CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED
}
};
export function createActiveCacheDiagnostics(logger?: Logger): ActiveCacheDiagnostics {
return createCatalogDiagnostics({
logger,
defaultCategory: CACH_MODULE,
defaultCategory: CACHE_MODULE,
catalog: ACTIVE_CACHE_DIAGNOSTIC_LOGS
});
}
@ -49,7 +49,7 @@ export function emitActiveCacheDiagnostic(
meta: ActiveCacheDiagnosticMeta
): void {
diagnostics.emit({
artifact: CACH_MODULE,
artifact: CACHE_MODULE,
type,
meta
});

@ -0,0 +1,17 @@
import { CodeError } from '$libs/errs';
import { CACHE_ERR_ACTIVE_ENTRY_DISPOSED } from '$libs/cache';
import { CACHE_ERROR_MSG_ACTIVE_ENTRY_DISPOSED } from './consts.ts';
export { CacheDisposedError, isCacheDisposedError } from '$svrs/cache';
export class CacheActiveEntryDisposedError extends CodeError {
constructor(message = CACHE_ERROR_MSG_ACTIVE_ENTRY_DISPOSED) {
super(CACHE_ERR_ACTIVE_ENTRY_DISPOSED, { message });
}
}
export function isCacheActiveEntryDisposedError(
error: unknown
): error is CacheActiveEntryDisposedError {
return error instanceof CacheActiveEntryDisposedError;
}

@ -0,0 +1,196 @@
export { createActiveCache } from './active-cache.svelte.ts';
export { CACHE_ERR, CACHE_ERR_ACTIVE_ENTRY_DISPOSED, CACHE_ERR_DISPOSED } from '$libs/cache';
export {
CACHE_ACTIVE_ENTRY_EVENT_INVALIDATE,
CACHE_ACTIVE_ENTRY_EVENT_LOAD,
CACHE_ACTIVE_ENTRY_EVENT_REFRESH,
CACHE_ACTIVE_ENTRY_EVENT_SET,
CACHE_ACTIVE_ENTRY_EVENTS,
CACHE_ACTIVE_DIAGNOSTIC_EVENTS,
CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED,
CACHE_ACTIVE_STATUS_DEGRADED,
CACHE_ACTIVE_STATUS_ERROR,
CACHE_ACTIVE_STATUS_IDLE,
CACHE_ACTIVE_STATUS_LOADING,
CACHE_ACTIVE_STATUS_REFRESHING,
CACHE_ACTIVE_STATUS_STALE,
CACHE_ACTIVE_STATUS_SUCCESS,
CACHE_ACTIVE_STATUSES,
CACHE_AUTO_INVALIDATE_NONE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACHE_ERROR_MSG_ACTIVE_ENTRY_DISPOSED,
CACHE_ERROR_MSG_DISPOSED_SUFFIX,
CACHE_LOG_MESSAGE_ADAPTER_ERROR,
CACHE_LOG_MESSAGE_BY_EVENT,
CACHE_LOG_MESSAGE_DELETE,
CACHE_LOG_MESSAGE_EVICTION,
CACHE_LOG_MESSAGE_HIT,
CACHE_LOG_MESSAGE_INVALIDATE,
CACHE_LOG_MESSAGE_MISS,
CACHE_LOG_MESSAGE_REFRESH_ERROR,
CACHE_LOG_MESSAGE_REFRESH_START,
CACHE_LOG_MESSAGE_REFRESH_SUCCESS,
CACHE_LOG_MESSAGE_SCHEMA_MISMATCH,
CACHE_LOG_MESSAGE_SCOPE_ERROR,
CACHE_LOG_MESSAGE_SET,
CACHE_LOG_MESSAGE_SINGLEFLIGHT_JOIN,
CACHE_LOG_MESSAGE_STALE_HIT,
CACHE_LOG_MESSAGE_STALE_IF_ERROR,
CACHE_METHOD_CLEAR,
CACHE_METHOD_ENTRY,
CACHE_METHOD_EXPLAIN,
CACHE_METHOD_GET,
CACHE_METHOD_INVALIDATE,
CACHE_METHOD_MUTATE,
CACHE_METHOD_ON,
CACHE_METHOD_QUERY,
CACHE_METHOD_SET,
CACHE_METHOD_STATS,
CACHE_MODULE
} from './consts.ts';
export { createActiveCacheDiagnostics, emitActiveCacheDiagnostic } from './diagnostics.ts';
export {
CacheActiveEntryDisposedError,
CacheDisposedError,
isCacheActiveEntryDisposedError,
isCacheDisposedError
} from './errors.ts';
export { disposedCacheMessage } from '$svrs/cache';
export type {
ActiveCacheDiagnosticEvent,
ActiveCacheDiagnosticMeta,
ActiveCacheDiagnostics,
ActiveCacheDiagnosticType
} from './diagnostics.ts';
export type {
ActiveCache,
ActiveCacheEntry,
ActiveCacheEntryEvent,
ActiveCacheEntryListener,
ActiveCacheEntryOptions,
ActiveCacheEntrySnapshot,
ActiveCacheEntryStatus,
ActiveCacheAutoInvalidateOn,
ActiveCacheAutoInvalidateTarget,
ActiveCacheOptions,
EngineCache,
EngineCacheOptions
} from './types.ts';
export {
CACHE_ADAPTER_MEMORY,
CACHE_ADAPTER_STORAGE,
CACHE_DECISION_ACTION_DELETE_AND_FETCH,
CACHE_DECISION_ACTION_FETCH,
CACHE_DECISION_ACTION_SERVE,
CACHE_DECISION_ACTION_SERVE_AND_REFRESH,
CACHE_DECISION_ACTION_SKIP_CACHE,
CACHE_DECISION_REASON_BYPASS_CACHE,
CACHE_DECISION_REASON_CACHE_MISS,
CACHE_DECISION_REASON_EXPIRED,
CACHE_DECISION_REASON_FRESH,
CACHE_DECISION_REASON_NO_STORE,
CACHE_DECISION_REASON_PREFIX_EPOCH_CHANGED,
CACHE_DECISION_REASON_SCHEMA_VERSION_MISMATCH,
CACHE_DECISION_REASON_SCOPE_MISMATCH,
CACHE_DECISION_REASON_STALE_WINDOW_VALID,
CACHE_DECISION_REASON_TAG_EPOCH_CHANGED,
CACHE_ENVELOPE_STATE_DEGRADED,
CACHE_ENVELOPE_STATE_EXPIRED,
CACHE_ENVELOPE_STATE_FRESH,
CACHE_ENVELOPE_STATE_INVALIDATED,
CACHE_ENVELOPE_STATE_STALE,
CACHE_EVENT_ADAPTER_ERROR,
CACHE_EVENT_ALL,
CACHE_EVENT_DELETE,
CACHE_EVENT_HIT,
CACHE_EVENT_INVALIDATE,
CACHE_EVENT_MISS,
CACHE_EVENT_REFRESH_ERROR,
CACHE_EVENT_REFRESH_START,
CACHE_EVENT_REFRESH_SUCCESS,
CACHE_EVENT_SCHEMA_MISMATCH,
CACHE_EVENT_SCOPE_ERROR,
CACHE_EVENT_SET,
CACHE_EVENT_SINGLEFLIGHT_JOIN,
CACHE_EVENT_STALE_HIT,
CACHE_EVENT_STALE_IF_ERROR,
CACHE_EVENT_EVICTION,
CACHE_POLICY_CATALOG,
CACHE_POLICY_IMMUTABLE,
CACHE_POLICY_INTERACTIVE,
CACHE_POLICY_PRIVATE_SESSION,
CACHE_POLICY_REALTIME,
CACHE_READ_MODE_BYPASS_CACHE,
CACHE_READ_MODE_CACHE_FIRST,
CACHE_READ_MODE_MUST_REVALIDATE,
CACHE_READ_MODE_NO_STORE,
CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
CACHE_SCOPE_ACTOR,
CACHE_SCOPE_CUSTOM,
CACHE_SCOPE_PERMISSION,
CACHE_SCOPE_PUBLIC,
CACHE_SCOPE_TENANT,
CacheKeyError,
CachePolicyError,
CacheScopeError,
createMapStorage,
defaultCachePolicies,
durationToMs,
isCacheKeyError,
isCachePolicyError,
isCacheScopeError,
memoryCacheAdapter,
normalizeKey,
normalizeKeyPrefixes,
normalizeTag,
normalizeTags,
resolveScope,
stableHash,
stableStringify,
storageCacheAdapter
} from '$svrs/cache';
export type {
CacheAdapter,
CacheAdapterSetOptions,
CacheClock,
CacheDecision,
CacheDecisionAction,
CacheDecisionReason,
CacheEnvelope,
CacheEnvelopeState,
CacheEvent,
CacheEventHandler,
CacheEventSelector,
CacheEventType,
CacheExplain,
CacheKey,
CachePolicy,
CacheReadMode,
CacheRuntime,
CacheRuntimeConfig,
CacheScopeInput,
CacheScopeMode,
CacheStats,
CacheTagLike,
CacheTagObject,
CustomCacheScope,
ExplainOptions,
GetOptions,
InvalidateOptions,
MemoryCacheAdapter,
MemoryCacheAdapterOptions,
MemoryCacheEvictReason,
MutateOptions,
QueryOptions,
ResolvedCachePolicy,
ResolvedCacheScope,
ResolvedScopeValues,
ScopeResolver,
SetOptions,
StorageCacheAdapterOptions,
StorageLike
} from '$svrs/cache';

@ -1,12 +1,12 @@
import { describe, expect, it } from 'vitest';
import { createActiveApp } from '$aapp';
import { createActiveApp } from '$active-app';
import {
CACH_ACTIVE_STATUS_STALE,
CACH_ACTIVE_STATUS_SUCCESS,
CACH_POLICY_INTERACTIVE,
CACH_SCOPE_PUBLIC,
CACHE_ACTIVE_STATUS_STALE,
CACHE_ACTIVE_STATUS_SUCCESS,
CACHE_POLICY_INTERACTIVE,
CACHE_SCOPE_PUBLIC,
createActiveCache,
isCachActiveEntryDisposedError,
isCacheActiveEntryDisposedError,
memoryCacheAdapter,
type CacheClock
} from '../index.ts';
@ -30,16 +30,16 @@ describe('createActiveCache', () => {
});
const entry = Cache.entry({
key: ['dashboard'],
scope: CACH_SCOPE_PUBLIC,
scope: CACHE_SCOPE_PUBLIC,
fetcher: async () => ({ cards: 3 })
});
await entry.load();
expect(entry.data).toEqual({ cards: 3 });
expect(entry.status).toBe(CACH_ACTIVE_STATUS_SUCCESS);
expect(entry.status).toBe(CACHE_ACTIVE_STATUS_SUCCESS);
await entry.invalidate();
expect(entry.status).toBe(CACH_ACTIVE_STATUS_STALE);
expect(entry.status).toBe(CACHE_ACTIVE_STATUS_STALE);
expect(Cache.eventCount).toBeGreaterThan(0);
Cache.dispose();
@ -53,8 +53,8 @@ describe('createActiveCache', () => {
});
const entry = Cache.entry<number>({
key: ['project', 7],
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: 'Project:v1',
tags: [{ type: 'project', id: 7 }],
fetcher: async () => 1
@ -64,8 +64,8 @@ describe('createActiveCache', () => {
await expect(
Cache.get<number>(['project', 7], {
scope: CACH_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE,
scope: CACHE_SCOPE_PUBLIC,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: 'Project:v1'
})
).resolves.toBe(2);
@ -78,7 +78,7 @@ describe('createActiveCache', () => {
const Cache = createActiveCache();
const entry = Cache.entry({
key: ['entry-disposed'],
scope: CACH_SCOPE_PUBLIC,
scope: CACHE_SCOPE_PUBLIC,
fetcher: async () => 'never'
});
@ -87,7 +87,7 @@ describe('createActiveCache', () => {
try {
await entry.load();
} catch (error) {
expect(isCachActiveEntryDisposedError(error)).toBe(true);
expect(isCacheActiveEntryDisposedError(error)).toBe(true);
}
Cache.dispose();
@ -98,7 +98,7 @@ describe('createActiveCache', () => {
const Cache = createActiveCache();
const entry = Cache.entry({
key: ['parent-disposed-entry'],
scope: CACH_SCOPE_PUBLIC,
scope: CACHE_SCOPE_PUBLIC,
fetcher: async () => 'never'
});
@ -107,7 +107,7 @@ describe('createActiveCache', () => {
try {
await entry.load();
} catch (error) {
expect(isCachActiveEntryDisposedError(error)).toBe(true);
expect(isCacheActiveEntryDisposedError(error)).toBe(true);
}
});

@ -1,21 +1,21 @@
import type {
CACH_ACTIVE_ENTRY_EVENTS,
CACH_ACTIVE_STATUSES,
CACH_AUTO_INVALIDATE_NONE,
CACH_AUTO_INVALIDATE_STANDARD,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED,
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
CACHE_ACTIVE_ENTRY_EVENTS,
CACHE_ACTIVE_STATUSES,
CACHE_AUTO_INVALIDATE_NONE,
CACHE_AUTO_INVALIDATE_STANDARD,
CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
} from './consts.ts';
import type { AppEventBus } from '$libs/aapp/events';
import type { CacheClock, CacheEvent, CacheKey, QueryOptions, SetOptions } from '$libs/cach';
import type { EngineCache, EngineCacheOptions } from '$svrs/cach';
import type { AppEventBus } from '$libs/active-app/events';
import type { CacheClock, CacheEvent, CacheKey, QueryOptions, SetOptions } from '$libs/cache';
import type { EngineCache, EngineCacheOptions } from '$svrs/cache';
import type { ActiveChangeListener, ActiveEngine } from '$libs/active';
export type { EngineCache, EngineCacheOptions } from '$svrs/cach';
export type { EngineCache, EngineCacheOptions } from '$svrs/cache';
export type ActiveCacheEntryStatus = (typeof CACH_ACTIVE_STATUSES)[number];
export type ActiveCacheEntryStatus = (typeof CACHE_ACTIVE_STATUSES)[number];
export type ActiveCacheEntryEvent = (typeof CACH_ACTIVE_ENTRY_EVENTS)[number];
export type ActiveCacheEntryEvent = (typeof CACHE_ACTIVE_ENTRY_EVENTS)[number];
export interface ActiveCacheEntrySnapshot<T> {
readonly key: CacheKey;
@ -49,12 +49,12 @@ export interface ActiveCacheEntryOptions<T> extends QueryOptions<T> {
}
export type ActiveCacheAutoInvalidateTarget =
| typeof CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
| typeof CACH_AUTO_INVALIDATE_TENANT_SWITCHED;
| typeof CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
| typeof CACHE_AUTO_INVALIDATE_TENANT_SWITCHED;
export type ActiveCacheAutoInvalidateOn =
| typeof CACH_AUTO_INVALIDATE_NONE
| typeof CACH_AUTO_INVALIDATE_STANDARD
| typeof CACHE_AUTO_INVALIDATE_NONE
| typeof CACHE_AUTO_INVALIDATE_STANDARD
| readonly ActiveCacheAutoInvalidateTarget[];
export interface ActiveCacheOptions extends EngineCacheOptions {

@ -1,59 +0,0 @@
import {
CONN_ACK_REASON_CLOSED,
CONN_ACK_REASON_REJECTED,
CONN_ACK_REASON_TIMEOUT,
CONN_AUTH_REASON_CLOSED,
CONN_AUTH_REASON_NO_PROVIDER,
CONN_AUTH_REASON_REJECTED,
CONN_AUTH_REASON_TIMEOUT,
CONN_AUTH_REASON_TRANSPORT_ERROR,
CONN_FRAME_TYPE_AUTH
} from './consts.ts';
import type {
ConnectionAckResult,
ConnectionAuthPayload,
ConnectionAuthResult,
ConnectionOptions
} from './types.ts';
export type ConnectionAuthProvider = () =>
| ConnectionAuthPayload
| null
| Promise<ConnectionAuthPayload | null>;
export interface ResolvedConnectionAuth {
readonly provider: ConnectionAuthProvider | null;
readonly authType: string;
readonly timeoutMs?: number;
}
export function resolveConnectionAuth(auth: ConnectionOptions['auth']): ResolvedConnectionAuth {
if (typeof auth === 'function') {
return { provider: auth, authType: CONN_FRAME_TYPE_AUTH };
}
return {
provider: auth?.getAuth ?? null,
authType: auth?.authType ?? CONN_FRAME_TYPE_AUTH,
timeoutMs: auth?.timeoutMs
};
}
export function missingConnectionAuthProvider(): ConnectionAuthResult {
return { ok: false, reason: CONN_AUTH_REASON_NO_PROVIDER };
}
export function mapConnectionAckToAuthResult(
result: ConnectionAckResult<unknown>
): ConnectionAuthResult {
if (result.ok) return { ok: true };
if (result.reason === CONN_ACK_REASON_TIMEOUT) {
return { ok: false, reason: CONN_AUTH_REASON_TIMEOUT, error: result.error };
}
if (result.reason === CONN_ACK_REASON_CLOSED) {
return { ok: false, reason: CONN_AUTH_REASON_CLOSED, error: result.error };
}
if (result.reason === CONN_ACK_REASON_REJECTED) {
return { ok: false, reason: CONN_AUTH_REASON_REJECTED, error: result.error };
}
return { ok: false, reason: CONN_AUTH_REASON_TRANSPORT_ERROR, error: result.error };
}

@ -1,251 +0,0 @@
import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs';
export const CONN_MODULE = 'conn';
export const CONN_LOGGER_SCOPE_SEPARATOR = ':';
export const CONN_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'conn.auth_failed',
BROWSER_RECONNECT: 'conn.browser_reconnect',
CONNECT_FAILED: 'conn.connect_failed',
FRAME_DECODE_FAILED: 'conn.frame_decode_failed',
FRAME_ENCODE_FAILED: 'conn.frame_encode_failed',
HEARTBEAT_TIMEOUT: 'conn.heartbeat_timeout',
LISTENER_THREW: 'conn.listener_threw',
REAUTH_FAILED: 'conn.reauth_failed',
RECONNECT_EXHAUSTED: 'conn.reconnect_exhausted',
SEND_FAILED: 'conn.send_failed',
SESS_CHANGED: 'conn.session_changed',
SESS_EXPIRED: 'conn.session_expired',
SESS_REFRESHED: 'conn.session_refreshed',
SESS_REVOKED: 'conn.session_revoked',
TRANSPORT_ERROR: 'conn.transport_error'
} as const;
export const CONN_STATE_IDLE = 'idle';
export const CONN_STATE_CONNECTING = 'connecting';
export const CONN_STATE_OPEN = 'open';
export const CONN_STATE_RECONNECTING = 'reconnecting';
export const CONN_STATE_CLOSING = 'closing';
export const CONN_STATE_CLOSED = 'closed';
export const CONN_STATE_FAILED = 'failed';
export const CONN_STATES = [
CONN_STATE_IDLE,
CONN_STATE_CONNECTING,
CONN_STATE_OPEN,
CONN_STATE_RECONNECTING,
CONN_STATE_CLOSING,
CONN_STATE_CLOSED,
CONN_STATE_FAILED
] as const;
export const CONN_CHANNEL_STATE_IDLE = 'idle';
export const CONN_CHANNEL_STATE_JOINING = 'joining';
export const CONN_CHANNEL_STATE_JOINED = 'joined';
export const CONN_CHANNEL_STATE_LEAVING = 'leaving';
export const CONN_CHANNEL_STATE_LEFT = 'left';
export const CONN_CHANNEL_STATE_FAILED = 'failed';
export const CONN_TRANSPORT_STATE_IDLE = 'idle';
export const CONN_TRANSPORT_STATE_OPENING = 'opening';
export const CONN_TRANSPORT_STATE_OPEN = 'open';
export const CONN_TRANSPORT_STATE_CLOSING = 'closing';
export const CONN_TRANSPORT_STATE_CLOSED = 'closed';
export const CONN_TRANSPORT_STATES = [
CONN_TRANSPORT_STATE_IDLE,
CONN_TRANSPORT_STATE_OPENING,
CONN_TRANSPORT_STATE_OPEN,
CONN_TRANSPORT_STATE_CLOSING,
CONN_TRANSPORT_STATE_CLOSED
] as const;
export const CONN_TRANSPORT_KIND_WEBSOCKET = 'websocket';
export const CONN_TRANSPORT_KIND_MOCK = 'mock';
export const CONN_FRAME_KEY_ID = 'id';
export const CONN_FRAME_KEY_TYPE = 'type';
export const CONN_FRAME_KEY_TOPIC = 'topic';
export const CONN_FRAME_KEY_PAYLOAD = 'payload';
export const CONN_FRAME_KEY_TS = 'ts';
export const CONN_FRAME_KEY_ACK = 'ack';
export const CONN_FRAME_KEY_REPLY_TO = 'replyTo';
export const CONN_FRAME_KEY_ERROR = 'error';
export const CONN_DEFAULT_ACK_TIMEOUT_MS = 10_000;
export const CONN_DEFAULT_RECONNECT_MIN_DELAY_MS = 500;
export const CONN_DEFAULT_RECONNECT_MAX_DELAY_MS = 15_000;
export const CONN_DEFAULT_RECONNECT_FACTOR = 1.8;
export const CONN_DEFAULT_RECONNECT_JITTER_MS = 500;
export const CONN_DEFAULT_HEARTBEAT_INTERVAL_MS = 25_000;
export const CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS = 10_000;
export const CONN_DEFAULT_BUFFER_MAX_MESSAGES = 100;
export const CONN_DEFAULT_BUFFER_MAX_BYTES = 1_000_000;
export const CONN_DEFAULT_RECONNECT_ENABLED = true;
export const CONN_DEFAULT_RECONNECT_ON_VISIBLE = true;
export const CONN_DEFAULT_RECONNECT_ON_ONLINE = true;
export const CONN_DEFAULT_HEARTBEAT_ENABLED = true;
export const CONN_BUFFER_POLICY_BUFFER = 'buffer';
export const CONN_BUFFER_POLICY_DROP = 'drop';
export const CONN_BUFFER_POLICY_FAIL = 'fail';
export const CONN_SEND_REASON_CLOSED = 'closed';
export const CONN_SEND_REASON_SEND_NOT_SUPPORTED = 'send_not_supported';
export const CONN_SEND_REASON_BUFFER_FULL = 'buffer_full';
export const CONN_SEND_REASON_INVALID_FRAME = 'invalid_frame';
export const CONN_SEND_REASON_SERIALIZE_FAILED = 'serialize_failed';
export const CONN_SEND_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONN_ACK_REASON_TIMEOUT = 'timeout';
export const CONN_ACK_REASON_CLOSED = 'closed';
export const CONN_ACK_REASON_REJECTED = 'rejected';
export const CONN_ACK_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONN_ACK_REASON_INVALID_REPLY = 'invalid_reply';
export const CONN_AUTH_REASON_NO_PROVIDER = 'no_auth_provider';
export const CONN_AUTH_REASON_CLOSED = 'closed';
export const CONN_AUTH_REASON_REJECTED = 'rejected';
export const CONN_AUTH_REASON_TIMEOUT = 'timeout';
export const CONN_AUTH_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONN_CONNECT_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONN_CONNECT_REASON_AUTH_FAILED = 'auth_failed';
export const CONN_CONNECT_REASON_DISPOSED = 'disposed';
export const CONN_CONNECT_REASON_RECONNECT_EXHAUSTED = 'reconnect_exhausted';
export const CONN_CHANNEL_JOIN_REASON_CLOSED = 'closed';
export const CONN_CHANNEL_JOIN_REASON_REJECTED = 'rejected';
export const CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR = 'transport_error';
// Connection event names. Scoped with the `conn.` prefix to disambiguate
// from cache, session or any other emitter when logs are aggregated.
export const CONN_EVENT_STATE = 'conn.state';
export const CONN_EVENT_MESSAGE = 'conn.message';
export const CONN_EVENT_CHANNELS = 'conn.channels';
export const CONN_EVENT_DISPOSED = 'conn.disposed';
export const CONN_METHOD_CONNECT = 'connect';
export const CONN_METHOD_DISCONNECT = 'disconnect';
export const CONN_METHOD_RECONNECT = 'reconnect';
export const CONN_METHOD_SEND = 'send';
export const CONN_METHOD_REQUEST = 'request';
export const CONN_METHOD_CREATE_CONNECTION = 'createConnection';
export const CONN_METHOD_CONNECTION = 'connection';
export const CONN_METHOD_OPEN_CONNECTION = 'openConnection';
export const CONN_METHOD_CLOSE_CONNECTION = 'closeConnection';
export const CONN_METHOD_RECONNECT_CONNECTION = 'reconnectConnection';
export const CONN_METHOD_OPEN_ALL = 'openAll';
export const CONN_METHOD_CLOSE_ALL = 'closeAll';
export const CONN_METHOD_RECONNECT_ALL = 'reconnectAll';
export const CONN_METHOD_REAUTHENTICATE_ALL = 'reauthenticateAll';
export const CONN_METHOD_REAUTHENTICATE = 'reauthenticate';
export const CONN_METHOD_JOIN = 'join';
export const CONN_METHOD_LEAVE = 'leave';
export const CONN_AUTO_REAUTH_NONE = 'none';
export const CONN_AUTO_REAUTH_STANDARD = 'standard';
export const CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE = 'userIdentityChange';
export const CONN_FRAME_TYPE_AUTH = 'conn.auth';
export const CONN_FRAME_TYPE_JOIN = 'conn.join';
export const CONN_FRAME_TYPE_LEAVE = 'conn.leave';
export const CONN_FRAME_TYPE_PING = 'conn.ping';
export const CONN_FRAME_TYPE_PONG = 'conn.pong';
export const CONN_ID_PREFIX = 'conn';
export const CONN_ID_SEPARATOR = '-';
export const CONN_CLOSE_REASON_DISCONNECT = 'disconnect';
export const CONN_CLOSE_REASON_RECONNECT = 'reconnect';
export const CONN_CLOSE_REASON_DISPOSE = 'dispose';
export const CONN_CLOSE_REASON_HEARTBEAT_TIMEOUT = 'heartbeat_timeout';
export const CONN_CLOSE_REASON_SESSION_EXPIRED = 'session_expired';
export const CONN_CLOSE_REASON_AUTH_FAILED = 'auth_failed';
export const CONN_TIMR_KEY_RECONNECT = 'reconnect';
export const CONN_TIMR_KEY_HEARTBEAT = 'heartbeat';
export const CONN_TIMR_KEY_HEARTBEAT_TIMEOUT = 'heartbeat-timeout';
export const CONN_TIMR_KEY_ACK = 'ack';
export const CONN_BROWSER_EVENT_OPEN = 'open';
export const CONN_BROWSER_EVENT_MESSAGE = 'message';
export const CONN_BROWSER_EVENT_CLOSE = 'close';
export const CONN_BROWSER_EVENT_ERROR = 'error';
export const CONN_BROWSER_EVENT_ONLINE = 'online';
export const CONN_BROWSER_EVENT_VISIBILITY_CHANGE = 'visibilitychange';
export const CONN_DOCUMENT_VISIBILITY_VISIBLE = 'visible';
export const CONN_WEBSOCKET_BINARY_TYPE_ARRAYBUFFER = 'arraybuffer';
export const CONN_WEBSOCKET_READY_STATE_CONNECTING = 0;
export const CONN_WEBSOCKET_READY_STATE_OPEN = 1;
export const CONN_WEBSOCKET_READY_STATE_CLOSING = 2;
export const CONN_WEBSOCKET_READY_STATE_CLOSED = 3;
export const CONN_WEBSOCKET_CLOSE_CODE_NORMAL = 1000;
export const CONN_WEBSOCKET_CLOSE_CODE_ABNORMAL = 1006;
export const CONN_MOCK_TRANSPORT_DEFAULT_CLOSE_CODE = CONN_WEBSOCKET_CLOSE_CODE_NORMAL;
export const CONN_MOCK_TRANSPORT_DEFAULT_CLOSE_CLEAN = true;
// Mirror of sess lifecycle discriminants. Kept in sync with the values
// declared in `arts/sess/consts.ts` so `session-wiring.ts` can compare
// `change.event` strings without importing arts/sess directly.
//
// Long term this should be replaced by a typed union imported from
// `$sess`; for now the values are duplicated and must move together.
export const CONN_SESS_EVENT_REFRESHED = 'sess.lifecycle.refreshed';
export const CONN_SESS_EVENT_ADOPTED = 'sess.lifecycle.adopted';
export const CONN_SESS_EVENT_ADOPTED_SERVER = 'sess.lifecycle.adopted_server';
export const CONN_SESS_EVENT_EXTERNAL_CHANGED = 'sess.lifecycle.external_changed';
export const CONN_SESS_EVENT_EXPIRED = 'sess.lifecycle.expired';
export const CONN_SESS_EVENT_REVOKED = 'sess.lifecycle.revoked';
export const CONN_LOG_MSG_LISTENER_THREW_PREFIX = 'Listener threw on ';
export const CONN_LOG_MSG_TRANSPORT_ERROR = 'transport error';
export const CONN_LOG_MSG_RECONNECT_EXHAUSTED = 'reconnect attempts exhausted';
export const CONN_LOG_MSG_CONNECT_FAILED = 'connect failed';
export const CONN_LOG_MSG_SEND_FAILED = 'send failed';
export const CONN_LOG_MSG_FRAME_DECODE_FAILED = 'frame decode failed';
export const CONN_LOG_MSG_FRAME_ENCODE_FAILED = 'frame encode failed';
export const CONN_LOG_MSG_AUTH_FAILED = 'auth failed';
export const CONN_LOG_MSG_REAUTH_FAILED = 'reauthentication failed';
export const CONN_LOG_MSG_HEARTBEAT_TIMEOUT = 'heartbeat timeout';
export const CONN_LOG_MSG_SESSION_CHANGED = 'session changed; reauthenticating connection';
export const CONN_LOG_MSG_SESSION_REVOKED = 'session revoked; disconnecting connection';
export const CONN_LOG_MSG_SESSION_EXPIRED = 'session expired; disconnecting connection';
export const CONN_LOG_MSG_SESSION_REFRESHED = 'session refreshed; reauthenticating connection';
export const CONN_LOG_MSG_BROWSER_RECONNECT = 'browser lifecycle reconnect requested';
export const CONN_ERROR_PREFIX = `[${CONN_MODULE}] `;
// ── Error codes ────────────────────────────────────────────────────────
export const CONN_ERR: ModuleSeed = moduleSeed(CONN_MODULE);
export const CONN_ERR_DISPOSED: ErrCode = errCode(CONN_ERR, 'disposed');
export const CONN_ERR_CONNECTION_ALREADY_EXISTS: ErrCode = errCode(CONN_ERR, 'connection_already_exists');
export const CONN_ERR_CONNECTION_NOT_FOUND: ErrCode = errCode(CONN_ERR, 'connection_not_found');
export const CONN_ERR_INVALID_CONNECTION_NAME: ErrCode = errCode(CONN_ERR, 'invalid_connection_name');
export const CONN_ERR_INVALID_FRAME: ErrCode = errCode(CONN_ERR, 'invalid_frame');
export const CONN_ERR_CHANNEL_ALREADY_EXISTS: ErrCode = errCode(CONN_ERR, 'channel_already_exists');
export const CONN_ERR_CHANNEL_NOT_FOUND: ErrCode = errCode(CONN_ERR, 'channel_not_found');
export const CONN_ERR_WEBSOCKET_UNAVAILABLE: ErrCode = errCode(CONN_ERR, 'websocket_unavailable');
export const CONN_ERROR_MSG_DISPOSED_SUFFIX = '() called on a disposed connection engine';
export const CONN_ERROR_MSG_ALREADY_EXISTS_PREFIX = 'connection already exists: ';
export const CONN_ERROR_MSG_NOT_FOUND_PREFIX = 'connection not found: ';
export const CONN_ERROR_MSG_INVALID_NAME_PREFIX = 'invalid connection name: ';
export const CONN_ERROR_MSG_INVALID_FRAME = 'invalid connection frame';
export const CONN_ERROR_MSG_FRAME_MUST_BE_OBJECT = 'Connection frame must be an object';
export const CONN_ERROR_MSG_FRAME_TYPE_MUST_BE_STRING =
'Connection frame type must be a non-empty string';
export const CONN_ERROR_MSG_FRAME_ID_MUST_BE_STRING = 'Connection frame id must be a string';
export const CONN_ERROR_MSG_FRAME_TOPIC_MUST_BE_STRING = 'Connection frame topic must be a string';
export const CONN_ERROR_MSG_FRAME_REPLY_TO_MUST_BE_STRING = 'Connection frame replyTo must be a string';
export const CONN_ERROR_MSG_FRAME_ACK_MUST_BE_BOOLEAN = 'Connection frame ack must be a boolean';
export const CONN_ERROR_MSG_FRAME_TS_MUST_BE_NUMBER = 'Connection frame ts must be a number';
export const CONN_ERROR_MSG_FRAME_ERROR_MUST_BE_OBJECT = 'Connection frame error must be an object';
export const CONN_ERROR_MSG_JSON_DECODE_FAILED = 'Connection JSON payload is invalid';
export const CONN_ERROR_MSG_WEBSOCKET_UNAVAILABLE = 'WebSocket is not available in this runtime';
export const CONN_ERROR_MSG_CHANNEL_ALREADY_EXISTS_PREFIX = 'channel already exists: ';
export const CONN_ERROR_MSG_CHANNEL_NOT_FOUND_PREFIX = 'channel not found: ';

@ -1,141 +0,0 @@
import {
LogLevel,
createCatalogDiagnostics,
type DiagnosticCatalog,
type DiagnosticEvent,
type Diagnostics
} from '$libs/logr';
import {
CONN_DIAGNOSTIC_EVENTS,
CONN_EVENT_MESSAGE,
CONN_MODULE,
CONN_LOG_MSG_AUTH_FAILED,
CONN_LOG_MSG_BROWSER_RECONNECT,
CONN_LOG_MSG_CONNECT_FAILED,
CONN_LOG_MSG_FRAME_DECODE_FAILED,
CONN_LOG_MSG_FRAME_ENCODE_FAILED,
CONN_LOG_MSG_HEARTBEAT_TIMEOUT,
CONN_LOG_MSG_REAUTH_FAILED,
CONN_LOG_MSG_RECONNECT_EXHAUSTED,
CONN_LOG_MSG_SEND_FAILED,
CONN_LOG_MSG_SESSION_CHANGED,
CONN_LOG_MSG_SESSION_EXPIRED,
CONN_LOG_MSG_SESSION_REFRESHED,
CONN_LOG_MSG_SESSION_REVOKED,
CONN_LOG_MSG_TRANSPORT_ERROR
} from './consts.ts';
import { listenerThrewMessage } from './helpers.ts';
import type { Logger } from '$libs/logr';
export type ConnectionDiagnosticType =
(typeof CONN_DIAGNOSTIC_EVENTS)[keyof typeof CONN_DIAGNOSTIC_EVENTS];
export type ConnectionDiagnosticEvent = DiagnosticEvent<ConnectionDiagnosticType>;
export type ConnectionDiagnostics = Diagnostics<ConnectionDiagnosticEvent>;
const CONN_DIAGNOSTIC_LOGS: DiagnosticCatalog<ConnectionDiagnosticEvent> = {
[CONN_DIAGNOSTIC_EVENTS.AUTH_FAILED]: {
level: LogLevel.WARN,
message: CONN_LOG_MSG_AUTH_FAILED
},
[CONN_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT]: {
level: LogLevel.DEBUG,
message: CONN_LOG_MSG_BROWSER_RECONNECT
},
[CONN_DIAGNOSTIC_EVENTS.CONNECT_FAILED]: {
level: LogLevel.ERROR,
message: CONN_LOG_MSG_CONNECT_FAILED
},
[CONN_DIAGNOSTIC_EVENTS.FRAME_DECODE_FAILED]: {
level: LogLevel.ERROR,
message: CONN_LOG_MSG_FRAME_DECODE_FAILED
},
[CONN_DIAGNOSTIC_EVENTS.FRAME_ENCODE_FAILED]: {
level: LogLevel.ERROR,
message: CONN_LOG_MSG_FRAME_ENCODE_FAILED
},
[CONN_DIAGNOSTIC_EVENTS.HEARTBEAT_TIMEOUT]: {
level: LogLevel.WARN,
message: CONN_LOG_MSG_HEARTBEAT_TIMEOUT
},
[CONN_DIAGNOSTIC_EVENTS.LISTENER_THREW]: (event) => ({
level: LogLevel.ERROR,
message: listenerThrewMessage(listenerEventName(event.meta))
}),
[CONN_DIAGNOSTIC_EVENTS.REAUTH_FAILED]: {
level: LogLevel.WARN,
message: CONN_LOG_MSG_REAUTH_FAILED
},
[CONN_DIAGNOSTIC_EVENTS.RECONNECT_EXHAUSTED]: {
level: LogLevel.WARN,
message: CONN_LOG_MSG_RECONNECT_EXHAUSTED
},
[CONN_DIAGNOSTIC_EVENTS.SEND_FAILED]: {
level: LogLevel.ERROR,
message: CONN_LOG_MSG_SEND_FAILED
},
[CONN_DIAGNOSTIC_EVENTS.SESS_CHANGED]: {
level: LogLevel.DEBUG,
message: CONN_LOG_MSG_SESSION_CHANGED
},
[CONN_DIAGNOSTIC_EVENTS.SESS_EXPIRED]: {
level: LogLevel.WARN,
message: CONN_LOG_MSG_SESSION_EXPIRED
},
[CONN_DIAGNOSTIC_EVENTS.SESS_REFRESHED]: {
level: LogLevel.DEBUG,
message: CONN_LOG_MSG_SESSION_REFRESHED
},
[CONN_DIAGNOSTIC_EVENTS.SESS_REVOKED]: {
level: LogLevel.WARN,
message: CONN_LOG_MSG_SESSION_REVOKED
},
[CONN_DIAGNOSTIC_EVENTS.TRANSPORT_ERROR]: {
level: LogLevel.ERROR,
message: CONN_LOG_MSG_TRANSPORT_ERROR
}
};
function listenerEventName(meta: unknown): string {
if (typeof meta !== 'object' || meta === null) return CONN_EVENT_MESSAGE;
if (!('event' in meta)) return CONN_EVENT_MESSAGE;
const event = meta.event;
return typeof event === 'string' && event.length > 0 ? event : CONN_EVENT_MESSAGE;
}
export function createConnectionDiagnostics(input: {
readonly logger?: Logger;
readonly scope: string;
}): ConnectionDiagnostics {
return createCatalogDiagnostics({
logger: input.logger,
defaultCategory: input.scope,
catalog: CONN_DIAGNOSTIC_LOGS
});
}
export function emitConnectionDiagnostic(
diagnostics: ConnectionDiagnostics,
type: ConnectionDiagnosticType,
meta?: unknown
): void {
diagnostics.emit({
artifact: CONN_MODULE,
type,
meta
});
}
export function emitScopedConnectionDiagnostic(
diagnostics: ConnectionDiagnostics,
scope: string,
type: ConnectionDiagnosticType,
meta?: unknown
): void {
diagnostics.emit({
artifact: CONN_MODULE,
scope,
type,
meta
});
}

@ -1,111 +0,0 @@
import { CodeError } from '$libs/errs';
import {
CONN_ERR_CHANNEL_ALREADY_EXISTS,
CONN_ERR_CHANNEL_NOT_FOUND,
CONN_ERR_CONNECTION_ALREADY_EXISTS,
CONN_ERR_CONNECTION_NOT_FOUND,
CONN_ERR_DISPOSED,
CONN_ERR_INVALID_CONNECTION_NAME,
CONN_ERR_INVALID_FRAME,
CONN_ERR_WEBSOCKET_UNAVAILABLE
} from './consts.ts';
export class ConnDisposedError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONN_ERR_DISPOSED, { message, cause: options?.cause });
}
}
export class ConnConnectionAlreadyExistsError extends CodeError {
readonly connection: string;
constructor(message: string, connection: string) {
super(CONN_ERR_CONNECTION_ALREADY_EXISTS, { message });
this.connection = connection;
}
}
export class ConnConnectionNotFoundError extends CodeError {
readonly connection: string;
constructor(message: string, connection: string) {
super(CONN_ERR_CONNECTION_NOT_FOUND, { message });
this.connection = connection;
}
}
export class ConnInvalidConnectionNameError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONN_ERR_INVALID_CONNECTION_NAME, { message, cause: options?.cause });
}
}
export class ConnInvalidFrameError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONN_ERR_INVALID_FRAME, { message, cause: options?.cause });
}
}
export class ConnChannelAlreadyExistsError extends CodeError {
readonly channel: string;
constructor(message: string, channel: string) {
super(CONN_ERR_CHANNEL_ALREADY_EXISTS, { message });
this.channel = channel;
}
}
export class ConnChannelNotFoundError extends CodeError {
readonly channel: string;
constructor(message: string, channel: string) {
super(CONN_ERR_CHANNEL_NOT_FOUND, { message });
this.channel = channel;
}
}
export class ConnWebSocketUnavailableError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONN_ERR_WEBSOCKET_UNAVAILABLE, { message, cause: options?.cause });
}
}
export function isConnDisposedError(value: unknown): value is ConnDisposedError {
return value instanceof ConnDisposedError;
}
export function isConnConnectionAlreadyExistsError(
value: unknown
): value is ConnConnectionAlreadyExistsError {
return value instanceof ConnConnectionAlreadyExistsError;
}
export function isConnConnectionNotFoundError(
value: unknown
): value is ConnConnectionNotFoundError {
return value instanceof ConnConnectionNotFoundError;
}
export function isConnInvalidConnectionNameError(
value: unknown
): value is ConnInvalidConnectionNameError {
return value instanceof ConnInvalidConnectionNameError;
}
export function isConnInvalidFrameError(value: unknown): value is ConnInvalidFrameError {
return value instanceof ConnInvalidFrameError;
}
export function isConnChannelAlreadyExistsError(
value: unknown
): value is ConnChannelAlreadyExistsError {
return value instanceof ConnChannelAlreadyExistsError;
}
export function isConnChannelNotFoundError(
value: unknown
): value is ConnChannelNotFoundError {
return value instanceof ConnChannelNotFoundError;
}
export function isConnWebSocketUnavailableError(
value: unknown
): value is ConnWebSocketUnavailableError {
return value instanceof ConnWebSocketUnavailableError;
}

@ -1,62 +0,0 @@
import {
CONN_ERROR_MSG_ALREADY_EXISTS_PREFIX,
CONN_ERROR_MSG_CHANNEL_ALREADY_EXISTS_PREFIX,
CONN_ERROR_MSG_CHANNEL_NOT_FOUND_PREFIX,
CONN_ERROR_MSG_DISPOSED_SUFFIX,
CONN_ERROR_MSG_INVALID_NAME_PREFIX,
CONN_ERROR_MSG_NOT_FOUND_PREFIX,
CONN_ERROR_PREFIX,
CONN_ID_PREFIX,
CONN_ID_SEPARATOR,
CONN_MODULE,
CONN_LOGGER_SCOPE_SEPARATOR,
CONN_LOG_MSG_LISTENER_THREW_PREFIX
} from './consts.ts';
export function disposedErrorMessage(method: string): string {
return `${CONN_ERROR_PREFIX}${method}${CONN_ERROR_MSG_DISPOSED_SUFFIX}`;
}
export function alreadyExistsErrorMessage(name: string): string {
return `${CONN_ERROR_PREFIX}${CONN_ERROR_MSG_ALREADY_EXISTS_PREFIX}${name}`;
}
export function notFoundErrorMessage(name: string): string {
return `${CONN_ERROR_PREFIX}${CONN_ERROR_MSG_NOT_FOUND_PREFIX}${name}`;
}
export function invalidNameErrorMessage(name: unknown): string {
return `${CONN_ERROR_PREFIX}${CONN_ERROR_MSG_INVALID_NAME_PREFIX}${String(name)}`;
}
export function channelAlreadyExistsErrorMessage(name: string): string {
return `${CONN_ERROR_PREFIX}${CONN_ERROR_MSG_CHANNEL_ALREADY_EXISTS_PREFIX}${name}`;
}
export function channelNotFoundErrorMessage(name: string): string {
return `${CONN_ERROR_PREFIX}${CONN_ERROR_MSG_CHANNEL_NOT_FOUND_PREFIX}${name}`;
}
export function listenerThrewMessage(event: string): string {
return `${CONN_LOG_MSG_LISTENER_THREW_PREFIX}${event}`;
}
export function loggerScope(name: string, topic?: string): string {
return topic === undefined
? `${CONN_MODULE}${CONN_LOGGER_SCOPE_SEPARATOR}${name}`
: `${CONN_MODULE}${CONN_LOGGER_SCOPE_SEPARATOR}${name}${CONN_LOGGER_SCOPE_SEPARATOR}${topic}`;
}
export function timerKey(connection: string, kind: string, id?: string): string {
return id === undefined
? `${CONN_MODULE}${CONN_LOGGER_SCOPE_SEPARATOR}${connection}${CONN_LOGGER_SCOPE_SEPARATOR}${kind}`
: `${CONN_MODULE}${CONN_LOGGER_SCOPE_SEPARATOR}${connection}${CONN_LOGGER_SCOPE_SEPARATOR}${kind}${CONN_LOGGER_SCOPE_SEPARATOR}${id}`;
}
export function createConnectionIdFactory(connectionName: string): () => string {
let next = 0;
return () => {
next += 1;
return [CONN_ID_PREFIX, connectionName, String(next)].join(CONN_ID_SEPARATOR);
};
}

@ -1,87 +0,0 @@
import {
CONN_FRAME_KEY_ACK,
CONN_FRAME_KEY_ERROR,
CONN_FRAME_KEY_ID,
CONN_FRAME_KEY_PAYLOAD,
CONN_FRAME_KEY_REPLY_TO,
CONN_FRAME_KEY_TOPIC,
CONN_FRAME_KEY_TS,
CONN_FRAME_KEY_TYPE,
CONN_ERROR_MSG_FRAME_ACK_MUST_BE_BOOLEAN,
CONN_ERROR_MSG_FRAME_ERROR_MUST_BE_OBJECT,
CONN_ERROR_MSG_FRAME_ID_MUST_BE_STRING,
CONN_ERROR_MSG_FRAME_MUST_BE_OBJECT,
CONN_ERROR_MSG_FRAME_REPLY_TO_MUST_BE_STRING,
CONN_ERROR_MSG_FRAME_TOPIC_MUST_BE_STRING,
CONN_ERROR_MSG_FRAME_TS_MUST_BE_NUMBER,
CONN_ERROR_MSG_FRAME_TYPE_MUST_BE_STRING
} from './consts.ts';
import { ConnInvalidFrameError } from './errors.ts';
import type { ConnectionFrame } from './types.ts';
export function assertConnectionFrame(value: unknown): asserts value is ConnectionFrame {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_MUST_BE_OBJECT);
}
const frame = value as Record<string, unknown>;
if (typeof frame[CONN_FRAME_KEY_TYPE] !== 'string' || frame[CONN_FRAME_KEY_TYPE].length === 0) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_TYPE_MUST_BE_STRING);
}
if (
CONN_FRAME_KEY_ID in frame &&
frame[CONN_FRAME_KEY_ID] !== undefined &&
typeof frame[CONN_FRAME_KEY_ID] !== 'string'
) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_ID_MUST_BE_STRING);
}
if (
CONN_FRAME_KEY_TOPIC in frame &&
frame[CONN_FRAME_KEY_TOPIC] !== undefined &&
typeof frame[CONN_FRAME_KEY_TOPIC] !== 'string'
) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_TOPIC_MUST_BE_STRING);
}
if (
CONN_FRAME_KEY_REPLY_TO in frame &&
frame[CONN_FRAME_KEY_REPLY_TO] !== undefined &&
typeof frame[CONN_FRAME_KEY_REPLY_TO] !== 'string'
) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_REPLY_TO_MUST_BE_STRING);
}
if (
CONN_FRAME_KEY_ACK in frame &&
frame[CONN_FRAME_KEY_ACK] !== undefined &&
typeof frame[CONN_FRAME_KEY_ACK] !== 'boolean'
) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_ACK_MUST_BE_BOOLEAN);
}
if (
CONN_FRAME_KEY_TS in frame &&
frame[CONN_FRAME_KEY_TS] !== undefined &&
typeof frame[CONN_FRAME_KEY_TS] !== 'number'
) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_TS_MUST_BE_NUMBER);
}
if (
CONN_FRAME_KEY_ERROR in frame &&
frame[CONN_FRAME_KEY_ERROR] !== undefined &&
(frame[CONN_FRAME_KEY_ERROR] === null || typeof frame[CONN_FRAME_KEY_ERROR] !== 'object')
) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_FRAME_ERROR_MUST_BE_OBJECT);
}
}
export function createFrame(input: ConnectionFrame): ConnectionFrame {
assertConnectionFrame(input);
const frame: Record<string, unknown> = {
[CONN_FRAME_KEY_TYPE]: input.type,
[CONN_FRAME_KEY_PAYLOAD]: input.payload
};
if (input.id !== undefined) frame[CONN_FRAME_KEY_ID] = input.id;
if (input.topic !== undefined) frame[CONN_FRAME_KEY_TOPIC] = input.topic;
if (input.ts !== undefined) frame[CONN_FRAME_KEY_TS] = input.ts;
if (input.ack !== undefined) frame[CONN_FRAME_KEY_ACK] = input.ack;
if (input.replyTo !== undefined) frame[CONN_FRAME_KEY_REPLY_TO] = input.replyTo;
if (input.error !== undefined) frame[CONN_FRAME_KEY_ERROR] = input.error;
return frame as unknown as ConnectionFrame;
}

@ -751,8 +751,8 @@ export interface EngineConnections<TConnections extends ConnectionMap = Connecti
Behavior:
- `createConnection(name, options)` creates and registers a connection.
- Creating the same name twice should throw `ConnConnectionAlreadyExistsError`.
- `connection(name)` returns an existing connection or throws `ConnConnectionNotFoundError`.
- Creating the same name twice should throw `ConnectionAlreadyExistsError`.
- `connection(name)` returns an existing connection or throws `ConnectionNotFoundError`.
- `closeAll()` disconnects all registered connections.
- `dispose()` disconnects everything and clears listeners.
@ -1377,8 +1377,8 @@ Defaults:
enabled: true,
intervalMs: 25_000,
timeoutMs: 10_000,
pingType: 'conn.ping',
pongType: 'conn.pong'
pingType: 'connection.ping',
pongType: 'connection.pong'
}
```
@ -1445,7 +1445,7 @@ Implementation may send a frame:
```ts
{
type: 'conn.auth',
type: 'connection.auth',
payload: await getAuth(),
ack: true
}
@ -1592,13 +1592,13 @@ Use custom errors for programmer errors, not normal runtime failures.
Examples:
```ts
ConnConnectionAlreadyExistsError;
ConnConnectionNotFoundError;
ConnInvalidConnectionNameError;
ConnInvalidFrameError;
ConnDisposedError;
ConnChannelAlreadyExistsError;
ConnChannelNotFoundError;
ConnectionAlreadyExistsError;
ConnectionNotFoundError;
ConnectionInvalidNameError;
ConnectionInvalidFrameError;
ConnectionDisposedError;
ConnectionChannelAlreadyExistsError;
ConnectionChannelNotFoundError;
```
Runtime network failures should generally return tagged results or emit state/error events, not throw unexpectedly.
@ -1608,13 +1608,13 @@ Runtime network failures should generally return tagged results or emit state/er
## 23. Public constants
```ts
export const CONN_STATE_IDLE = 'idle';
export const CONN_STATE_CONNECTING = 'connecting';
export const CONN_STATE_OPEN = 'open';
export const CONN_STATE_RECONNECTING = 'reconnecting';
export const CONN_STATE_CLOSING = 'closing';
export const CONN_STATE_CLOSED = 'closed';
export const CONN_STATE_FAILED = 'failed';
export const CONNECTION_STATE_IDLE = 'idle';
export const CONNECTION_STATE_CONNECTING = 'connecting';
export const CONNECTION_STATE_OPEN = 'open';
export const CONNECTION_STATE_RECONNECTING = 'reconnecting';
export const CONNECTION_STATE_CLOSING = 'closing';
export const CONNECTION_STATE_CLOSED = 'closed';
export const CONNECTION_STATE_FAILED = 'failed';
export const DEFAULT_RECONNECT_MIN_DELAY_MS = 500;
export const DEFAULT_RECONNECT_MAX_DELAY_MS = 15_000;
@ -1638,11 +1638,11 @@ export const FRAME_KEY_REPLY_TO = 'replyTo';
export const LOGGER_CATEGORY = 'conn';
export const LOG_MSG_LISTENER_THREW_PREFIX = 'Listener threw on ';
export const ERROR_NAME_DISPOSED = 'ConnDisposedError';
export const ERROR_NAME_ALREADY_EXISTS = 'ConnConnectionAlreadyExistsError';
export const ERROR_NAME_NOT_FOUND = 'ConnConnectionNotFoundError';
export const ERROR_NAME_INVALID_NAME = 'ConnInvalidConnectionNameError';
export const ERROR_NAME_INVALID_FRAME = 'ConnInvalidFrameError';
export const ERROR_NAME_DISPOSED = 'ConnectionDisposedError';
export const ERROR_NAME_ALREADY_EXISTS = 'ConnectionAlreadyExistsError';
export const ERROR_NAME_NOT_FOUND = 'ConnectionNotFoundError';
export const ERROR_NAME_INVALID_NAME = 'ConnectionInvalidNameError';
export const ERROR_NAME_INVALID_FRAME = 'ConnectionInvalidFrameError';
```
Prefer exported constants over magic strings/numbers. Engine bodies should not
@ -1681,7 +1681,7 @@ const outboundBuffer: ConnectionFrame[] = [];
On `connect()`:
1. If disposed, throw `ConnDisposedError`.
1. If disposed, throw `ConnectionDisposedError`.
2. If open, return reused success.
3. If connecting, return existing promise.
4. Create/open transport.

@ -197,7 +197,7 @@ 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 `ConnInvalidFrameError` según el punto de entrada.
tagged o errores `ConnectionInvalidFrameError` según el punto de entrada.
## Send Y Request/Reply
@ -296,8 +296,8 @@ Connections.createConnection('main', {
enabled: true,
intervalMs: 25_000,
timeoutMs: 10_000,
pingType: 'conn.ping',
pongType: 'conn.pong'
pingType: 'connection.ping',
pongType: 'connection.pong'
}
});
```
@ -315,7 +315,7 @@ Connections.createConnection('main', {
transport,
auth: {
getAuth: () => ({ token }),
authType: 'conn.auth',
authType: 'connection.auth',
timeoutMs: 10_000
}
});
@ -338,7 +338,7 @@ definido, el session source derivado de `App.Bus` puede:
El flujo completo es:
```txt
sess -> SESS_EVENT_CHANGED
sess -> SESSION_EVENT_CHANGED
aapp -> APP_EVENT_USER_IDENTITY_CHANGED
conn -> reauth/disconnect si autoReauthOn + connection.session + connection.auth lo permiten
```
@ -376,7 +376,7 @@ import type { Logger } from '$libs/logr';
```
Internamente `conn` usa `createConnectionDiagnostics(logger)` y eventos
catalogados en `CONN_DIAGNOSTIC_EVENTS`:
catalogados en `CONNECTION_DIAGNOSTIC_EVENTS`:
- `connect_failed`
- `transport_error`
@ -401,14 +401,14 @@ Sentry, Loki o Datadog, inyecta un `EngineLogger` con el transporte adecuado;
Programmer errors lanzan clases tipadas:
- `ConnDisposedError`
- `ConnConnectionAlreadyExistsError`
- `ConnConnectionNotFoundError`
- `ConnInvalidConnectionNameError`
- `ConnInvalidFrameError`
- `ConnChannelAlreadyExistsError`
- `ConnChannelNotFoundError`
- `ConnWebSocketUnavailableError`
- `ConnectionDisposedError`
- `ConnectionAlreadyExistsError`
- `ConnectionNotFoundError`
- `ConnectionInvalidNameError`
- `ConnectionInvalidFrameError`
- `ConnectionChannelAlreadyExistsError`
- `ConnectionChannelNotFoundError`
- `ConnectionWebSocketUnavailableError`
Fallos runtime de transporte/envío/auth/request devuelven resultados tagged
para que el consumidor pueda decidir sin `try/catch` obligatorio.

@ -1,11 +1,11 @@
import type { TimerScheduler } from '$libs/timers';
import type { TimerScheduler } from '$libs/timer';
import {
CONN_ACK_REASON_CLOSED,
CONN_ACK_REASON_REJECTED,
CONN_ACK_REASON_TIMEOUT,
CONN_ACK_REASON_TRANSPORT_ERROR,
CONN_SEND_REASON_CLOSED,
CONN_TIMR_KEY_ACK
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_ACK_REASON_REJECTED,
CONNECTION_ACK_REASON_TIMEOUT,
CONNECTION_ACK_REASON_TRANSPORT_ERROR,
CONNECTION_SEND_REASON_CLOSED,
CONNECTION_TIMER_KEY_ACK
} from './consts.ts';
import { timerKey } from './helpers.ts';
import type { ConnectionAckResult, ConnectionFrame, ConnectionSendResult } from './types.ts';
@ -39,7 +39,7 @@ export function createConnectionAckRegistry(
return {
wait<TResult>(id: string, timeoutMs: number): Promise<ConnectionAckResult<TResult>> {
const ackTimer = timerKey(connectionName, CONN_TIMR_KEY_ACK, id);
const ackTimer = timerKey(connectionName, CONNECTION_TIMER_KEY_ACK, id);
return new Promise<ConnectionAckResult<TResult>>((resolveWaiter) => {
pending.set(id, {
timer: ackTimer,
@ -49,7 +49,7 @@ export function createConnectionAckRegistry(
ackTimer,
timeoutMs,
() => {
resolve(id, { ok: false, reason: CONN_ACK_REASON_TIMEOUT });
resolve(id, { ok: false, reason: CONNECTION_ACK_REASON_TIMEOUT });
},
{ replace: true }
);
@ -64,7 +64,7 @@ export function createConnectionAckRegistry(
if (frame.error !== undefined) {
resolve(frame.replyTo, {
ok: false,
reason: CONN_ACK_REASON_REJECTED,
reason: CONNECTION_ACK_REASON_REJECTED,
error: frame.error
});
return;
@ -73,10 +73,10 @@ export function createConnectionAckRegistry(
},
mapSendFailure(result: ConnectionSendResult): ConnectionAckResult<unknown> {
if (result.ok) return { ok: true, payload: undefined };
if (result.reason === CONN_SEND_REASON_CLOSED) {
return { ok: false, reason: CONN_ACK_REASON_CLOSED, error: result.error };
if (result.reason === CONNECTION_SEND_REASON_CLOSED) {
return { ok: false, reason: CONNECTION_ACK_REASON_CLOSED, error: result.error };
}
return { ok: false, reason: CONN_ACK_REASON_TRANSPORT_ERROR, error: result.error };
return { ok: false, reason: CONNECTION_ACK_REASON_TRANSPORT_ERROR, error: result.error };
}
};
}

@ -1,10 +1,10 @@
import { createEngineConnections } from './engine-connections.ts';
import {
CONN_STATE_CLOSED,
CONN_STATE_CONNECTING,
CONN_STATE_FAILED,
CONN_STATE_OPEN,
CONN_STATE_RECONNECTING
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_CONNECTING,
CONNECTION_STATE_FAILED,
CONNECTION_STATE_OPEN,
CONNECTION_STATE_RECONNECTING
} from './consts.ts';
import type {
ActiveConnections,
@ -61,37 +61,37 @@ export function createActiveConnections<TConnections extends ConnectionMap = Con
return states;
},
get connectedNames() {
return namesBy(CONN_STATE_OPEN);
return namesBy(CONNECTION_STATE_OPEN);
},
get connectingNames() {
return namesBy(CONN_STATE_CONNECTING);
return namesBy(CONNECTION_STATE_CONNECTING);
},
get reconnectingNames() {
return namesBy(CONN_STATE_RECONNECTING);
return namesBy(CONNECTION_STATE_RECONNECTING);
},
get failedNames() {
return namesBy(CONN_STATE_FAILED);
return namesBy(CONNECTION_STATE_FAILED);
},
get closedNames() {
return namesBy(CONN_STATE_CLOSED);
return namesBy(CONNECTION_STATE_CLOSED);
},
get allConnected() {
return (
activeNames.length > 0 &&
activeNames.every((name) => states[name] === CONN_STATE_OPEN)
activeNames.every((name) => states[name] === CONNECTION_STATE_OPEN)
);
},
get anyConnected() {
return activeNames.some((name) => states[name] === CONN_STATE_OPEN);
return activeNames.some((name) => states[name] === CONNECTION_STATE_OPEN);
},
get anyConnecting() {
return activeNames.some((name) => states[name] === CONN_STATE_CONNECTING);
return activeNames.some((name) => states[name] === CONNECTION_STATE_CONNECTING);
},
get anyReconnecting() {
return activeNames.some((name) => states[name] === CONN_STATE_RECONNECTING);
return activeNames.some((name) => states[name] === CONNECTION_STATE_RECONNECTING);
},
get anyFailed() {
return activeNames.some((name) => states[name] === CONN_STATE_FAILED);
return activeNames.some((name) => states[name] === CONNECTION_STATE_FAILED);
},
createConnection<TChannels extends ConnectionChannelMap = ConnectionChannelMap>(
name: string,

@ -1,10 +1,10 @@
import {
CONN_BROWSER_EVENT_ONLINE,
CONN_BROWSER_EVENT_VISIBILITY_CHANGE,
CONN_DIAGNOSTIC_EVENTS,
CONN_DEFAULT_RECONNECT_ON_ONLINE,
CONN_DEFAULT_RECONNECT_ON_VISIBLE,
CONN_DOCUMENT_VISIBILITY_VISIBLE
CONNECTION_BROWSER_EVENT_ONLINE,
CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE,
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_DEFAULT_RECONNECT_ON_ONLINE,
CONNECTION_DEFAULT_RECONNECT_ON_VISIBLE,
CONNECTION_DOCUMENT_VISIBILITY_VISIBLE
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionReconnectOptions } from './types.ts';
@ -21,9 +21,9 @@ export function wireBrowserReconnect(options: BrowserReconnectOptions): () => vo
if (options.disabled) return () => {};
const detachers: Array<() => void> = [];
const reconnectOnOnline =
options.reconnectOptions?.reconnectOnOnline ?? CONN_DEFAULT_RECONNECT_ON_ONLINE;
options.reconnectOptions?.reconnectOnOnline ?? CONNECTION_DEFAULT_RECONNECT_ON_ONLINE;
const reconnectOnVisible =
options.reconnectOptions?.reconnectOnVisible ?? CONN_DEFAULT_RECONNECT_ON_VISIBLE;
options.reconnectOptions?.reconnectOnVisible ?? CONNECTION_DEFAULT_RECONNECT_ON_VISIBLE;
const target = globalThis as {
addEventListener?: (type: string, listener: () => void) => void;
removeEventListener?: (type: string, listener: () => void) => void;
@ -39,14 +39,14 @@ export function wireBrowserReconnect(options: BrowserReconnectOptions): () => vo
if (!options.shouldReconnect()) return;
emitConnectionDiagnostic(
options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT,
{ trigger: CONN_BROWSER_EVENT_ONLINE }
CONNECTION_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT,
{ trigger: CONNECTION_BROWSER_EVENT_ONLINE }
);
options.reconnect();
};
target.addEventListener(CONN_BROWSER_EVENT_ONLINE, onOnline);
target.addEventListener(CONNECTION_BROWSER_EVENT_ONLINE, onOnline);
detachers.push(() => {
target.removeEventListener?.(CONN_BROWSER_EVENT_ONLINE, onOnline);
target.removeEventListener?.(CONNECTION_BROWSER_EVENT_ONLINE, onOnline);
});
}
@ -56,18 +56,18 @@ export function wireBrowserReconnect(options: BrowserReconnectOptions): () => vo
target.document.removeEventListener
) {
const onVisible = (): void => {
if (target.document?.visibilityState !== CONN_DOCUMENT_VISIBILITY_VISIBLE) return;
if (target.document?.visibilityState !== CONNECTION_DOCUMENT_VISIBILITY_VISIBLE) return;
if (!options.shouldReconnect()) return;
emitConnectionDiagnostic(
options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT,
{ trigger: CONN_BROWSER_EVENT_VISIBILITY_CHANGE }
CONNECTION_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT,
{ trigger: CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE }
);
options.reconnect();
};
target.document.addEventListener(CONN_BROWSER_EVENT_VISIBILITY_CHANGE, onVisible);
target.document.addEventListener(CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE, onVisible);
detachers.push(() => {
target.document?.removeEventListener?.(CONN_BROWSER_EVENT_VISIBILITY_CHANGE, onVisible);
target.document?.removeEventListener?.(CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE, onVisible);
});
}

@ -1,13 +1,13 @@
import {
AAPP_EVENT_USER_IDENTITY_CHANGED,
APP_EVENT_USER_IDENTITY_CHANGED,
type AppEventBus
} from '$libs/aapp/events';
} from '$libs/active-app/events';
import type { ConnectionSessionSource } from './types.ts';
export function createBusSessionSource(bus: AppEventBus): ConnectionSessionSource {
return {
onChange(listener) {
const subscription = bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => {
const subscription = bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => {
listener(event.payload);
});
return () => {

@ -1,15 +1,15 @@
import {
CONN_CHANNEL_JOIN_REASON_CLOSED,
CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
CONN_CHANNEL_STATE_FAILED,
CONN_CHANNEL_STATE_IDLE,
CONN_CHANNEL_STATE_JOINED,
CONN_CHANNEL_STATE_JOINING,
CONN_CHANNEL_STATE_LEAVING,
CONN_CHANNEL_STATE_LEFT,
CONN_FRAME_TYPE_JOIN,
CONN_FRAME_TYPE_LEAVE,
CONN_SEND_REASON_CLOSED
CONNECTION_CHANNEL_JOIN_REASON_CLOSED,
CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
CONNECTION_CHANNEL_STATE_FAILED,
CONNECTION_CHANNEL_STATE_IDLE,
CONNECTION_CHANNEL_STATE_JOINED,
CONNECTION_CHANNEL_STATE_JOINING,
CONNECTION_CHANNEL_STATE_LEAVING,
CONNECTION_CHANNEL_STATE_LEFT,
CONNECTION_FRAME_TYPE_JOIN,
CONNECTION_FRAME_TYPE_LEAVE,
CONNECTION_SEND_REASON_CLOSED
} from './consts.ts';
import type {
Connection,
@ -41,12 +41,12 @@ export interface ConnectionChannelRuntime<TEvents extends ConnectionEventMap = C
function mapSendToJoinResult(result: ConnectionSendResult): ConnectionChannelJoinResult {
if (result.ok) return { ok: true };
if (result.reason === CONN_SEND_REASON_CLOSED) {
return { ok: false, reason: CONN_CHANNEL_JOIN_REASON_CLOSED, error: result.error };
if (result.reason === CONNECTION_SEND_REASON_CLOSED) {
return { ok: false, reason: CONNECTION_CHANNEL_JOIN_REASON_CLOSED, error: result.error };
}
return {
ok: false,
reason: CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
reason: CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
error: result.error
};
}
@ -56,7 +56,7 @@ export function createConnectionChannel<TEvents extends ConnectionEventMap = Con
options: ConnectionChannelOptions = {},
runtime: ConnectionChannelRuntime<TEvents>
): InternalConnectionChannel<TEvents> {
let state: ConnectionChannelState = CONN_CHANNEL_STATE_IDLE;
let state: ConnectionChannelState = CONNECTION_CHANNEL_STATE_IDLE;
let desiredJoined = options.autoJoin === true;
let disposed = false;
@ -103,27 +103,27 @@ export function createConnectionChannel<TEvents extends ConnectionEventMap = Con
return desiredJoined && options.rejoinOnReconnect !== false;
},
async join(params) {
if (disposed) return { ok: false, reason: CONN_CHANNEL_JOIN_REASON_CLOSED };
setState(CONN_CHANNEL_STATE_JOINING);
if (disposed) return { ok: false, reason: CONNECTION_CHANNEL_JOIN_REASON_CLOSED };
setState(CONNECTION_CHANNEL_STATE_JOINING);
const payload = await resolveJoinPayload(params);
const result = await runtime.connection.send(CONN_FRAME_TYPE_JOIN, payload, {
const result = await runtime.connection.send(CONNECTION_FRAME_TYPE_JOIN, payload, {
topic: name
});
const mapped = mapSendToJoinResult(result);
if (mapped.ok) {
desiredJoined = true;
setState(CONN_CHANNEL_STATE_JOINED);
setState(CONNECTION_CHANNEL_STATE_JOINED);
} else {
setState(CONN_CHANNEL_STATE_FAILED);
setState(CONNECTION_CHANNEL_STATE_FAILED);
}
return mapped;
},
async leave() {
if (disposed) return;
setState(CONN_CHANNEL_STATE_LEAVING);
setState(CONNECTION_CHANNEL_STATE_LEAVING);
desiredJoined = false;
await runtime.connection.send(CONN_FRAME_TYPE_LEAVE, undefined, { topic: name });
setState(CONN_CHANNEL_STATE_LEFT);
await runtime.connection.send(CONNECTION_FRAME_TYPE_LEAVE, undefined, { topic: name });
setState(CONNECTION_CHANNEL_STATE_LEFT);
},
send(type, payload, sendOptions?: ConnectionSendOptions): Promise<ConnectionSendResult> {
return runtime.connection.send(type, payload, { ...sendOptions, topic: name });
@ -164,7 +164,7 @@ export function createConnectionChannel<TEvents extends ConnectionEventMap = Con
},
dispose() {
desiredJoined = false;
setState(CONN_CHANNEL_STATE_LEFT);
setState(CONNECTION_CHANNEL_STATE_LEFT);
disposed = true;
anyListeners.clear();
typeListeners.clear();

@ -0,0 +1,59 @@
import {
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_ACK_REASON_REJECTED,
CONNECTION_ACK_REASON_TIMEOUT,
CONNECTION_AUTH_REASON_CLOSED,
CONNECTION_AUTH_REASON_NO_PROVIDER,
CONNECTION_AUTH_REASON_REJECTED,
CONNECTION_AUTH_REASON_TIMEOUT,
CONNECTION_AUTH_REASON_TRANSPORT_ERROR,
CONNECTION_FRAME_TYPE_AUTH
} from './consts.ts';
import type {
ConnectionAckResult,
ConnectionAuthPayload,
ConnectionAuthResult,
ConnectionOptions
} from './types.ts';
export type ConnectionAuthProvider = () =>
| ConnectionAuthPayload
| null
| Promise<ConnectionAuthPayload | null>;
export interface ResolvedConnectionAuth {
readonly provider: ConnectionAuthProvider | null;
readonly authType: string;
readonly timeoutMs?: number;
}
export function resolveConnectionAuth(auth: ConnectionOptions['auth']): ResolvedConnectionAuth {
if (typeof auth === 'function') {
return { provider: auth, authType: CONNECTION_FRAME_TYPE_AUTH };
}
return {
provider: auth?.getAuth ?? null,
authType: auth?.authType ?? CONNECTION_FRAME_TYPE_AUTH,
timeoutMs: auth?.timeoutMs
};
}
export function missingConnectionAuthProvider(): ConnectionAuthResult {
return { ok: false, reason: CONNECTION_AUTH_REASON_NO_PROVIDER };
}
export function mapConnectionAckToAuthResult(
result: ConnectionAckResult<unknown>
): ConnectionAuthResult {
if (result.ok) return { ok: true };
if (result.reason === CONNECTION_ACK_REASON_TIMEOUT) {
return { ok: false, reason: CONNECTION_AUTH_REASON_TIMEOUT, error: result.error };
}
if (result.reason === CONNECTION_ACK_REASON_CLOSED) {
return { ok: false, reason: CONNECTION_AUTH_REASON_CLOSED, error: result.error };
}
if (result.reason === CONNECTION_ACK_REASON_REJECTED) {
return { ok: false, reason: CONNECTION_AUTH_REASON_REJECTED, error: result.error };
}
return { ok: false, reason: CONNECTION_AUTH_REASON_TRANSPORT_ERROR, error: result.error };
}

@ -1,10 +1,10 @@
import {
CONN_ACK_REASON_CLOSED,
CONN_STATE_CLOSED,
CONN_STATE_CLOSING,
CONN_STATE_FAILED,
CONN_STATE_RECONNECTING,
CONN_TIMR_KEY_RECONNECT
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_CLOSING,
CONNECTION_STATE_FAILED,
CONNECTION_STATE_RECONNECTING,
CONNECTION_TIMER_KEY_RECONNECT
} from './consts.ts';
import type { ConnectionAckRegistry } from './acks.ts';
import type { ConnectionStateTracker } from './connection-state.ts';
@ -33,26 +33,26 @@ export function handleConnectionTransportClose(
): void {
if (runtime.isDisposed()) return;
if (runtime.isIntentionalClose()) {
runtime.markClosed(CONN_STATE_CLOSED);
runtime.markClosed(CONNECTION_STATE_CLOSED);
return;
}
runtime.state.setError(event);
if (runtime.shouldReconnect()) {
runtime.markClosed(CONN_STATE_RECONNECTING, event);
runtime.markClosed(CONNECTION_STATE_RECONNECTING, event);
runtime.scheduleReconnect();
return;
}
runtime.markClosed(CONN_STATE_FAILED, event);
runtime.markClosed(CONNECTION_STATE_FAILED, event);
}
export function closeConnectionTransport(reason: string, runtime: ConnectionCloseRuntime): void {
runtime.setIntentionalClose(true);
runtime.timers.cancel(CONN_TIMR_KEY_RECONNECT);
runtime.timers.cancel(CONNECTION_TIMER_KEY_RECONNECT);
runtime.heartbeat.stop();
runtime.acks.resolveAll({ ok: false, reason: CONN_ACK_REASON_CLOSED });
runtime.state.transition(CONN_STATE_CLOSING);
runtime.acks.resolveAll({ ok: false, reason: CONNECTION_ACK_REASON_CLOSED });
runtime.state.transition(CONNECTION_STATE_CLOSING);
runtime.transport.close(reason);
if (runtime.state.state !== CONN_STATE_CLOSED) {
runtime.markClosed(CONN_STATE_CLOSED);
if (runtime.state.state !== CONNECTION_STATE_CLOSED) {
runtime.markClosed(CONNECTION_STATE_CLOSED);
}
}

@ -1,6 +1,6 @@
import {
CONN_CONNECT_REASON_DISPOSED,
CONN_STATE_OPEN
CONNECTION_CONNECT_REASON_DISPOSED,
CONNECTION_STATE_OPEN
} from './consts.ts';
import type { ConnectionConnectResult, ConnectionState } from './types.ts';
@ -27,10 +27,10 @@ export function createConnectionConnectController(
return Promise.resolve({
ok: false,
state,
reason: CONN_CONNECT_REASON_DISPOSED
reason: CONNECTION_CONNECT_REASON_DISPOSED
});
}
if (state === CONN_STATE_OPEN && options.isConnected()) {
if (state === CONNECTION_STATE_OPEN && options.isConnected()) {
return Promise.resolve({ ok: true, state, reused: true });
}
if (connectPromise !== null) return connectPromise;

@ -1,12 +1,12 @@
import {
CONN_CLOSE_REASON_AUTH_FAILED,
CONN_CONNECT_REASON_AUTH_FAILED,
CONN_CONNECT_REASON_TRANSPORT_ERROR,
CONN_DIAGNOSTIC_EVENTS,
CONN_STATE_CONNECTING,
CONN_STATE_FAILED,
CONN_STATE_RECONNECTING,
CONN_TIMR_KEY_RECONNECT
CONNECTION_CLOSE_REASON_AUTH_FAILED,
CONNECTION_CONNECT_REASON_AUTH_FAILED,
CONNECTION_CONNECT_REASON_TRANSPORT_ERROR,
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_STATE_CONNECTING,
CONNECTION_STATE_FAILED,
CONNECTION_STATE_RECONNECTING,
CONNECTION_TIMER_KEY_RECONNECT
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionRequestRuntime } from './connection-requests.ts';
@ -33,10 +33,10 @@ export interface RunConnectionConnectInput {
export async function runConnectionConnect(
input: RunConnectionConnectInput
): Promise<ConnectionConnectResult> {
input.timers.cancel(CONN_TIMR_KEY_RECONNECT);
input.timers.cancel(CONNECTION_TIMER_KEY_RECONNECT);
const nextTransport = input.resolveTransport();
input.attachTransport(nextTransport);
input.state.transition(input.reconnect ? CONN_STATE_RECONNECTING : CONN_STATE_CONNECTING);
input.state.transition(input.reconnect ? CONNECTION_STATE_RECONNECTING : CONNECTION_STATE_CONNECTING);
try {
await nextTransport.open();
input.markOpen();
@ -45,14 +45,14 @@ export async function runConnectionConnect(
if (!auth.ok) {
const authError = auth.error ?? auth.reason;
input.state.setError(authError);
emitConnectionDiagnostic(input.diagnostics, CONN_DIAGNOSTIC_EVENTS.AUTH_FAILED, auth);
emitConnectionDiagnostic(input.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.AUTH_FAILED, auth);
input.detachTransport();
nextTransport.close(undefined, CONN_CLOSE_REASON_AUTH_FAILED);
nextTransport.close(undefined, CONNECTION_CLOSE_REASON_AUTH_FAILED);
input.markClosedFailed(authError);
return {
ok: false,
state: input.state.state,
reason: CONN_CONNECT_REASON_AUTH_FAILED,
reason: CONNECTION_CONNECT_REASON_AUTH_FAILED,
error: authError
};
}
@ -62,14 +62,14 @@ export async function runConnectionConnect(
return { ok: true, state: input.state.state };
} catch (err) {
input.state.setError(err);
emitConnectionDiagnostic(input.diagnostics, CONN_DIAGNOSTIC_EVENTS.CONNECT_FAILED, {
emitConnectionDiagnostic(input.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.CONNECT_FAILED, {
error: err
});
input.state.transition(CONN_STATE_FAILED, err);
input.state.transition(CONNECTION_STATE_FAILED, err);
return {
ok: false,
state: input.state.state,
reason: CONN_CONNECT_REASON_TRANSPORT_ERROR,
reason: CONNECTION_CONNECT_REASON_TRANSPORT_ERROR,
error: err
};
}

@ -1,4 +1,4 @@
import { CONN_DIAGNOSTIC_EVENTS } from './consts.ts';
import { CONNECTION_DIAGNOSTIC_EVENTS } from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionFrame, ConnectionMessageMeta, ConnectionStateChange } from './types.ts';
@ -29,7 +29,7 @@ export function createConnectionEventBus(options: ConnectionEventBusOptions): Co
} catch (err) {
emitConnectionDiagnostic(
options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.LISTENER_THREW,
CONNECTION_DIAGNOSTIC_EVENTS.LISTENER_THREW,
{ error: err, event: change.to }
);
}
@ -42,7 +42,7 @@ export function createConnectionEventBus(options: ConnectionEventBusOptions): Co
} catch (err) {
emitConnectionDiagnostic(
options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.LISTENER_THREW,
CONNECTION_DIAGNOSTIC_EVENTS.LISTENER_THREW,
{ error: err, event: frame.type }
);
}

@ -1,7 +1,7 @@
import {
CONN_ACK_REASON_CLOSED,
CONN_STATE_OPEN,
CONN_TRANSPORT_STATE_OPEN
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_STATE_OPEN,
CONNECTION_TRANSPORT_STATE_OPEN
} from './consts.ts';
import type { ConnectionAckRegistry } from './acks.ts';
import type { ConnectionStateTracker } from './connection-state.ts';
@ -34,14 +34,14 @@ export function createConnectionLifecycle(
function markClosed(nextState: ConnectionState, cause?: unknown): void {
state.markClosed(nextState, cause, () => {
heartbeat.stop();
acks.resolveAll({ ok: false, reason: CONN_ACK_REASON_CLOSED });
acks.resolveAll({ ok: false, reason: CONNECTION_ACK_REASON_CLOSED });
});
}
function isConnected(): boolean {
return (
state.state === CONN_STATE_OPEN &&
transport.current?.state === CONN_TRANSPORT_STATE_OPEN
state.state === CONNECTION_STATE_OPEN &&
transport.current?.state === CONNECTION_TRANSPORT_STATE_OPEN
);
}

@ -1,6 +1,6 @@
import {
CONN_DIAGNOSTIC_EVENTS,
CONN_FRAME_TYPE_PONG
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_FRAME_TYPE_PONG
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import { assertConnectionFrame } from './serializer.ts';
@ -39,7 +39,7 @@ export function routeConnectionMessage<TChannels extends ConnectionChannelMap>(
assertConnectionFrame(frame);
} catch (err) {
input.state.setError(err);
emitConnectionDiagnostic(input.diagnostics, CONN_DIAGNOSTIC_EVENTS.FRAME_DECODE_FAILED, {
emitConnectionDiagnostic(input.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.FRAME_DECODE_FAILED, {
error: err
});
return;
@ -49,7 +49,7 @@ export function routeConnectionMessage<TChannels extends ConnectionChannelMap>(
input.acks.resolveFromFrame(frame);
return;
}
if (frame.type === CONN_FRAME_TYPE_PONG) return;
if (frame.type === CONNECTION_FRAME_TYPE_PONG) return;
const meta: ConnectionMessageMeta = {
connection: input.name,

@ -1,8 +1,8 @@
import {
CONN_DIAGNOSTIC_EVENTS,
CONN_STATE_FAILED,
CONN_LOG_MSG_RECONNECT_EXHAUSTED,
CONN_TIMR_KEY_RECONNECT
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_STATE_FAILED,
CONNECTION_LOG_MSG_RECONNECT_EXHAUSTED,
CONNECTION_TIMER_KEY_RECONNECT
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionReconnectPolicy } from './reconnect.ts';
@ -20,16 +20,16 @@ export interface ScheduleConnectionReconnectInput {
export function scheduleConnectionReconnect(input: ScheduleConnectionReconnectInput): void {
const nextReconnect = input.policy.next(input.state.reconnectAttempt);
if (!nextReconnect.ok) {
emitConnectionDiagnostic(input.diagnostics, CONN_DIAGNOSTIC_EVENTS.RECONNECT_EXHAUSTED, {
emitConnectionDiagnostic(input.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.RECONNECT_EXHAUSTED, {
reconnectAttempt: nextReconnect.reconnectAttempt,
maxAttempts: nextReconnect.maxAttempts
});
const exhaustedError = new Error(CONN_LOG_MSG_RECONNECT_EXHAUSTED);
const exhaustedError = new Error(CONNECTION_LOG_MSG_RECONNECT_EXHAUSTED);
input.state.setError(exhaustedError);
input.state.transition(CONN_STATE_FAILED, exhaustedError);
input.state.transition(CONNECTION_STATE_FAILED, exhaustedError);
return;
}
input.state.setReconnectAttempt(nextReconnect.attempt);
input.timers.schedule(CONN_TIMR_KEY_RECONNECT, nextReconnect.delayMs, input.connect);
input.timers.schedule(CONNECTION_TIMER_KEY_RECONNECT, nextReconnect.delayMs, input.connect);
}

@ -1,8 +1,8 @@
import {
CONN_ACK_REASON_CLOSED,
CONN_AUTH_REASON_CLOSED,
CONN_AUTH_REASON_REJECTED,
CONN_DEFAULT_ACK_TIMEOUT_MS
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_AUTH_REASON_CLOSED,
CONNECTION_AUTH_REASON_REJECTED,
CONNECTION_DEFAULT_ACK_TIMEOUT_MS
} from './consts.ts';
import type { ConnectionAckRegistry } from './acks.ts';
import {
@ -45,7 +45,7 @@ export function createConnectionRequestRuntime(
}
async function runAuth(): Promise<ConnectionAuthResult> {
if (!runtime.isConnected()) return { ok: false, reason: CONN_AUTH_REASON_CLOSED };
if (!runtime.isConnected()) return { ok: false, reason: CONNECTION_AUTH_REASON_CLOSED };
const auth = resolveConnectionAuth(runtime.auth);
if (auth.provider === null) return missingConnectionAuthProvider();
try {
@ -54,7 +54,7 @@ export function createConnectionRequestRuntime(
const result = await requestFrame(auth.authType, payload, { timeoutMs: auth.timeoutMs });
return mapConnectionAckToAuthResult(result);
} catch (err) {
return { ok: false, reason: CONN_AUTH_REASON_REJECTED, error: err };
return { ok: false, reason: CONNECTION_AUTH_REASON_REJECTED, error: err };
}
}
@ -63,7 +63,7 @@ export function createConnectionRequestRuntime(
payload: TPayload,
requestOptions: ConnectionRequestOptions = {}
): Promise<ConnectionAckResult<TResult>> {
if (!runtime.isConnected()) return { ok: false, reason: CONN_ACK_REASON_CLOSED };
if (!runtime.isConnected()) return { ok: false, reason: CONNECTION_ACK_REASON_CLOSED };
const id = requestOptions.id ?? runtime.nextId();
const frame = createFrame({
id,
@ -73,7 +73,7 @@ export function createConnectionRequestRuntime(
ack: true,
ts: requestOptions.ts
});
const timeoutMs = requestOptions.timeoutMs ?? CONN_DEFAULT_ACK_TIMEOUT_MS;
const timeoutMs = requestOptions.timeoutMs ?? CONNECTION_DEFAULT_ACK_TIMEOUT_MS;
const promise = runtime.acks.wait<TResult>(id, timeoutMs);
const result = await runtime.sender.sendFrame(frame, requestOptions.buffer === true);
if (!result.ok) runtime.acks.resolve(id, runtime.acks.mapSendFailure(result));

@ -1,8 +1,8 @@
import {
CONN_STATE_CLOSED,
CONN_STATE_FAILED,
CONN_STATE_IDLE,
CONN_STATE_OPEN
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_FAILED,
CONNECTION_STATE_IDLE,
CONNECTION_STATE_OPEN
} from './consts.ts';
import type { ConnectionState, ConnectionStateChange } from './types.ts';
@ -28,7 +28,7 @@ export function createConnectionStateTracker(input: {
readonly now: () => number;
readonly emitState: (change: ConnectionStateChange) => void;
}): ConnectionStateTracker {
let state: ConnectionState = CONN_STATE_IDLE;
let state: ConnectionState = CONNECTION_STATE_IDLE;
let generation = 0;
let error: unknown | null = null;
let openedAt: number | null = null;
@ -85,20 +85,20 @@ export function createConnectionStateTracker(input: {
},
isClosedLike() {
return (
state === CONN_STATE_CLOSED ||
state === CONN_STATE_FAILED ||
state === CONN_STATE_IDLE
state === CONNECTION_STATE_CLOSED ||
state === CONNECTION_STATE_FAILED ||
state === CONNECTION_STATE_IDLE
);
},
transition,
markOpen() {
if (state === CONN_STATE_OPEN) return false;
if (state === CONNECTION_STATE_OPEN) return false;
generation += 1;
error = null;
openedAt = input.now();
closedAt = null;
reconnectAttempt = 0;
return transition(CONN_STATE_OPEN);
return transition(CONNECTION_STATE_OPEN);
},
markClosed(to, closeError, beforeEmit) {
if (state === to) return false;

@ -1,4 +1,4 @@
import type { TimerScheduler } from '$libs/timers';
import type { TimerScheduler } from '$libs/timer';
import { timerKey } from './helpers.ts';
export interface ConnectionTimerControls {

@ -1,4 +1,4 @@
import type { TimerScheduler } from '$libs/timers';
import type { TimerScheduler } from '$libs/timer';
import { createConnectionAckRegistry } from './acks.ts';
import { wireBrowserReconnect } from './browser-reconnect.ts';
import { createConnectionChannelRegistry } from './channel-registry.ts';
@ -16,12 +16,12 @@ import { createConnectionRequestRuntime } from './connection-requests.ts';
import { createConnectionStateTracker } from './connection-state.ts';
import { createConnectionTimerControls } from './connection-timers.ts';
import {
CONN_CLOSE_REASON_DISCONNECT,
CONN_CLOSE_REASON_DISPOSE,
CONN_CLOSE_REASON_RECONNECT,
CONN_CLOSE_REASON_SESSION_EXPIRED,
CONN_DIAGNOSTIC_EVENTS,
CONN_STATE_FAILED
CONNECTION_CLOSE_REASON_DISCONNECT,
CONNECTION_CLOSE_REASON_DISPOSE,
CONNECTION_CLOSE_REASON_RECONNECT,
CONNECTION_CLOSE_REASON_SESSION_EXPIRED,
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_STATE_FAILED
} from './consts.ts';
import {
createConnectionDiagnostics,
@ -41,7 +41,7 @@ import { createConnectionSender } from './sender.ts';
import { createFrame } from './serializer.ts';
import { jsonConnectionSerializer } from './serializers/json.ts';
import { createConnectionTransportRuntime } from './connection-transport-runtime.ts';
import type { Logger } from '$libs/logr';
import type { Logger } from '$libs/logger';
import type {
Connection,
ConnectionChannel,
@ -131,7 +131,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
function handleTransportError(err: unknown): void {
stateTracker.setError(err);
emitConnectionDiagnostic(diagnostics, CONN_DIAGNOSTIC_EVENTS.TRANSPORT_ERROR, {
emitConnectionDiagnostic(diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.TRANSPORT_ERROR, {
error: err
});
}
@ -211,7 +211,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
detachTransport: transportRuntime.detach,
markOpen: lifecycle.markOpen,
markClosedFailed: (error) => {
lifecycle.markClosed(CONN_STATE_FAILED, error);
lifecycle.markClosed(CONNECTION_STATE_FAILED, error);
},
joinConfiguredChannels,
state: stateTracker,
@ -233,7 +233,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
emitScopedConnectionDiagnostic(
diagnostics,
scope,
CONN_DIAGNOSTIC_EVENTS.LISTENER_THREW,
CONNECTION_DIAGNOSTIC_EVENTS.LISTENER_THREW,
{ error: err, event }
);
}
@ -270,11 +270,11 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
connect() {
return connectInternal(false);
},
disconnect(reason = CONN_CLOSE_REASON_DISCONNECT) {
disconnect(reason = CONNECTION_CLOSE_REASON_DISCONNECT) {
if (disposed) return;
closeTransport(reason);
},
reconnect(reason = CONN_CLOSE_REASON_RECONNECT) {
reconnect(reason = CONNECTION_CLOSE_REASON_RECONNECT) {
if (!disposed) {
closeTransport(reason);
intentionalClose = false;
@ -326,7 +326,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
dispose() {
if (disposed) return;
disposed = true;
closeTransport(CONN_CLOSE_REASON_DISPOSE);
closeTransport(CONNECTION_CLOSE_REASON_DISPOSE);
detachSession();
detachBrowserReconnect();
transportRuntime.detach();

@ -0,0 +1,251 @@
import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs';
export const CONNECTION_MODULE = 'connection';
export const CONNECTION_LOGGER_SCOPE_SEPARATOR = ':';
export const CONNECTION_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'connection.auth_failed',
BROWSER_RECONNECT: 'connection.browser_reconnect',
CONNECT_FAILED: 'connection.connect_failed',
FRAME_DECODE_FAILED: 'connection.frame_decode_failed',
FRAME_ENCODE_FAILED: 'connection.frame_encode_failed',
HEARTBEAT_TIMEOUT: 'connection.heartbeat_timeout',
LISTENER_THREW: 'connection.listener_threw',
REAUTH_FAILED: 'connection.reauth_failed',
RECONNECT_EXHAUSTED: 'connection.reconnect_exhausted',
SEND_FAILED: 'connection.send_failed',
SESSION_CHANGED: 'connection.session_changed',
SESSION_EXPIRED: 'connection.session_expired',
SESSION_REFRESHED: 'connection.session_refreshed',
SESSION_REVOKED: 'connection.session_revoked',
TRANSPORT_ERROR: 'connection.transport_error'
} as const;
export const CONNECTION_STATE_IDLE = 'idle';
export const CONNECTION_STATE_CONNECTING = 'connecting';
export const CONNECTION_STATE_OPEN = 'open';
export const CONNECTION_STATE_RECONNECTING = 'reconnecting';
export const CONNECTION_STATE_CLOSING = 'closing';
export const CONNECTION_STATE_CLOSED = 'closed';
export const CONNECTION_STATE_FAILED = 'failed';
export const CONNECTION_STATES = [
CONNECTION_STATE_IDLE,
CONNECTION_STATE_CONNECTING,
CONNECTION_STATE_OPEN,
CONNECTION_STATE_RECONNECTING,
CONNECTION_STATE_CLOSING,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_FAILED
] as const;
export const CONNECTION_CHANNEL_STATE_IDLE = 'idle';
export const CONNECTION_CHANNEL_STATE_JOINING = 'joining';
export const CONNECTION_CHANNEL_STATE_JOINED = 'joined';
export const CONNECTION_CHANNEL_STATE_LEAVING = 'leaving';
export const CONNECTION_CHANNEL_STATE_LEFT = 'left';
export const CONNECTION_CHANNEL_STATE_FAILED = 'failed';
export const CONNECTION_TRANSPORT_STATE_IDLE = 'idle';
export const CONNECTION_TRANSPORT_STATE_OPENING = 'opening';
export const CONNECTION_TRANSPORT_STATE_OPEN = 'open';
export const CONNECTION_TRANSPORT_STATE_CLOSING = 'closing';
export const CONNECTION_TRANSPORT_STATE_CLOSED = 'closed';
export const CONNECTION_TRANSPORT_STATES = [
CONNECTION_TRANSPORT_STATE_IDLE,
CONNECTION_TRANSPORT_STATE_OPENING,
CONNECTION_TRANSPORT_STATE_OPEN,
CONNECTION_TRANSPORT_STATE_CLOSING,
CONNECTION_TRANSPORT_STATE_CLOSED
] as const;
export const CONNECTION_TRANSPORT_KIND_WEBSOCKET = 'websocket';
export const CONNECTION_TRANSPORT_KIND_MOCK = 'mock';
export const CONNECTION_FRAME_KEY_ID = 'id';
export const CONNECTION_FRAME_KEY_TYPE = 'type';
export const CONNECTION_FRAME_KEY_TOPIC = 'topic';
export const CONNECTION_FRAME_KEY_PAYLOAD = 'payload';
export const CONNECTION_FRAME_KEY_TS = 'ts';
export const CONNECTION_FRAME_KEY_ACK = 'ack';
export const CONNECTION_FRAME_KEY_REPLY_TO = 'replyTo';
export const CONNECTION_FRAME_KEY_ERROR = 'error';
export const CONNECTION_DEFAULT_ACK_TIMEOUT_MS = 10_000;
export const CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_MS = 500;
export const CONNECTION_DEFAULT_RECONNECT_MAX_DELAY_MS = 15_000;
export const CONNECTION_DEFAULT_RECONNECT_FACTOR = 1.8;
export const CONNECTION_DEFAULT_RECONNECT_JITTER_MS = 500;
export const CONNECTION_DEFAULT_HEARTBEAT_INTERVAL_MS = 25_000;
export const CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS = 10_000;
export const CONNECTION_DEFAULT_BUFFER_MAX_MESSAGES = 100;
export const CONNECTION_DEFAULT_BUFFER_MAX_BYTES = 1_000_000;
export const CONNECTION_DEFAULT_RECONNECT_ENABLED = true;
export const CONNECTION_DEFAULT_RECONNECT_ON_VISIBLE = true;
export const CONNECTION_DEFAULT_RECONNECT_ON_ONLINE = true;
export const CONNECTION_DEFAULT_HEARTBEAT_ENABLED = true;
export const CONNECTION_BUFFER_POLICY_BUFFER = 'buffer';
export const CONNECTION_BUFFER_POLICY_DROP = 'drop';
export const CONNECTION_BUFFER_POLICY_FAIL = 'fail';
export const CONNECTION_SEND_REASON_CLOSED = 'closed';
export const CONNECTION_SEND_REASON_SEND_NOT_SUPPORTED = 'send_not_supported';
export const CONNECTION_SEND_REASON_BUFFER_FULL = 'buffer_full';
export const CONNECTION_SEND_REASON_INVALID_FRAME = 'invalid_frame';
export const CONNECTION_SEND_REASON_SERIALIZE_FAILED = 'serialize_failed';
export const CONNECTION_SEND_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONNECTION_ACK_REASON_TIMEOUT = 'timeout';
export const CONNECTION_ACK_REASON_CLOSED = 'closed';
export const CONNECTION_ACK_REASON_REJECTED = 'rejected';
export const CONNECTION_ACK_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONNECTION_ACK_REASON_INVALID_REPLY = 'invalid_reply';
export const CONNECTION_AUTH_REASON_NO_PROVIDER = 'no_auth_provider';
export const CONNECTION_AUTH_REASON_CLOSED = 'closed';
export const CONNECTION_AUTH_REASON_REJECTED = 'rejected';
export const CONNECTION_AUTH_REASON_TIMEOUT = 'timeout';
export const CONNECTION_AUTH_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONNECTION_CONNECT_REASON_TRANSPORT_ERROR = 'transport_error';
export const CONNECTION_CONNECT_REASON_AUTH_FAILED = 'auth_failed';
export const CONNECTION_CONNECT_REASON_DISPOSED = 'disposed';
export const CONNECTION_CONNECT_REASON_RECONNECT_EXHAUSTED = 'reconnect_exhausted';
export const CONNECTION_CHANNEL_JOIN_REASON_CLOSED = 'closed';
export const CONNECTION_CHANNEL_JOIN_REASON_REJECTED = 'rejected';
export const CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR = 'transport_error';
// Connection event names. Scoped with the `conn.` prefix to disambiguate
// from cache, session or any other emitter when logs are aggregated.
export const CONNECTION_EVENT_STATE = 'connection.state';
export const CONNECTION_EVENT_MESSAGE = 'connection.message';
export const CONNECTION_EVENT_CHANNELS = 'connection.channels';
export const CONNECTION_EVENT_DISPOSED = 'connection.disposed';
export const CONNECTION_METHOD_CONNECT = 'connect';
export const CONNECTION_METHOD_DISCONNECT = 'disconnect';
export const CONNECTION_METHOD_RECONNECT = 'reconnect';
export const CONNECTION_METHOD_SEND = 'send';
export const CONNECTION_METHOD_REQUEST = 'request';
export const CONNECTION_METHOD_CREATE_CONNECTION = 'createConnection';
export const CONNECTION_METHOD_CONNECTION = 'connection';
export const CONNECTION_METHOD_OPEN_CONNECTION = 'openConnection';
export const CONNECTION_METHOD_CLOSE_CONNECTION = 'closeConnection';
export const CONNECTION_METHOD_RECONNECT_CONNECTION = 'reconnectConnection';
export const CONNECTION_METHOD_OPEN_ALL = 'openAll';
export const CONNECTION_METHOD_CLOSE_ALL = 'closeAll';
export const CONNECTION_METHOD_RECONNECT_ALL = 'reconnectAll';
export const CONNECTION_METHOD_REAUTHENTICATE_ALL = 'reauthenticateAll';
export const CONNECTION_METHOD_REAUTHENTICATE = 'reauthenticate';
export const CONNECTION_METHOD_JOIN = 'join';
export const CONNECTION_METHOD_LEAVE = 'leave';
export const CONNECTION_AUTO_REAUTH_NONE = 'none';
export const CONNECTION_AUTO_REAUTH_STANDARD = 'standard';
export const CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE = 'userIdentityChange';
export const CONNECTION_FRAME_TYPE_AUTH = 'connection.auth';
export const CONNECTION_FRAME_TYPE_JOIN = 'connection.join';
export const CONNECTION_FRAME_TYPE_LEAVE = 'connection.leave';
export const CONNECTION_FRAME_TYPE_PING = 'connection.ping';
export const CONNECTION_FRAME_TYPE_PONG = 'connection.pong';
export const CONNECTION_ID_PREFIX = 'connection';
export const CONNECTION_ID_SEPARATOR = '-';
export const CONNECTION_CLOSE_REASON_DISCONNECT = 'disconnect';
export const CONNECTION_CLOSE_REASON_RECONNECT = 'reconnect';
export const CONNECTION_CLOSE_REASON_DISPOSE = 'dispose';
export const CONNECTION_CLOSE_REASON_HEARTBEAT_TIMEOUT = 'heartbeat_timeout';
export const CONNECTION_CLOSE_REASON_SESSION_EXPIRED = 'session_expired';
export const CONNECTION_CLOSE_REASON_AUTH_FAILED = 'auth_failed';
export const CONNECTION_TIMER_KEY_RECONNECT = 'reconnect';
export const CONNECTION_TIMER_KEY_HEARTBEAT = 'heartbeat';
export const CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT = 'heartbeat-timeout';
export const CONNECTION_TIMER_KEY_ACK = 'ack';
export const CONNECTION_BROWSER_EVENT_OPEN = 'open';
export const CONNECTION_BROWSER_EVENT_MESSAGE = 'message';
export const CONNECTION_BROWSER_EVENT_CLOSE = 'close';
export const CONNECTION_BROWSER_EVENT_ERROR = 'error';
export const CONNECTION_BROWSER_EVENT_ONLINE = 'online';
export const CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE = 'visibilitychange';
export const CONNECTION_DOCUMENT_VISIBILITY_VISIBLE = 'visible';
export const CONNECTION_WEBSOCKET_BINARY_TYPE_ARRAYBUFFER = 'arraybuffer';
export const CONNECTION_WEBSOCKET_READY_STATE_CONNECTING = 0;
export const CONNECTION_WEBSOCKET_READY_STATE_OPEN = 1;
export const CONNECTION_WEBSOCKET_READY_STATE_CLOSING = 2;
export const CONNECTION_WEBSOCKET_READY_STATE_CLOSED = 3;
export const CONNECTION_WEBSOCKET_CLOSE_CODE_NORMAL = 1000;
export const CONNECTION_WEBSOCKET_CLOSE_CODE_ABNORMAL = 1006;
export const CONNECTION_MOCK_TRANSPORT_DEFAULT_CLOSE_CODE = CONNECTION_WEBSOCKET_CLOSE_CODE_NORMAL;
export const CONNECTION_MOCK_TRANSPORT_DEFAULT_CLOSE_CLEAN = true;
// Mirror of sess lifecycle discriminants. Kept in sync with the values
// declared in `arts/sess/consts.ts` so `session-wiring.ts` can compare
// `change.event` strings without importing arts/sess directly.
//
// Long term this should be replaced by a typed union imported from
// `$session`; for now the values are duplicated and must move together.
export const CONNECTION_SESSION_EVENT_REFRESHED = 'session.lifecycle.refreshed';
export const CONNECTION_SESSION_EVENT_ADOPTED = 'session.lifecycle.adopted';
export const CONNECTION_SESSION_EVENT_ADOPTED_SERVER = 'session.lifecycle.adopted_server';
export const CONNECTION_SESSION_EVENT_EXTERNAL_CHANGED = 'session.lifecycle.external_changed';
export const CONNECTION_SESSION_EVENT_EXPIRED = 'session.lifecycle.expired';
export const CONNECTION_SESSION_EVENT_REVOKED = 'session.lifecycle.revoked';
export const CONNECTION_LOG_MSG_LISTENER_THREW_PREFIX = 'Listener threw on ';
export const CONNECTION_LOG_MSG_TRANSPORT_ERROR = 'transport error';
export const CONNECTION_LOG_MSG_RECONNECT_EXHAUSTED = 'reconnect attempts exhausted';
export const CONNECTION_LOG_MSG_CONNECT_FAILED = 'connect failed';
export const CONNECTION_LOG_MSG_SEND_FAILED = 'send failed';
export const CONNECTION_LOG_MSG_FRAME_DECODE_FAILED = 'frame decode failed';
export const CONNECTION_LOG_MSG_FRAME_ENCODE_FAILED = 'frame encode failed';
export const CONNECTION_LOG_MSG_AUTH_FAILED = 'auth failed';
export const CONNECTION_LOG_MSG_REAUTH_FAILED = 'reauthentication failed';
export const CONNECTION_LOG_MSG_HEARTBEAT_TIMEOUT = 'heartbeat timeout';
export const CONNECTION_LOG_MSG_SESSION_CHANGED = 'session changed; reauthenticating connection';
export const CONNECTION_LOG_MSG_SESSION_REVOKED = 'session revoked; disconnecting connection';
export const CONNECTION_LOG_MSG_SESSION_EXPIRED = 'session expired; disconnecting connection';
export const CONNECTION_LOG_MSG_SESSION_REFRESHED = 'session refreshed; reauthenticating connection';
export const CONNECTION_LOG_MSG_BROWSER_RECONNECT = 'browser lifecycle reconnect requested';
export const CONNECTION_ERROR_PREFIX = `[${CONNECTION_MODULE}] `;
// ── Error codes ────────────────────────────────────────────────────────
export const CONNECTION_ERR: ModuleSeed = moduleSeed(CONNECTION_MODULE);
export const CONNECTION_ERR_DISPOSED: ErrCode = errCode(CONNECTION_ERR, 'disposed');
export const CONNECTION_ERR_CONNECTION_ALREADY_EXISTS: ErrCode = errCode(CONNECTION_ERR, 'connection_already_exists');
export const CONNECTION_ERR_CONNECTION_NOT_FOUND: ErrCode = errCode(CONNECTION_ERR, 'connection_not_found');
export const CONNECTION_ERR_INVALID_CONNECTION_NAME: ErrCode = errCode(CONNECTION_ERR, 'invalid_connection_name');
export const CONNECTION_ERR_INVALID_FRAME: ErrCode = errCode(CONNECTION_ERR, 'invalid_frame');
export const CONNECTION_ERR_CHANNEL_ALREADY_EXISTS: ErrCode = errCode(CONNECTION_ERR, 'channel_already_exists');
export const CONNECTION_ERR_CHANNEL_NOT_FOUND: ErrCode = errCode(CONNECTION_ERR, 'channel_not_found');
export const CONNECTION_ERR_WEBSOCKET_UNAVAILABLE: ErrCode = errCode(CONNECTION_ERR, 'websocket_unavailable');
export const CONNECTION_ERROR_MSG_DISPOSED_SUFFIX = '() called on a disposed connection engine';
export const CONNECTION_ERROR_MSG_ALREADY_EXISTS_PREFIX = 'connection already exists: ';
export const CONNECTION_ERROR_MSG_NOT_FOUND_PREFIX = 'connection not found: ';
export const CONNECTION_ERROR_MSG_INVALID_NAME_PREFIX = 'invalid connection name: ';
export const CONNECTION_ERROR_MSG_INVALID_FRAME = 'invalid connection frame';
export const CONNECTION_ERROR_MSG_FRAME_MUST_BE_OBJECT = 'Connection frame must be an object';
export const CONNECTION_ERROR_MSG_FRAME_TYPE_MUST_BE_STRING =
'Connection frame type must be a non-empty string';
export const CONNECTION_ERROR_MSG_FRAME_ID_MUST_BE_STRING = 'Connection frame id must be a string';
export const CONNECTION_ERROR_MSG_FRAME_TOPIC_MUST_BE_STRING = 'Connection frame topic must be a string';
export const CONNECTION_ERROR_MSG_FRAME_REPLY_TO_MUST_BE_STRING = 'Connection frame replyTo must be a string';
export const CONNECTION_ERROR_MSG_FRAME_ACK_MUST_BE_BOOLEAN = 'Connection frame ack must be a boolean';
export const CONNECTION_ERROR_MSG_FRAME_TS_MUST_BE_NUMBER = 'Connection frame ts must be a number';
export const CONNECTION_ERROR_MSG_FRAME_ERROR_MUST_BE_OBJECT = 'Connection frame error must be an object';
export const CONNECTION_ERROR_MSG_JSON_DECODE_FAILED = 'Connection JSON payload is invalid';
export const CONNECTION_ERROR_MSG_WEBSOCKET_UNAVAILABLE = 'WebSocket is not available in this runtime';
export const CONNECTION_ERROR_MSG_CHANNEL_ALREADY_EXISTS_PREFIX = 'channel already exists: ';
export const CONNECTION_ERROR_MSG_CHANNEL_NOT_FOUND_PREFIX = 'channel not found: ';

@ -0,0 +1,141 @@
import {
LogLevel,
createCatalogDiagnostics,
type DiagnosticCatalog,
type DiagnosticEvent,
type Diagnostics
} from '$libs/logger';
import {
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_EVENT_MESSAGE,
CONNECTION_MODULE,
CONNECTION_LOG_MSG_AUTH_FAILED,
CONNECTION_LOG_MSG_BROWSER_RECONNECT,
CONNECTION_LOG_MSG_CONNECT_FAILED,
CONNECTION_LOG_MSG_FRAME_DECODE_FAILED,
CONNECTION_LOG_MSG_FRAME_ENCODE_FAILED,
CONNECTION_LOG_MSG_HEARTBEAT_TIMEOUT,
CONNECTION_LOG_MSG_REAUTH_FAILED,
CONNECTION_LOG_MSG_RECONNECT_EXHAUSTED,
CONNECTION_LOG_MSG_SEND_FAILED,
CONNECTION_LOG_MSG_SESSION_CHANGED,
CONNECTION_LOG_MSG_SESSION_EXPIRED,
CONNECTION_LOG_MSG_SESSION_REFRESHED,
CONNECTION_LOG_MSG_SESSION_REVOKED,
CONNECTION_LOG_MSG_TRANSPORT_ERROR
} from './consts.ts';
import { listenerThrewMessage } from './helpers.ts';
import type { Logger } from '$libs/logger';
export type ConnectionDiagnosticType =
(typeof CONNECTION_DIAGNOSTIC_EVENTS)[keyof typeof CONNECTION_DIAGNOSTIC_EVENTS];
export type ConnectionDiagnosticEvent = DiagnosticEvent<ConnectionDiagnosticType>;
export type ConnectionDiagnostics = Diagnostics<ConnectionDiagnosticEvent>;
const CONNECTION_DIAGNOSTIC_LOGS: DiagnosticCatalog<ConnectionDiagnosticEvent> = {
[CONNECTION_DIAGNOSTIC_EVENTS.AUTH_FAILED]: {
level: LogLevel.WARN,
message: CONNECTION_LOG_MSG_AUTH_FAILED
},
[CONNECTION_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT]: {
level: LogLevel.DEBUG,
message: CONNECTION_LOG_MSG_BROWSER_RECONNECT
},
[CONNECTION_DIAGNOSTIC_EVENTS.CONNECT_FAILED]: {
level: LogLevel.ERROR,
message: CONNECTION_LOG_MSG_CONNECT_FAILED
},
[CONNECTION_DIAGNOSTIC_EVENTS.FRAME_DECODE_FAILED]: {
level: LogLevel.ERROR,
message: CONNECTION_LOG_MSG_FRAME_DECODE_FAILED
},
[CONNECTION_DIAGNOSTIC_EVENTS.FRAME_ENCODE_FAILED]: {
level: LogLevel.ERROR,
message: CONNECTION_LOG_MSG_FRAME_ENCODE_FAILED
},
[CONNECTION_DIAGNOSTIC_EVENTS.HEARTBEAT_TIMEOUT]: {
level: LogLevel.WARN,
message: CONNECTION_LOG_MSG_HEARTBEAT_TIMEOUT
},
[CONNECTION_DIAGNOSTIC_EVENTS.LISTENER_THREW]: (event) => ({
level: LogLevel.ERROR,
message: listenerThrewMessage(listenerEventName(event.meta))
}),
[CONNECTION_DIAGNOSTIC_EVENTS.REAUTH_FAILED]: {
level: LogLevel.WARN,
message: CONNECTION_LOG_MSG_REAUTH_FAILED
},
[CONNECTION_DIAGNOSTIC_EVENTS.RECONNECT_EXHAUSTED]: {
level: LogLevel.WARN,
message: CONNECTION_LOG_MSG_RECONNECT_EXHAUSTED
},
[CONNECTION_DIAGNOSTIC_EVENTS.SEND_FAILED]: {
level: LogLevel.ERROR,
message: CONNECTION_LOG_MSG_SEND_FAILED
},
[CONNECTION_DIAGNOSTIC_EVENTS.SESSION_CHANGED]: {
level: LogLevel.DEBUG,
message: CONNECTION_LOG_MSG_SESSION_CHANGED
},
[CONNECTION_DIAGNOSTIC_EVENTS.SESSION_EXPIRED]: {
level: LogLevel.WARN,
message: CONNECTION_LOG_MSG_SESSION_EXPIRED
},
[CONNECTION_DIAGNOSTIC_EVENTS.SESSION_REFRESHED]: {
level: LogLevel.DEBUG,
message: CONNECTION_LOG_MSG_SESSION_REFRESHED
},
[CONNECTION_DIAGNOSTIC_EVENTS.SESSION_REVOKED]: {
level: LogLevel.WARN,
message: CONNECTION_LOG_MSG_SESSION_REVOKED
},
[CONNECTION_DIAGNOSTIC_EVENTS.TRANSPORT_ERROR]: {
level: LogLevel.ERROR,
message: CONNECTION_LOG_MSG_TRANSPORT_ERROR
}
};
function listenerEventName(meta: unknown): string {
if (typeof meta !== 'object' || meta === null) return CONNECTION_EVENT_MESSAGE;
if (!('event' in meta)) return CONNECTION_EVENT_MESSAGE;
const event = meta.event;
return typeof event === 'string' && event.length > 0 ? event : CONNECTION_EVENT_MESSAGE;
}
export function createConnectionDiagnostics(input: {
readonly logger?: Logger;
readonly scope: string;
}): ConnectionDiagnostics {
return createCatalogDiagnostics({
logger: input.logger,
defaultCategory: input.scope,
catalog: CONNECTION_DIAGNOSTIC_LOGS
});
}
export function emitConnectionDiagnostic(
diagnostics: ConnectionDiagnostics,
type: ConnectionDiagnosticType,
meta?: unknown
): void {
diagnostics.emit({
artifact: CONNECTION_MODULE,
type,
meta
});
}
export function emitScopedConnectionDiagnostic(
diagnostics: ConnectionDiagnostics,
scope: string,
type: ConnectionDiagnosticType,
meta?: unknown
): void {
diagnostics.emit({
artifact: CONNECTION_MODULE,
scope,
type,
meta
});
}

@ -1,24 +1,24 @@
import { createBusSessionSource } from './bus-session-source.ts';
import { createConnection } from './connection.ts';
import {
CONN_AUTO_REAUTH_NONE,
CONN_AUTO_REAUTH_STANDARD,
CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE,
CONN_METHOD_CLOSE_ALL,
CONN_METHOD_CLOSE_CONNECTION,
CONN_METHOD_CONNECTION,
CONN_METHOD_CREATE_CONNECTION,
CONN_METHOD_OPEN_ALL,
CONN_METHOD_OPEN_CONNECTION,
CONN_METHOD_RECONNECT_ALL,
CONN_METHOD_RECONNECT_CONNECTION,
CONN_METHOD_REAUTHENTICATE_ALL
CONNECTION_AUTO_REAUTH_NONE,
CONNECTION_AUTO_REAUTH_STANDARD,
CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE,
CONNECTION_METHOD_CLOSE_ALL,
CONNECTION_METHOD_CLOSE_CONNECTION,
CONNECTION_METHOD_CONNECTION,
CONNECTION_METHOD_CREATE_CONNECTION,
CONNECTION_METHOD_OPEN_ALL,
CONNECTION_METHOD_OPEN_CONNECTION,
CONNECTION_METHOD_RECONNECT_ALL,
CONNECTION_METHOD_RECONNECT_CONNECTION,
CONNECTION_METHOD_REAUTHENTICATE_ALL
} from './consts.ts';
import {
ConnConnectionAlreadyExistsError,
ConnConnectionNotFoundError,
ConnDisposedError,
ConnInvalidConnectionNameError
ConnectionAlreadyExistsError,
ConnectionNotFoundError,
ConnectionDisposedError,
ConnectionInvalidNameError
} from './errors.ts';
import {
alreadyExistsErrorMessage,
@ -37,7 +37,7 @@ import type {
function validateConnectionName(name: unknown): asserts name is string {
if (typeof name !== 'string' || name.trim().length === 0) {
throw new ConnInvalidConnectionNameError(invalidNameErrorMessage(name));
throw new ConnectionInvalidNameError(invalidNameErrorMessage(name));
}
}
@ -92,7 +92,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
let disposed = false;
function ensureLive(method: string): void {
if (disposed) throw new ConnDisposedError(disposedErrorMessage(method));
if (disposed) throw new ConnectionDisposedError(disposedErrorMessage(method));
}
function getConnection(name: string, method: string): Connection {
@ -100,7 +100,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
validateConnectionName(name);
const connection = connections.get(name);
if (connection === undefined) {
throw new ConnConnectionNotFoundError(notFoundErrorMessage(name), name);
throw new ConnectionNotFoundError(notFoundErrorMessage(name), name);
}
return connection;
}
@ -110,10 +110,10 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
name: string,
connectionOptions: ConnectionOptions<TChannels>
): Connection<TChannels> {
ensureLive(CONN_METHOD_CREATE_CONNECTION);
ensureLive(CONNECTION_METHOD_CREATE_CONNECTION);
validateConnectionName(name);
if (connections.has(name)) {
throw new ConnConnectionAlreadyExistsError(alreadyExistsErrorMessage(name), name);
throw new ConnectionAlreadyExistsError(alreadyExistsErrorMessage(name), name);
}
const connection = createConnection<TChannels>(
name,
@ -130,7 +130,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
connection<TChannels extends ConnectionChannelMap = ConnectionChannelMap>(
name: string
): Connection<TChannels> {
return getConnection(name, CONN_METHOD_CONNECTION) as Connection<TChannels>;
return getConnection(name, CONNECTION_METHOD_CONNECTION) as Connection<TChannels>;
},
has(name) {
if (disposed) return false;
@ -141,34 +141,34 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
return [...connections.keys()];
},
openConnection(name) {
return getConnection(name, CONN_METHOD_OPEN_CONNECTION).connect();
return getConnection(name, CONNECTION_METHOD_OPEN_CONNECTION).connect();
},
closeConnection(name, reason) {
getConnection(name, CONN_METHOD_CLOSE_CONNECTION).disconnect(reason);
getConnection(name, CONNECTION_METHOD_CLOSE_CONNECTION).disconnect(reason);
},
reconnectConnection(name, reason) {
return getConnection(name, CONN_METHOD_RECONNECT_CONNECTION).reconnect(reason);
return getConnection(name, CONNECTION_METHOD_RECONNECT_CONNECTION).reconnect(reason);
},
openAll() {
ensureLive(CONN_METHOD_OPEN_ALL);
ensureLive(CONNECTION_METHOD_OPEN_ALL);
return Promise.all([...connections.values()].map((connection) => connection.connect()));
},
closeAll(reason) {
ensureLive(CONN_METHOD_CLOSE_ALL);
ensureLive(CONNECTION_METHOD_CLOSE_ALL);
for (const connection of connections.values()) connection.disconnect(reason);
},
reconnectAll(reason) {
ensureLive(CONN_METHOD_RECONNECT_ALL);
ensureLive(CONNECTION_METHOD_RECONNECT_ALL);
return Promise.all(
[...connections.values()].map((connection) => connection.reconnect(reason))
);
},
reauthenticateAll() {
ensureLive(CONN_METHOD_REAUTHENTICATE_ALL);
ensureLive(CONNECTION_METHOD_REAUTHENTICATE_ALL);
return Promise.all([...connections.values()].map((connection) => connection.reauthenticate()));
},
close(name, reason) {
getConnection(name, CONN_METHOD_CLOSE_CONNECTION).disconnect(reason);
getConnection(name, CONNECTION_METHOD_CLOSE_CONNECTION).disconnect(reason);
},
dispose() {
if (disposed) return;
@ -184,7 +184,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
function shouldWireBusSessionSource(
autoReauthOn: EngineConnectionsOptions['autoReauthOn']
): boolean {
if (autoReauthOn === undefined || autoReauthOn === CONN_AUTO_REAUTH_NONE) return false;
if (autoReauthOn === CONN_AUTO_REAUTH_STANDARD) return true;
return autoReauthOn.includes(CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE);
if (autoReauthOn === undefined || autoReauthOn === CONNECTION_AUTO_REAUTH_NONE) return false;
if (autoReauthOn === CONNECTION_AUTO_REAUTH_STANDARD) return true;
return autoReauthOn.includes(CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE);
}

@ -0,0 +1,111 @@
import { CodeError } from '$libs/errs';
import {
CONNECTION_ERR_CHANNEL_ALREADY_EXISTS,
CONNECTION_ERR_CHANNEL_NOT_FOUND,
CONNECTION_ERR_CONNECTION_ALREADY_EXISTS,
CONNECTION_ERR_CONNECTION_NOT_FOUND,
CONNECTION_ERR_DISPOSED,
CONNECTION_ERR_INVALID_CONNECTION_NAME,
CONNECTION_ERR_INVALID_FRAME,
CONNECTION_ERR_WEBSOCKET_UNAVAILABLE
} from './consts.ts';
export class ConnectionDisposedError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONNECTION_ERR_DISPOSED, { message, cause: options?.cause });
}
}
export class ConnectionAlreadyExistsError extends CodeError {
readonly connection: string;
constructor(message: string, connection: string) {
super(CONNECTION_ERR_CONNECTION_ALREADY_EXISTS, { message });
this.connection = connection;
}
}
export class ConnectionNotFoundError extends CodeError {
readonly connection: string;
constructor(message: string, connection: string) {
super(CONNECTION_ERR_CONNECTION_NOT_FOUND, { message });
this.connection = connection;
}
}
export class ConnectionInvalidNameError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONNECTION_ERR_INVALID_CONNECTION_NAME, { message, cause: options?.cause });
}
}
export class ConnectionInvalidFrameError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONNECTION_ERR_INVALID_FRAME, { message, cause: options?.cause });
}
}
export class ConnectionChannelAlreadyExistsError extends CodeError {
readonly channel: string;
constructor(message: string, channel: string) {
super(CONNECTION_ERR_CHANNEL_ALREADY_EXISTS, { message });
this.channel = channel;
}
}
export class ConnectionChannelNotFoundError extends CodeError {
readonly channel: string;
constructor(message: string, channel: string) {
super(CONNECTION_ERR_CHANNEL_NOT_FOUND, { message });
this.channel = channel;
}
}
export class ConnectionWebSocketUnavailableError extends CodeError {
constructor(message: string, options?: { cause?: unknown }) {
super(CONNECTION_ERR_WEBSOCKET_UNAVAILABLE, { message, cause: options?.cause });
}
}
export function isConnectionDisposedError(value: unknown): value is ConnectionDisposedError {
return value instanceof ConnectionDisposedError;
}
export function isConnectionAlreadyExistsError(
value: unknown
): value is ConnectionAlreadyExistsError {
return value instanceof ConnectionAlreadyExistsError;
}
export function isConnectionNotFoundError(
value: unknown
): value is ConnectionNotFoundError {
return value instanceof ConnectionNotFoundError;
}
export function isConnectionInvalidNameError(
value: unknown
): value is ConnectionInvalidNameError {
return value instanceof ConnectionInvalidNameError;
}
export function isConnectionInvalidFrameError(value: unknown): value is ConnectionInvalidFrameError {
return value instanceof ConnectionInvalidFrameError;
}
export function isConnectionChannelAlreadyExistsError(
value: unknown
): value is ConnectionChannelAlreadyExistsError {
return value instanceof ConnectionChannelAlreadyExistsError;
}
export function isConnectionChannelNotFoundError(
value: unknown
): value is ConnectionChannelNotFoundError {
return value instanceof ConnectionChannelNotFoundError;
}
export function isConnectionWebSocketUnavailableError(
value: unknown
): value is ConnectionWebSocketUnavailableError {
return value instanceof ConnectionWebSocketUnavailableError;
}

@ -1,9 +1,9 @@
import {
CONN_BUFFER_POLICY_BUFFER,
CONN_BUFFER_POLICY_DROP,
CONN_SEND_REASON_BUFFER_FULL,
CONN_DEFAULT_BUFFER_MAX_BYTES,
CONN_DEFAULT_BUFFER_MAX_MESSAGES
CONNECTION_BUFFER_POLICY_BUFFER,
CONNECTION_BUFFER_POLICY_DROP,
CONNECTION_SEND_REASON_BUFFER_FULL,
CONNECTION_DEFAULT_BUFFER_MAX_BYTES,
CONNECTION_DEFAULT_BUFFER_MAX_MESSAGES
} from './consts.ts';
import type { ConnectionBufferOptions, ConnectionFrame, ConnectionSendResult } from './types.ts';
@ -25,7 +25,7 @@ export function createConnectionFrameBuffer(
return {
get maxBytes() {
return options.maxBytes ?? CONN_DEFAULT_BUFFER_MAX_BYTES;
return options.maxBytes ?? CONNECTION_DEFAULT_BUFFER_MAX_BYTES;
},
get length() {
return frames.length;
@ -33,15 +33,15 @@ export function createConnectionFrameBuffer(
canBuffer(frame: ConnectionFrame, allowBuffer: boolean): boolean {
if (!allowBuffer) return false;
if (frame.ack === true) return false;
return options.policy === CONN_BUFFER_POLICY_BUFFER;
return options.policy === CONNECTION_BUFFER_POLICY_BUFFER;
},
shouldDropClosedFrame(): boolean {
return options.policy === CONN_BUFFER_POLICY_DROP;
return options.policy === CONNECTION_BUFFER_POLICY_DROP;
},
push(frame: ConnectionFrame): ConnectionSendResult {
const maxMessages = options.maxMessages ?? CONN_DEFAULT_BUFFER_MAX_MESSAGES;
const maxMessages = options.maxMessages ?? CONNECTION_DEFAULT_BUFFER_MAX_MESSAGES;
if (frames.length >= maxMessages) {
return { ok: false, reason: CONN_SEND_REASON_BUFFER_FULL };
return { ok: false, reason: CONNECTION_SEND_REASON_BUFFER_FULL };
}
frames.push(frame);
return { ok: true, id: frame.id };

@ -1,12 +1,12 @@
import {
CONN_DIAGNOSTIC_EVENTS,
CONN_CLOSE_REASON_HEARTBEAT_TIMEOUT,
CONN_FRAME_TYPE_PING,
CONN_DEFAULT_HEARTBEAT_ENABLED,
CONN_DEFAULT_HEARTBEAT_INTERVAL_MS,
CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS,
CONN_TIMR_KEY_HEARTBEAT,
CONN_TIMR_KEY_HEARTBEAT_TIMEOUT
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_CLOSE_REASON_HEARTBEAT_TIMEOUT,
CONNECTION_FRAME_TYPE_PING,
CONNECTION_DEFAULT_HEARTBEAT_ENABLED,
CONNECTION_DEFAULT_HEARTBEAT_INTERVAL_MS,
CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS,
CONNECTION_TIMER_KEY_HEARTBEAT,
CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import { createFrame } from './serializer.ts';
@ -41,7 +41,7 @@ export function createConnectionHeartbeat(
runtime.heartbeatOptions === false ? undefined : runtime.heartbeatOptions?.pingType;
const result = await runtime.sendFrame(
createFrame({
type: pingType ?? CONN_FRAME_TYPE_PING,
type: pingType ?? CONNECTION_FRAME_TYPE_PING,
payload: undefined
}),
false
@ -49,29 +49,29 @@ export function createConnectionHeartbeat(
if (!result.ok) return;
const timeoutMs =
runtime.heartbeatOptions === false
? CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS
: (runtime.heartbeatOptions?.timeoutMs ?? CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS);
runtime.scheduleTimer(CONN_TIMR_KEY_HEARTBEAT_TIMEOUT, timeoutMs, () => {
emitConnectionDiagnostic(runtime.diagnostics, CONN_DIAGNOSTIC_EVENTS.HEARTBEAT_TIMEOUT);
runtime.closeTransport(CONN_CLOSE_REASON_HEARTBEAT_TIMEOUT);
? CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS
: (runtime.heartbeatOptions?.timeoutMs ?? CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS);
runtime.scheduleTimer(CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT, timeoutMs, () => {
emitConnectionDiagnostic(runtime.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.HEARTBEAT_TIMEOUT);
runtime.closeTransport(CONNECTION_CLOSE_REASON_HEARTBEAT_TIMEOUT);
});
}
return {
received(): void {
runtime.cancelTimer(CONN_TIMR_KEY_HEARTBEAT_TIMEOUT);
runtime.cancelTimer(CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT);
},
start(): void {
if (runtime.heartbeatOptions === false) return;
if ((runtime.heartbeatOptions?.enabled ?? CONN_DEFAULT_HEARTBEAT_ENABLED) === false) return;
const intervalMs = runtime.heartbeatOptions?.intervalMs ?? CONN_DEFAULT_HEARTBEAT_INTERVAL_MS;
runtime.scheduleInterval(CONN_TIMR_KEY_HEARTBEAT, intervalMs, () => {
if ((runtime.heartbeatOptions?.enabled ?? CONNECTION_DEFAULT_HEARTBEAT_ENABLED) === false) return;
const intervalMs = runtime.heartbeatOptions?.intervalMs ?? CONNECTION_DEFAULT_HEARTBEAT_INTERVAL_MS;
runtime.scheduleInterval(CONNECTION_TIMER_KEY_HEARTBEAT, intervalMs, () => {
void sendHeartbeat();
});
},
stop(): void {
runtime.cancelTimer(CONN_TIMR_KEY_HEARTBEAT);
runtime.cancelTimer(CONN_TIMR_KEY_HEARTBEAT_TIMEOUT);
runtime.cancelTimer(CONNECTION_TIMER_KEY_HEARTBEAT);
runtime.cancelTimer(CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT);
}
};
}

@ -0,0 +1,62 @@
import {
CONNECTION_ERROR_MSG_ALREADY_EXISTS_PREFIX,
CONNECTION_ERROR_MSG_CHANNEL_ALREADY_EXISTS_PREFIX,
CONNECTION_ERROR_MSG_CHANNEL_NOT_FOUND_PREFIX,
CONNECTION_ERROR_MSG_DISPOSED_SUFFIX,
CONNECTION_ERROR_MSG_INVALID_NAME_PREFIX,
CONNECTION_ERROR_MSG_NOT_FOUND_PREFIX,
CONNECTION_ERROR_PREFIX,
CONNECTION_ID_PREFIX,
CONNECTION_ID_SEPARATOR,
CONNECTION_MODULE,
CONNECTION_LOGGER_SCOPE_SEPARATOR,
CONNECTION_LOG_MSG_LISTENER_THREW_PREFIX
} from './consts.ts';
export function disposedErrorMessage(method: string): string {
return `${CONNECTION_ERROR_PREFIX}${method}${CONNECTION_ERROR_MSG_DISPOSED_SUFFIX}`;
}
export function alreadyExistsErrorMessage(name: string): string {
return `${CONNECTION_ERROR_PREFIX}${CONNECTION_ERROR_MSG_ALREADY_EXISTS_PREFIX}${name}`;
}
export function notFoundErrorMessage(name: string): string {
return `${CONNECTION_ERROR_PREFIX}${CONNECTION_ERROR_MSG_NOT_FOUND_PREFIX}${name}`;
}
export function invalidNameErrorMessage(name: unknown): string {
return `${CONNECTION_ERROR_PREFIX}${CONNECTION_ERROR_MSG_INVALID_NAME_PREFIX}${String(name)}`;
}
export function channelAlreadyExistsErrorMessage(name: string): string {
return `${CONNECTION_ERROR_PREFIX}${CONNECTION_ERROR_MSG_CHANNEL_ALREADY_EXISTS_PREFIX}${name}`;
}
export function channelNotFoundErrorMessage(name: string): string {
return `${CONNECTION_ERROR_PREFIX}${CONNECTION_ERROR_MSG_CHANNEL_NOT_FOUND_PREFIX}${name}`;
}
export function listenerThrewMessage(event: string): string {
return `${CONNECTION_LOG_MSG_LISTENER_THREW_PREFIX}${event}`;
}
export function loggerScope(name: string, topic?: string): string {
return topic === undefined
? `${CONNECTION_MODULE}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${name}`
: `${CONNECTION_MODULE}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${name}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${topic}`;
}
export function timerKey(connection: string, kind: string, id?: string): string {
return id === undefined
? `${CONNECTION_MODULE}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${connection}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${kind}`
: `${CONNECTION_MODULE}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${connection}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${kind}${CONNECTION_LOGGER_SCOPE_SEPARATOR}${id}`;
}
export function createConnectionIdFactory(connectionName: string): () => string {
let next = 0;
return () => {
next += 1;
return [CONNECTION_ID_PREFIX, connectionName, String(next)].join(CONNECTION_ID_SEPARATOR);
};
}

@ -1,10 +1,10 @@
import { computeBackoffDelay } from '$libs/timers';
import { computeBackoffDelay } from '$libs/timer';
import {
CONN_DEFAULT_RECONNECT_ENABLED,
CONN_DEFAULT_RECONNECT_FACTOR,
CONN_DEFAULT_RECONNECT_JITTER_MS,
CONN_DEFAULT_RECONNECT_MAX_DELAY_MS,
CONN_DEFAULT_RECONNECT_MIN_DELAY_MS
CONNECTION_DEFAULT_RECONNECT_ENABLED,
CONNECTION_DEFAULT_RECONNECT_FACTOR,
CONNECTION_DEFAULT_RECONNECT_JITTER_MS,
CONNECTION_DEFAULT_RECONNECT_MAX_DELAY_MS,
CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_MS
} from './consts.ts';
import type { ConnectionOptions, ConnectionReconnectOptions } from './types.ts';
@ -39,7 +39,7 @@ export function createConnectionReconnectPolicy(
isEnabled(input) {
if (input.disposed || input.intentionalClose) return false;
if (disabled) return false;
return options?.enabled ?? CONN_DEFAULT_RECONNECT_ENABLED;
return options?.enabled ?? CONNECTION_DEFAULT_RECONNECT_ENABLED;
},
next(currentAttempt) {
const maxAttempts = options?.maxAttempts;
@ -51,10 +51,10 @@ export function createConnectionReconnectPolicy(
ok: true,
attempt,
delayMs: computeBackoffDelay(attempt - 1, {
minDelayMs: options?.minDelayMs ?? CONN_DEFAULT_RECONNECT_MIN_DELAY_MS,
maxDelayMs: options?.maxDelayMs ?? CONN_DEFAULT_RECONNECT_MAX_DELAY_MS,
factor: options?.factor ?? CONN_DEFAULT_RECONNECT_FACTOR,
jitterMs: options?.jitterMs ?? CONN_DEFAULT_RECONNECT_JITTER_MS
minDelayMs: options?.minDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_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
})
};
}

@ -1,12 +1,12 @@
import { isPromiseLike } from '$libs/standard-schema';
import {
CONN_DIAGNOSTIC_EVENTS,
CONN_SEND_REASON_BUFFER_FULL,
CONN_SEND_REASON_CLOSED,
CONN_SEND_REASON_INVALID_FRAME,
CONN_SEND_REASON_SEND_NOT_SUPPORTED,
CONN_SEND_REASON_SERIALIZE_FAILED,
CONN_SEND_REASON_TRANSPORT_ERROR
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_SEND_REASON_BUFFER_FULL,
CONNECTION_SEND_REASON_CLOSED,
CONNECTION_SEND_REASON_INVALID_FRAME,
CONNECTION_SEND_REASON_SEND_NOT_SUPPORTED,
CONNECTION_SEND_REASON_SERIALIZE_FAILED,
CONNECTION_SEND_REASON_TRANSPORT_ERROR
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionFrameBuffer } from './frame-buffer.ts';
@ -40,21 +40,21 @@ export function createConnectionSender(runtime: ConnectionSenderRuntime): Connec
try {
createFrame(frame);
} catch (err) {
return { ok: false, reason: CONN_SEND_REASON_INVALID_FRAME, error: err };
return { ok: false, reason: CONNECTION_SEND_REASON_INVALID_FRAME, error: err };
}
if (!runtime.isConnected()) {
if (runtime.frameBuffer.canBuffer(frame, allowBuffer)) return runtime.frameBuffer.push(frame);
if (runtime.frameBuffer.shouldDropClosedFrame()) return { ok: true, id: frame.id };
return { ok: false, reason: CONN_SEND_REASON_CLOSED };
return { ok: false, reason: CONNECTION_SEND_REASON_CLOSED };
}
const currentTransport = runtime.getTransport();
if (currentTransport === null || !currentTransport.canSend) {
return { ok: false, reason: CONN_SEND_REASON_SEND_NOT_SUPPORTED };
return { ok: false, reason: CONNECTION_SEND_REASON_SEND_NOT_SUPPORTED };
}
if (currentTransport.bufferedAmount > runtime.frameBuffer.maxBytes) {
return { ok: false, reason: CONN_SEND_REASON_BUFFER_FULL };
return { ok: false, reason: CONNECTION_SEND_REASON_BUFFER_FULL };
}
let encoded: string | ArrayBuffer;
@ -63,10 +63,10 @@ export function createConnectionSender(runtime: ConnectionSenderRuntime): Connec
} catch (err) {
emitConnectionDiagnostic(
runtime.diagnostics,
CONN_DIAGNOSTIC_EVENTS.FRAME_ENCODE_FAILED,
CONNECTION_DIAGNOSTIC_EVENTS.FRAME_ENCODE_FAILED,
{ error: err }
);
return { ok: false, reason: CONN_SEND_REASON_SERIALIZE_FAILED, error: err };
return { ok: false, reason: CONNECTION_SEND_REASON_SERIALIZE_FAILED, error: err };
}
try {
@ -75,10 +75,10 @@ export function createConnectionSender(runtime: ConnectionSenderRuntime): Connec
return { ok: true, id: frame.id };
} catch (err) {
runtime.setError(err);
emitConnectionDiagnostic(runtime.diagnostics, CONN_DIAGNOSTIC_EVENTS.SEND_FAILED, {
emitConnectionDiagnostic(runtime.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.SEND_FAILED, {
error: err
});
return { ok: false, reason: CONN_SEND_REASON_TRANSPORT_ERROR, error: err };
return { ok: false, reason: CONNECTION_SEND_REASON_TRANSPORT_ERROR, error: err };
}
}

@ -0,0 +1,87 @@
import {
CONNECTION_FRAME_KEY_ACK,
CONNECTION_FRAME_KEY_ERROR,
CONNECTION_FRAME_KEY_ID,
CONNECTION_FRAME_KEY_PAYLOAD,
CONNECTION_FRAME_KEY_REPLY_TO,
CONNECTION_FRAME_KEY_TOPIC,
CONNECTION_FRAME_KEY_TS,
CONNECTION_FRAME_KEY_TYPE,
CONNECTION_ERROR_MSG_FRAME_ACK_MUST_BE_BOOLEAN,
CONNECTION_ERROR_MSG_FRAME_ERROR_MUST_BE_OBJECT,
CONNECTION_ERROR_MSG_FRAME_ID_MUST_BE_STRING,
CONNECTION_ERROR_MSG_FRAME_MUST_BE_OBJECT,
CONNECTION_ERROR_MSG_FRAME_REPLY_TO_MUST_BE_STRING,
CONNECTION_ERROR_MSG_FRAME_TOPIC_MUST_BE_STRING,
CONNECTION_ERROR_MSG_FRAME_TS_MUST_BE_NUMBER,
CONNECTION_ERROR_MSG_FRAME_TYPE_MUST_BE_STRING
} from './consts.ts';
import { ConnectionInvalidFrameError } from './errors.ts';
import type { ConnectionFrame } from './types.ts';
export function assertConnectionFrame(value: unknown): asserts value is ConnectionFrame {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_MUST_BE_OBJECT);
}
const frame = value as Record<string, unknown>;
if (typeof frame[CONNECTION_FRAME_KEY_TYPE] !== 'string' || frame[CONNECTION_FRAME_KEY_TYPE].length === 0) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_TYPE_MUST_BE_STRING);
}
if (
CONNECTION_FRAME_KEY_ID in frame &&
frame[CONNECTION_FRAME_KEY_ID] !== undefined &&
typeof frame[CONNECTION_FRAME_KEY_ID] !== 'string'
) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_ID_MUST_BE_STRING);
}
if (
CONNECTION_FRAME_KEY_TOPIC in frame &&
frame[CONNECTION_FRAME_KEY_TOPIC] !== undefined &&
typeof frame[CONNECTION_FRAME_KEY_TOPIC] !== 'string'
) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_TOPIC_MUST_BE_STRING);
}
if (
CONNECTION_FRAME_KEY_REPLY_TO in frame &&
frame[CONNECTION_FRAME_KEY_REPLY_TO] !== undefined &&
typeof frame[CONNECTION_FRAME_KEY_REPLY_TO] !== 'string'
) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_REPLY_TO_MUST_BE_STRING);
}
if (
CONNECTION_FRAME_KEY_ACK in frame &&
frame[CONNECTION_FRAME_KEY_ACK] !== undefined &&
typeof frame[CONNECTION_FRAME_KEY_ACK] !== 'boolean'
) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_ACK_MUST_BE_BOOLEAN);
}
if (
CONNECTION_FRAME_KEY_TS in frame &&
frame[CONNECTION_FRAME_KEY_TS] !== undefined &&
typeof frame[CONNECTION_FRAME_KEY_TS] !== 'number'
) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_TS_MUST_BE_NUMBER);
}
if (
CONNECTION_FRAME_KEY_ERROR in frame &&
frame[CONNECTION_FRAME_KEY_ERROR] !== undefined &&
(frame[CONNECTION_FRAME_KEY_ERROR] === null || typeof frame[CONNECTION_FRAME_KEY_ERROR] !== 'object')
) {
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_FRAME_ERROR_MUST_BE_OBJECT);
}
}
export function createFrame(input: ConnectionFrame): ConnectionFrame {
assertConnectionFrame(input);
const frame: Record<string, unknown> = {
[CONNECTION_FRAME_KEY_TYPE]: input.type,
[CONNECTION_FRAME_KEY_PAYLOAD]: input.payload
};
if (input.id !== undefined) frame[CONNECTION_FRAME_KEY_ID] = input.id;
if (input.topic !== undefined) frame[CONNECTION_FRAME_KEY_TOPIC] = input.topic;
if (input.ts !== undefined) frame[CONNECTION_FRAME_KEY_TS] = input.ts;
if (input.ack !== undefined) frame[CONNECTION_FRAME_KEY_ACK] = input.ack;
if (input.replyTo !== undefined) frame[CONNECTION_FRAME_KEY_REPLY_TO] = input.replyTo;
if (input.error !== undefined) frame[CONNECTION_FRAME_KEY_ERROR] = input.error;
return frame as unknown as ConnectionFrame;
}

@ -1,5 +1,5 @@
import { CONN_ERROR_MSG_JSON_DECODE_FAILED } from '../consts.ts';
import { ConnInvalidFrameError } from '../errors.ts';
import { CONNECTION_ERROR_MSG_JSON_DECODE_FAILED } from '../consts.ts';
import { ConnectionInvalidFrameError } from '../errors.ts';
import { assertConnectionFrame, createFrame } from '../serializer.ts';
import type { ConnectionFrame, ConnectionSerializer } from '../types.ts';
@ -18,7 +18,7 @@ export const jsonConnectionSerializer: ConnectionSerializer<ConnectionFrame> = {
try {
parsed = JSON.parse(rawToText(raw));
} catch (cause) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_JSON_DECODE_FAILED, { cause });
throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_JSON_DECODE_FAILED, { cause });
}
assertConnectionFrame(parsed);
return createFrame(parsed);

@ -1,12 +1,12 @@
import {
CONN_DIAGNOSTIC_EVENTS,
CONN_CLOSE_REASON_SESSION_EXPIRED,
CONN_SESS_EVENT_ADOPTED,
CONN_SESS_EVENT_ADOPTED_SERVER,
CONN_SESS_EVENT_EXPIRED,
CONN_SESS_EVENT_EXTERNAL_CHANGED,
CONN_SESS_EVENT_REFRESHED,
CONN_SESS_EVENT_REVOKED
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_CLOSE_REASON_SESSION_EXPIRED,
CONNECTION_SESSION_EVENT_ADOPTED,
CONNECTION_SESSION_EVENT_ADOPTED_SERVER,
CONNECTION_SESSION_EVENT_EXPIRED,
CONNECTION_SESSION_EVENT_EXTERNAL_CHANGED,
CONNECTION_SESSION_EVENT_REFRESHED,
CONNECTION_SESSION_EVENT_REVOKED
} from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type {
@ -24,11 +24,11 @@ interface ConnectionSessionWiringOptions {
readonly diagnostics: ConnectionDiagnostics;
}
const SESS_REAUTH_EVENTS = new Set<string>([
CONN_SESS_EVENT_ADOPTED,
CONN_SESS_EVENT_ADOPTED_SERVER,
CONN_SESS_EVENT_EXTERNAL_CHANGED,
CONN_SESS_EVENT_REFRESHED
const SESSION_REAUTH_EVENTS = new Set<string>([
CONNECTION_SESSION_EVENT_ADOPTED,
CONNECTION_SESSION_EVENT_ADOPTED_SERVER,
CONNECTION_SESSION_EVENT_EXTERNAL_CHANGED,
CONNECTION_SESSION_EVENT_REFRESHED
]);
export function wireConnectionSession(options: ConnectionSessionWiringOptions): () => void {
@ -44,17 +44,17 @@ export function wireConnectionSession(options: ConnectionSessionWiringOptions):
return options.source.onChange((change) => {
if (
(change.event === CONN_SESS_EVENT_EXPIRED || change.event === CONN_SESS_EVENT_REVOKED) &&
(change.event === CONNECTION_SESSION_EVENT_EXPIRED || change.event === CONNECTION_SESSION_EVENT_REVOKED) &&
sessionOptions.disconnectOnExpire !== false
) {
emitConnectionDiagnostic(
options.diagnostics,
change.event === CONN_SESS_EVENT_EXPIRED
? CONN_DIAGNOSTIC_EVENTS.SESS_EXPIRED
: CONN_DIAGNOSTIC_EVENTS.SESS_REVOKED,
change.event === CONNECTION_SESSION_EVENT_EXPIRED
? CONNECTION_DIAGNOSTIC_EVENTS.SESSION_EXPIRED
: CONNECTION_DIAGNOSTIC_EVENTS.SESSION_REVOKED,
{ event: change.event }
);
options.disconnect(CONN_CLOSE_REASON_SESSION_EXPIRED);
options.disconnect(CONNECTION_CLOSE_REASON_SESSION_EXPIRED);
return;
}
@ -65,10 +65,10 @@ export function wireConnectionSession(options: ConnectionSessionWiringOptions):
) {
emitConnectionDiagnostic(
options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.SESS_REVOKED,
CONNECTION_DIAGNOSTIC_EVENTS.SESSION_REVOKED,
{ event: change.event }
);
options.disconnect(CONN_CLOSE_REASON_SESSION_EXPIRED);
options.disconnect(CONNECTION_CLOSE_REASON_SESSION_EXPIRED);
return;
}
@ -76,16 +76,16 @@ export function wireConnectionSession(options: ConnectionSessionWiringOptions):
emitConnectionDiagnostic(
options.diagnostics,
change.event === CONN_SESS_EVENT_REFRESHED
? CONN_DIAGNOSTIC_EVENTS.SESS_REFRESHED
: CONN_DIAGNOSTIC_EVENTS.SESS_CHANGED,
change.event === CONNECTION_SESSION_EVENT_REFRESHED
? CONNECTION_DIAGNOSTIC_EVENTS.SESSION_REFRESHED
: CONNECTION_DIAGNOSTIC_EVENTS.SESSION_CHANGED,
{ event: change.event }
);
void options.reauthenticate().then((result) => {
if (!result.ok) {
emitConnectionDiagnostic(
options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.REAUTH_FAILED,
CONNECTION_DIAGNOSTIC_EVENTS.REAUTH_FAILED,
result
);
}
@ -98,8 +98,8 @@ function shouldReauthenticate(
sessionOptions: ConnectionSessionOptions,
hasAuthProvider: () => boolean
): boolean {
if (!SESS_REAUTH_EVENTS.has(event)) return false;
if (!SESSION_REAUTH_EVENTS.has(event)) return false;
if (!hasAuthProvider()) return false;
if (event === CONN_SESS_EVENT_REFRESHED) return sessionOptions.reauthOnRefresh !== false;
if (event === CONNECTION_SESSION_EVENT_REFRESHED) return sessionOptions.reauthOnRefresh !== false;
return sessionOptions.reauthOnChange !== false;
}

@ -1,13 +1,13 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
CONN_STATE_CLOSED,
CONN_STATE_IDLE,
CONN_STATE_OPEN,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_IDLE,
CONNECTION_STATE_OPEN,
createMockTransport
} from '../index.ts';
import { createActiveConnections } from '../active-connections.svelte.ts';
import { createEngineTimers } from '$timr';
import type { EngineTimers } from '$libs/timers';
import { createEngineTimers } from '$timer';
import type { EngineTimers } from '$libs/timer';
let timers: EngineTimers;
beforeEach(() => {
@ -34,8 +34,8 @@ describe('createActiveConnections', () => {
expect(Connections.size).toBe(2);
expect(Connections.activeNames).toEqual(['main', 'chat']);
expect(Connections.states).toEqual({
main: CONN_STATE_IDLE,
chat: CONN_STATE_IDLE
main: CONNECTION_STATE_IDLE,
chat: CONNECTION_STATE_IDLE
});
expect(Connections.allConnected).toBe(false);
expect(Connections.anyConnected).toBe(false);
@ -49,7 +49,7 @@ describe('createActiveConnections', () => {
Main.disconnect();
expect(Connections.closedNames).toEqual(['main']);
expect(Connections.states.main).toBe(CONN_STATE_CLOSED);
expect(Connections.states.main).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
expect(Connections.size).toBe(0);
@ -66,11 +66,11 @@ describe('createActiveConnections', () => {
});
await Connections.openConnection('main');
expect(Connections.states.main).toBe(CONN_STATE_OPEN);
expect(Connections.states.main).toBe(CONNECTION_STATE_OPEN);
expect(Connections.allConnected).toBe(true);
Connections.closeConnection('main');
expect(Connections.states.main).toBe(CONN_STATE_CLOSED);
expect(Connections.states.main).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
});

@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import {
CONN_STATE_CLOSED,
CONN_STATE_OPEN,
CONN_STATE_RECONNECTING
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_OPEN,
CONNECTION_STATE_RECONNECTING
} from '../consts.ts';
import { createConnectionStateTracker } from '../connection-state.ts';
import type { ConnectionStateChange } from '../types.ts';
@ -20,17 +20,17 @@ describe('ConnectionStateTracker', () => {
});
expect(tracker.markOpen()).toBe(true);
expect(tracker.state).toBe(CONN_STATE_OPEN);
expect(tracker.state).toBe(CONNECTION_STATE_OPEN);
expect(tracker.generation).toBe(1);
expect(tracker.openedAt).toBe(10);
now = 20;
expect(tracker.markClosed(CONN_STATE_CLOSED)).toBe(true);
expect(tracker.state).toBe(CONN_STATE_CLOSED);
expect(tracker.markClosed(CONNECTION_STATE_CLOSED)).toBe(true);
expect(tracker.state).toBe(CONNECTION_STATE_CLOSED);
expect(tracker.closedAt).toBe(20);
expect(changes.map((change) => change.to)).toEqual([
CONN_STATE_OPEN,
CONN_STATE_CLOSED
CONNECTION_STATE_OPEN,
CONNECTION_STATE_CLOSED
]);
});
@ -43,10 +43,10 @@ describe('ConnectionStateTracker', () => {
});
tracker.markOpen();
tracker.markClosed(CONN_STATE_CLOSED);
tracker.markClosed(CONNECTION_STATE_CLOSED);
now = 200;
expect(tracker.markClosed(CONN_STATE_CLOSED)).toBe(false);
expect(tracker.markClosed(CONNECTION_STATE_CLOSED)).toBe(false);
expect(tracker.closedAt).toBe(100);
});
@ -62,7 +62,7 @@ describe('ConnectionStateTracker', () => {
tracker.markOpen();
order.length = 0;
tracker.markClosed(CONN_STATE_RECONNECTING, undefined, () => {
tracker.markClosed(CONNECTION_STATE_RECONNECTING, undefined, () => {
order.push('cleanup');
});

@ -1,33 +1,33 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
CONN_ACK_REASON_CLOSED,
CONN_ACK_REASON_TIMEOUT,
CONN_BUFFER_POLICY_BUFFER,
CONN_CHANNEL_STATE_LEFT,
CONN_CONNECT_REASON_AUTH_FAILED,
CONN_FRAME_TYPE_JOIN,
CONN_SEND_REASON_CLOSED,
CONN_STATE_CLOSED,
CONN_STATE_FAILED,
CONN_STATE_OPEN,
CONN_STATE_RECONNECTING,
CONN_AUTO_REAUTH_STANDARD,
CONN_SESS_EVENT_EXPIRED,
CONN_SESS_EVENT_REVOKED,
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_ACK_REASON_TIMEOUT,
CONNECTION_BUFFER_POLICY_BUFFER,
CONNECTION_CHANNEL_STATE_LEFT,
CONNECTION_CONNECT_REASON_AUTH_FAILED,
CONNECTION_FRAME_TYPE_JOIN,
CONNECTION_SEND_REASON_CLOSED,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_FAILED,
CONNECTION_STATE_OPEN,
CONNECTION_STATE_RECONNECTING,
CONNECTION_AUTO_REAUTH_STANDARD,
CONNECTION_SESSION_EVENT_EXPIRED,
CONNECTION_SESSION_EVENT_REVOKED,
createEngineConnections,
createFrame,
createMockTransport
} from '../index.ts';
import {
createEngineBus
} from '$buss';
} from '$bus';
import {
AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
APP_USER_IDENTITY_CAUSE_SESSION_REVOKED,
publishAppUserIdentityChanged,
type AppEventMap
} from '$libs/aapp/events';
import { createEngineTimers } from '$timr';
import type { EngineTimers, TimerClock } from '$libs/timers';
} from '$libs/active-app/events';
import { createEngineTimers } from '$timer';
import type { EngineTimers, TimerClock } from '$libs/timer';
import type { ConnectionFrame, MockConnectionTransport } from '../index.ts';
function latestFrame(transport: MockConnectionTransport): ConnectionFrame {
@ -60,12 +60,12 @@ describe('Connection lifecycle', () => {
const result = await Main.connect();
expect(result.ok).toBe(true);
expect(Main.state).toBe(CONN_STATE_OPEN);
expect(Main.state).toBe(CONNECTION_STATE_OPEN);
expect(Main.connected).toBe(true);
Main.disconnect();
expect(Main.state).toBe(CONN_STATE_CLOSED);
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
expect(Main.connected).toBe(false);
Connections.dispose();
@ -138,7 +138,7 @@ describe('Connection lifecycle', () => {
Main.disconnect();
await vi.advanceTimersByTimeAsync(10);
expect(Main.state).toBe(CONN_STATE_CLOSED);
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
});
@ -157,11 +157,11 @@ describe('Connection lifecycle', () => {
const firstGeneration = Main.generation;
transport.emitClose({ clean: false });
expect(Main.state).toBe(CONN_STATE_RECONNECTING);
expect(Main.state).toBe(CONNECTION_STATE_RECONNECTING);
await vi.advanceTimersByTimeAsync(1);
expect(Main.state).toBe(CONN_STATE_OPEN);
expect(Main.state).toBe(CONNECTION_STATE_OPEN);
expect(Main.generation).toBe(firstGeneration + 1);
Connections.dispose();
@ -187,9 +187,9 @@ describe('Connection lifecycle', () => {
expect(result).toMatchObject({
ok: false,
reason: CONN_CONNECT_REASON_AUTH_FAILED
reason: CONNECTION_CONNECT_REASON_AUTH_FAILED
});
expect(Main.state).toBe(CONN_STATE_FAILED);
expect(Main.state).toBe(CONNECTION_STATE_FAILED);
expect(transport.canSend).toBe(false);
Connections.dispose();
@ -229,7 +229,7 @@ describe('Connection send/request', () => {
const result = await Main.send('demo.event', undefined);
expect(result).toEqual({ ok: false, reason: CONN_SEND_REASON_CLOSED });
expect(result).toEqual({ ok: false, reason: CONNECTION_SEND_REASON_CLOSED });
Connections.dispose();
});
@ -241,7 +241,7 @@ describe('Connection send/request', () => {
transport,
heartbeat: false,
reconnect: false,
buffer: { policy: CONN_BUFFER_POLICY_BUFFER }
buffer: { policy: CONNECTION_BUFFER_POLICY_BUFFER }
});
const buffered = await Main.send('demo.buffered', { queued: true });
@ -298,7 +298,7 @@ describe('Connection send/request', () => {
const pending = Main.request('demo.request', undefined, { timeoutMs: 5 });
await vi.advanceTimersByTimeAsync(5);
await expect(pending).resolves.toEqual({ ok: false, reason: CONN_ACK_REASON_TIMEOUT });
await expect(pending).resolves.toEqual({ ok: false, reason: CONNECTION_ACK_REASON_TIMEOUT });
Connections.dispose();
});
@ -315,7 +315,7 @@ describe('Connection send/request', () => {
const pending = Main.request('demo.request', undefined);
Main.disconnect();
await expect(pending).resolves.toEqual({ ok: false, reason: CONN_ACK_REASON_CLOSED });
await expect(pending).resolves.toEqual({ ok: false, reason: CONNECTION_ACK_REASON_CLOSED });
Connections.dispose();
});
@ -378,7 +378,7 @@ describe('Connection channels', () => {
expect(result.ok).toBe(true);
expect(latestFrame(transport)).toMatchObject({
topic: 'orders',
type: CONN_FRAME_TYPE_JOIN,
type: CONNECTION_FRAME_TYPE_JOIN,
payload: { tenant: 'acme' }
});
@ -398,7 +398,7 @@ describe('Connection channels', () => {
await Orders.join();
Orders.dispose();
expect(Orders.state).toBe(CONN_CHANNEL_STATE_LEFT);
expect(Orders.state).toBe(CONNECTION_CHANNEL_STATE_LEFT);
Connections.dispose();
});
@ -409,7 +409,7 @@ describe('Connection session bridge', () => {
const Bus = createEngineBus<AppEventMap>();
const Connections = createEngineConnections({
bus: Bus,
autoReauthOn: CONN_AUTO_REAUTH_STANDARD,
autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD,
timers
});
const Main = Connections.createConnection('main', {
@ -421,13 +421,13 @@ describe('Connection session bridge', () => {
await Main.connect();
publishAppUserIdentityChanged(Bus, {
event: CONN_SESS_EVENT_REVOKED,
event: CONNECTION_SESSION_EVENT_REVOKED,
generation: 1,
identity: { from: 'identified', to: 'none' },
cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED
cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED
});
expect(Main.state).toBe(CONN_STATE_CLOSED);
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
Bus.dispose();
@ -454,9 +454,9 @@ describe('Connection session bridge', () => {
});
await Main.connect();
listener?.({ event: CONN_SESS_EVENT_EXPIRED });
listener?.({ event: CONNECTION_SESSION_EVENT_EXPIRED });
expect(Main.state).toBe(CONN_STATE_CLOSED);
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
});

@ -1,14 +1,14 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
CONN_STATE_CLOSED,
CONN_STATE_OPEN,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_OPEN,
createEngineConnections,
createMockTransport,
isConnConnectionAlreadyExistsError,
isConnConnectionNotFoundError
isConnectionAlreadyExistsError,
isConnectionNotFoundError
} from '../index.ts';
import { createEngineTimers } from '$timr';
import type { EngineTimers } from '$libs/timers';
import { createEngineTimers } from '$timer';
import type { EngineTimers } from '$libs/timer';
let timers: EngineTimers;
beforeEach(() => {
@ -51,7 +51,7 @@ describe('createEngineConnections', () => {
reconnect: false
});
} catch (error) {
expect(isConnConnectionAlreadyExistsError(error)).toBe(true);
expect(isConnectionAlreadyExistsError(error)).toBe(true);
}
Connections.dispose();
@ -63,7 +63,7 @@ describe('createEngineConnections', () => {
try {
Connections.connection('missing');
} catch (error) {
expect(isConnConnectionNotFoundError(error)).toBe(true);
expect(isConnectionNotFoundError(error)).toBe(true);
}
Connections.dispose();
@ -84,13 +84,13 @@ describe('createEngineConnections', () => {
const results = await Connections.openAll();
expect(results.every((result) => result.ok)).toBe(true);
expect(Connections.connection('main').state).toBe(CONN_STATE_OPEN);
expect(Connections.connection('chat').state).toBe(CONN_STATE_OPEN);
expect(Connections.connection('main').state).toBe(CONNECTION_STATE_OPEN);
expect(Connections.connection('chat').state).toBe(CONNECTION_STATE_OPEN);
Connections.closeAll();
expect(Connections.connection('main').state).toBe(CONN_STATE_CLOSED);
expect(Connections.connection('chat').state).toBe(CONN_STATE_CLOSED);
expect(Connections.connection('main').state).toBe(CONNECTION_STATE_CLOSED);
expect(Connections.connection('chat').state).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
});
@ -107,7 +107,7 @@ describe('createEngineConnections', () => {
await Main.connect();
close('main');
expect(Main.state).toBe(CONN_STATE_CLOSED);
expect(Main.state).toBe(CONNECTION_STATE_CLOSED);
Connections.dispose();
});

@ -1,8 +1,8 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
ConnWebSocketUnavailableError,
ConnectionWebSocketUnavailableError,
createWebSocketTransport,
isConnWebSocketUnavailableError
isConnectionWebSocketUnavailableError
} from '../index.ts';
afterEach(() => {
@ -15,11 +15,11 @@ describe('createWebSocketTransport', () => {
const transport = createWebSocketTransport({ url: 'ws://example.test/socket' });
expect(() => transport.open()).toThrow(ConnWebSocketUnavailableError);
expect(() => transport.open()).toThrow(ConnectionWebSocketUnavailableError);
try {
transport.open();
} catch (error) {
expect(isConnWebSocketUnavailableError(error)).toBe(true);
expect(isConnectionWebSocketUnavailableError(error)).toBe(true);
}
});
});

@ -1,12 +1,12 @@
import {
CONN_TRANSPORT_STATE_CLOSED,
CONN_TRANSPORT_STATE_CLOSING,
CONN_TRANSPORT_STATE_IDLE,
CONN_TRANSPORT_STATE_OPEN,
CONN_TRANSPORT_STATE_OPENING,
CONN_MOCK_TRANSPORT_DEFAULT_CLOSE_CLEAN,
CONN_MOCK_TRANSPORT_DEFAULT_CLOSE_CODE,
CONN_TRANSPORT_KIND_MOCK
CONNECTION_TRANSPORT_STATE_CLOSED,
CONNECTION_TRANSPORT_STATE_CLOSING,
CONNECTION_TRANSPORT_STATE_IDLE,
CONNECTION_TRANSPORT_STATE_OPEN,
CONNECTION_TRANSPORT_STATE_OPENING,
CONNECTION_MOCK_TRANSPORT_DEFAULT_CLOSE_CLEAN,
CONNECTION_MOCK_TRANSPORT_DEFAULT_CLOSE_CODE,
CONNECTION_TRANSPORT_KIND_MOCK
} from '../consts.ts';
import { createTransportEmitter } from '../transport.ts';
import type {
@ -23,7 +23,7 @@ export function createMockTransport(options: MockTransportOptions = {}): MockCon
const closeEmitter = createTransportEmitter<(event: ConnectionCloseEvent) => void>();
const errorEmitter = createTransportEmitter<(error: unknown) => void>();
const sent: (string | ArrayBuffer)[] = [];
let state: ConnectionTransportState = CONN_TRANSPORT_STATE_IDLE;
let state: ConnectionTransportState = CONNECTION_TRANSPORT_STATE_IDLE;
const openResolvers = new Set<() => void>();
function resolveOpenWaiters(): void {
@ -32,23 +32,23 @@ export function createMockTransport(options: MockTransportOptions = {}): MockCon
}
function emitOpen(): void {
state = CONN_TRANSPORT_STATE_OPEN;
state = CONNECTION_TRANSPORT_STATE_OPEN;
resolveOpenWaiters();
openEmitter.emit();
}
function emitClose(event: Partial<ConnectionCloseEvent> = {}): void {
state = CONN_TRANSPORT_STATE_CLOSED;
state = CONNECTION_TRANSPORT_STATE_CLOSED;
resolveOpenWaiters();
closeEmitter.emit({
code: event.code ?? CONN_MOCK_TRANSPORT_DEFAULT_CLOSE_CODE,
code: event.code ?? CONNECTION_MOCK_TRANSPORT_DEFAULT_CLOSE_CODE,
reason: event.reason,
clean: event.clean ?? CONN_MOCK_TRANSPORT_DEFAULT_CLOSE_CLEAN
clean: event.clean ?? CONNECTION_MOCK_TRANSPORT_DEFAULT_CLOSE_CLEAN
});
}
return {
kind: CONN_TRANSPORT_KIND_MOCK,
kind: CONNECTION_TRANSPORT_KIND_MOCK,
get state() {
return state;
},
@ -56,11 +56,11 @@ export function createMockTransport(options: MockTransportOptions = {}): MockCon
return sent.length;
},
get canSend() {
return state === CONN_TRANSPORT_STATE_OPEN;
return state === CONNECTION_TRANSPORT_STATE_OPEN;
},
open() {
if (state === CONN_TRANSPORT_STATE_OPEN) return Promise.resolve();
state = CONN_TRANSPORT_STATE_OPENING;
if (state === CONNECTION_TRANSPORT_STATE_OPEN) return Promise.resolve();
state = CONNECTION_TRANSPORT_STATE_OPENING;
if (autoOpen) {
emitOpen();
return Promise.resolve();
@ -73,8 +73,8 @@ export function createMockTransport(options: MockTransportOptions = {}): MockCon
sent.push(data);
},
close(code, reason) {
if (state === CONN_TRANSPORT_STATE_CLOSED) return;
state = CONN_TRANSPORT_STATE_CLOSING;
if (state === CONNECTION_TRANSPORT_STATE_CLOSED) return;
state = CONNECTION_TRANSPORT_STATE_CLOSING;
emitClose({ code, reason });
},
onOpen: (listener) => openEmitter.on(listener),

@ -1,23 +1,23 @@
import {
CONN_BROWSER_EVENT_CLOSE,
CONN_BROWSER_EVENT_ERROR,
CONN_BROWSER_EVENT_MESSAGE,
CONN_BROWSER_EVENT_OPEN,
CONN_TRANSPORT_STATE_CLOSED,
CONN_TRANSPORT_STATE_CLOSING,
CONN_TRANSPORT_STATE_IDLE,
CONN_TRANSPORT_STATE_OPEN,
CONN_TRANSPORT_STATE_OPENING,
CONN_ERROR_MSG_WEBSOCKET_UNAVAILABLE,
CONN_TRANSPORT_KIND_WEBSOCKET,
CONN_WEBSOCKET_BINARY_TYPE_ARRAYBUFFER,
CONN_WEBSOCKET_CLOSE_CODE_ABNORMAL,
CONN_WEBSOCKET_CLOSE_CODE_NORMAL,
CONN_WEBSOCKET_READY_STATE_CLOSED,
CONN_WEBSOCKET_READY_STATE_CLOSING,
CONN_WEBSOCKET_READY_STATE_OPEN
CONNECTION_BROWSER_EVENT_CLOSE,
CONNECTION_BROWSER_EVENT_ERROR,
CONNECTION_BROWSER_EVENT_MESSAGE,
CONNECTION_BROWSER_EVENT_OPEN,
CONNECTION_TRANSPORT_STATE_CLOSED,
CONNECTION_TRANSPORT_STATE_CLOSING,
CONNECTION_TRANSPORT_STATE_IDLE,
CONNECTION_TRANSPORT_STATE_OPEN,
CONNECTION_TRANSPORT_STATE_OPENING,
CONNECTION_ERROR_MSG_WEBSOCKET_UNAVAILABLE,
CONNECTION_TRANSPORT_KIND_WEBSOCKET,
CONNECTION_WEBSOCKET_BINARY_TYPE_ARRAYBUFFER,
CONNECTION_WEBSOCKET_CLOSE_CODE_ABNORMAL,
CONNECTION_WEBSOCKET_CLOSE_CODE_NORMAL,
CONNECTION_WEBSOCKET_READY_STATE_CLOSED,
CONNECTION_WEBSOCKET_READY_STATE_CLOSING,
CONNECTION_WEBSOCKET_READY_STATE_OPEN
} from '../consts.ts';
import { ConnWebSocketUnavailableError } from '../errors.ts';
import { ConnectionWebSocketUnavailableError } from '../errors.ts';
import { createTransportEmitter } from '../transport.ts';
import type {
ConnectionCloseEvent,
@ -40,7 +40,7 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
const errorEmitter = createTransportEmitter<(error: unknown) => void>();
let socket: WebSocketLike | null = null;
let state: ConnectionTransportState = CONN_TRANSPORT_STATE_IDLE;
let state: ConnectionTransportState = CONNECTION_TRANSPORT_STATE_IDLE;
let openWaiter: OpenWaiter | null = null;
let detachSocketListeners: (() => void) | null = null;
@ -58,7 +58,7 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
options.WebSocket ??
(globalThis as { readonly WebSocket?: WebSocketConstructorLike }).WebSocket;
if (ctor === undefined)
throw new ConnWebSocketUnavailableError(CONN_ERROR_MSG_WEBSOCKET_UNAVAILABLE);
throw new ConnectionWebSocketUnavailableError(CONNECTION_ERROR_MSG_WEBSOCKET_UNAVAILABLE);
return ctor;
}
@ -73,14 +73,14 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
function readNativeState(): ConnectionTransportState {
const current = socket;
if (current === null) return state;
if (current.readyState === CONN_WEBSOCKET_READY_STATE_OPEN) return CONN_TRANSPORT_STATE_OPEN;
if (current.readyState === CONN_WEBSOCKET_READY_STATE_CLOSING) {
return CONN_TRANSPORT_STATE_CLOSING;
if (current.readyState === CONNECTION_WEBSOCKET_READY_STATE_OPEN) return CONNECTION_TRANSPORT_STATE_OPEN;
if (current.readyState === CONNECTION_WEBSOCKET_READY_STATE_CLOSING) {
return CONNECTION_TRANSPORT_STATE_CLOSING;
}
if (current.readyState === CONN_WEBSOCKET_READY_STATE_CLOSED) {
return CONN_TRANSPORT_STATE_CLOSED;
if (current.readyState === CONNECTION_WEBSOCKET_READY_STATE_CLOSED) {
return CONNECTION_TRANSPORT_STATE_CLOSED;
}
return CONN_TRANSPORT_STATE_OPENING;
return CONNECTION_TRANSPORT_STATE_OPENING;
}
function removeSocketListeners(): void {
@ -90,7 +90,7 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
function attachSocketListeners(next: WebSocketLike): void {
const handleOpen = (): void => {
state = CONN_TRANSPORT_STATE_OPEN;
state = CONNECTION_TRANSPORT_STATE_OPEN;
settleOpen();
openEmitter.emit();
};
@ -103,14 +103,14 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
if (data !== undefined) messageEmitter.emit(String(data));
};
const handleClose = (event: unknown): void => {
state = CONN_TRANSPORT_STATE_CLOSED;
state = CONNECTION_TRANSPORT_STATE_CLOSED;
const closeEvent = event as {
readonly code?: unknown;
readonly reason?: unknown;
readonly wasClean?: unknown;
};
const mapped: ConnectionCloseEvent = {
code: typeof closeEvent.code === 'number' ? closeEvent.code : CONN_WEBSOCKET_CLOSE_CODE_ABNORMAL,
code: typeof closeEvent.code === 'number' ? closeEvent.code : CONNECTION_WEBSOCKET_CLOSE_CODE_ABNORMAL,
reason: typeof closeEvent.reason === 'string' ? closeEvent.reason : undefined,
clean: closeEvent.wasClean === true
};
@ -121,20 +121,20 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
errorEmitter.emit(event);
settleOpen(event);
};
next.addEventListener(CONN_BROWSER_EVENT_OPEN, handleOpen);
next.addEventListener(CONN_BROWSER_EVENT_MESSAGE, handleMessage);
next.addEventListener(CONN_BROWSER_EVENT_CLOSE, handleClose);
next.addEventListener(CONN_BROWSER_EVENT_ERROR, handleError);
next.addEventListener(CONNECTION_BROWSER_EVENT_OPEN, handleOpen);
next.addEventListener(CONNECTION_BROWSER_EVENT_MESSAGE, handleMessage);
next.addEventListener(CONNECTION_BROWSER_EVENT_CLOSE, handleClose);
next.addEventListener(CONNECTION_BROWSER_EVENT_ERROR, handleError);
detachSocketListeners = () => {
next.removeEventListener(CONN_BROWSER_EVENT_OPEN, handleOpen);
next.removeEventListener(CONN_BROWSER_EVENT_MESSAGE, handleMessage);
next.removeEventListener(CONN_BROWSER_EVENT_CLOSE, handleClose);
next.removeEventListener(CONN_BROWSER_EVENT_ERROR, handleError);
next.removeEventListener(CONNECTION_BROWSER_EVENT_OPEN, handleOpen);
next.removeEventListener(CONNECTION_BROWSER_EVENT_MESSAGE, handleMessage);
next.removeEventListener(CONNECTION_BROWSER_EVENT_CLOSE, handleClose);
next.removeEventListener(CONNECTION_BROWSER_EVENT_ERROR, handleError);
};
}
return {
kind: CONN_TRANSPORT_KIND_WEBSOCKET,
kind: CONNECTION_TRANSPORT_KIND_WEBSOCKET,
get state() {
return readNativeState();
},
@ -142,10 +142,10 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
return socket?.bufferedAmount ?? 0;
},
get canSend() {
return socket?.readyState === CONN_WEBSOCKET_READY_STATE_OPEN;
return socket?.readyState === CONNECTION_WEBSOCKET_READY_STATE_OPEN;
},
open() {
if (socket?.readyState === CONN_WEBSOCKET_READY_STATE_OPEN) return Promise.resolve();
if (socket?.readyState === CONNECTION_WEBSOCKET_READY_STATE_OPEN) return Promise.resolve();
if (openWaiter !== null) {
return new Promise<void>((resolve, reject) => {
const previous = openWaiter;
@ -162,12 +162,12 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
});
}
removeSocketListeners();
state = CONN_TRANSPORT_STATE_OPENING;
state = CONNECTION_TRANSPORT_STATE_OPENING;
const Ctor = resolveConstructor();
const protocols = resolveProtocols();
const next =
protocols === undefined ? new Ctor(resolveUrl()) : new Ctor(resolveUrl(), protocols);
next.binaryType = CONN_WEBSOCKET_BINARY_TYPE_ARRAYBUFFER;
next.binaryType = CONNECTION_WEBSOCKET_BINARY_TYPE_ARRAYBUFFER;
socket = next;
attachSocketListeners(next);
return new Promise<void>((resolve, reject) => {
@ -177,8 +177,8 @@ export function createWebSocketTransport(options: WebSocketTransportOptions): Co
send(data) {
socket?.send(data);
},
close(code = CONN_WEBSOCKET_CLOSE_CODE_NORMAL, reason) {
state = CONN_TRANSPORT_STATE_CLOSING;
close(code = CONNECTION_WEBSOCKET_CLOSE_CODE_NORMAL, reason) {
state = CONNECTION_TRANSPORT_STATE_CLOSING;
socket?.close(code, reason);
},
onOpen: (listener) => openEmitter.on(listener),

@ -1,56 +1,56 @@
import type { Logger } from '$libs/logr';
import type { AppEventBus } from '$libs/aapp/events';
import type { TimerScheduler } from '$libs/timers';
import type { Logger } from '$libs/logger';
import type { AppEventBus } from '$libs/active-app/events';
import type { TimerScheduler } from '$libs/timer';
import type {
CONN_ACK_REASON_CLOSED,
CONN_ACK_REASON_INVALID_REPLY,
CONN_ACK_REASON_REJECTED,
CONN_ACK_REASON_TIMEOUT,
CONN_ACK_REASON_TRANSPORT_ERROR,
CONN_AUTH_REASON_CLOSED,
CONN_AUTH_REASON_NO_PROVIDER,
CONN_AUTH_REASON_REJECTED,
CONN_AUTH_REASON_TIMEOUT,
CONN_AUTH_REASON_TRANSPORT_ERROR,
CONN_BUFFER_POLICY_BUFFER,
CONN_BUFFER_POLICY_DROP,
CONN_BUFFER_POLICY_FAIL,
CONN_CHANNEL_JOIN_REASON_CLOSED,
CONN_CHANNEL_JOIN_REASON_REJECTED,
CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
CONN_CHANNEL_STATE_FAILED,
CONN_CHANNEL_STATE_IDLE,
CONN_CHANNEL_STATE_JOINED,
CONN_CHANNEL_STATE_JOINING,
CONN_CHANNEL_STATE_LEAVING,
CONN_CHANNEL_STATE_LEFT,
CONN_CONNECT_REASON_AUTH_FAILED,
CONN_CONNECT_REASON_DISPOSED,
CONN_CONNECT_REASON_RECONNECT_EXHAUSTED,
CONN_CONNECT_REASON_TRANSPORT_ERROR,
CONN_AUTO_REAUTH_NONE,
CONN_AUTO_REAUTH_STANDARD,
CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE,
CONN_SEND_REASON_BUFFER_FULL,
CONN_SEND_REASON_CLOSED,
CONN_SEND_REASON_INVALID_FRAME,
CONN_SEND_REASON_SEND_NOT_SUPPORTED,
CONN_SEND_REASON_SERIALIZE_FAILED,
CONN_SEND_REASON_TRANSPORT_ERROR,
CONN_STATES,
CONN_TRANSPORT_STATES
CONNECTION_ACK_REASON_CLOSED,
CONNECTION_ACK_REASON_INVALID_REPLY,
CONNECTION_ACK_REASON_REJECTED,
CONNECTION_ACK_REASON_TIMEOUT,
CONNECTION_ACK_REASON_TRANSPORT_ERROR,
CONNECTION_AUTH_REASON_CLOSED,
CONNECTION_AUTH_REASON_NO_PROVIDER,
CONNECTION_AUTH_REASON_REJECTED,
CONNECTION_AUTH_REASON_TIMEOUT,
CONNECTION_AUTH_REASON_TRANSPORT_ERROR,
CONNECTION_BUFFER_POLICY_BUFFER,
CONNECTION_BUFFER_POLICY_DROP,
CONNECTION_BUFFER_POLICY_FAIL,
CONNECTION_CHANNEL_JOIN_REASON_CLOSED,
CONNECTION_CHANNEL_JOIN_REASON_REJECTED,
CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
CONNECTION_CHANNEL_STATE_FAILED,
CONNECTION_CHANNEL_STATE_IDLE,
CONNECTION_CHANNEL_STATE_JOINED,
CONNECTION_CHANNEL_STATE_JOINING,
CONNECTION_CHANNEL_STATE_LEAVING,
CONNECTION_CHANNEL_STATE_LEFT,
CONNECTION_CONNECT_REASON_AUTH_FAILED,
CONNECTION_CONNECT_REASON_DISPOSED,
CONNECTION_CONNECT_REASON_RECONNECT_EXHAUSTED,
CONNECTION_CONNECT_REASON_TRANSPORT_ERROR,
CONNECTION_AUTO_REAUTH_NONE,
CONNECTION_AUTO_REAUTH_STANDARD,
CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE,
CONNECTION_SEND_REASON_BUFFER_FULL,
CONNECTION_SEND_REASON_CLOSED,
CONNECTION_SEND_REASON_INVALID_FRAME,
CONNECTION_SEND_REASON_SEND_NOT_SUPPORTED,
CONNECTION_SEND_REASON_SERIALIZE_FAILED,
CONNECTION_SEND_REASON_TRANSPORT_ERROR,
CONNECTION_STATES,
CONNECTION_TRANSPORT_STATES
} from './consts.ts';
export type ConnectionState = (typeof CONN_STATES)[number];
export type ConnectionTransportState = (typeof CONN_TRANSPORT_STATES)[number];
export type ConnectionState = (typeof CONNECTION_STATES)[number];
export type ConnectionTransportState = (typeof CONNECTION_TRANSPORT_STATES)[number];
export type ConnectionChannelState =
| typeof CONN_CHANNEL_STATE_IDLE
| typeof CONN_CHANNEL_STATE_JOINING
| typeof CONN_CHANNEL_STATE_JOINED
| typeof CONN_CHANNEL_STATE_LEAVING
| typeof CONN_CHANNEL_STATE_LEFT
| typeof CONN_CHANNEL_STATE_FAILED;
| typeof CONNECTION_CHANNEL_STATE_IDLE
| typeof CONNECTION_CHANNEL_STATE_JOINING
| typeof CONNECTION_CHANNEL_STATE_JOINED
| typeof CONNECTION_CHANNEL_STATE_LEAVING
| typeof CONNECTION_CHANNEL_STATE_LEFT
| typeof CONNECTION_CHANNEL_STATE_FAILED;
export type ConnectionEventMap = Record<string, unknown>;
export type ConnectionChannelMap = Record<string, ConnectionEventMap>;
@ -172,9 +172,9 @@ export interface ConnectionHeartbeatOptions {
export interface ConnectionBufferOptions {
readonly policy?:
| typeof CONN_BUFFER_POLICY_BUFFER
| typeof CONN_BUFFER_POLICY_DROP
| typeof CONN_BUFFER_POLICY_FAIL;
| typeof CONNECTION_BUFFER_POLICY_BUFFER
| typeof CONNECTION_BUFFER_POLICY_DROP
| typeof CONNECTION_BUFFER_POLICY_FAIL;
readonly maxMessages?: number;
readonly maxBytes?: number;
}
@ -233,12 +233,12 @@ export type ConnectionSendResult =
| {
readonly ok: false;
readonly reason:
| typeof CONN_SEND_REASON_CLOSED
| typeof CONN_SEND_REASON_SEND_NOT_SUPPORTED
| typeof CONN_SEND_REASON_BUFFER_FULL
| typeof CONN_SEND_REASON_INVALID_FRAME
| typeof CONN_SEND_REASON_SERIALIZE_FAILED
| typeof CONN_SEND_REASON_TRANSPORT_ERROR;
| typeof CONNECTION_SEND_REASON_CLOSED
| typeof CONNECTION_SEND_REASON_SEND_NOT_SUPPORTED
| typeof CONNECTION_SEND_REASON_BUFFER_FULL
| typeof CONNECTION_SEND_REASON_INVALID_FRAME
| typeof CONNECTION_SEND_REASON_SERIALIZE_FAILED
| typeof CONNECTION_SEND_REASON_TRANSPORT_ERROR;
readonly error?: unknown;
};
@ -247,11 +247,11 @@ export type ConnectionAckResult<T = unknown> =
| {
readonly ok: false;
readonly reason:
| typeof CONN_ACK_REASON_TIMEOUT
| typeof CONN_ACK_REASON_CLOSED
| typeof CONN_ACK_REASON_REJECTED
| typeof CONN_ACK_REASON_TRANSPORT_ERROR
| typeof CONN_ACK_REASON_INVALID_REPLY;
| typeof CONNECTION_ACK_REASON_TIMEOUT
| typeof CONNECTION_ACK_REASON_CLOSED
| typeof CONNECTION_ACK_REASON_REJECTED
| typeof CONNECTION_ACK_REASON_TRANSPORT_ERROR
| typeof CONNECTION_ACK_REASON_INVALID_REPLY;
readonly error?: unknown;
};
@ -260,11 +260,11 @@ export type ConnectionAuthResult =
| {
readonly ok: false;
readonly reason:
| typeof CONN_AUTH_REASON_NO_PROVIDER
| typeof CONN_AUTH_REASON_CLOSED
| typeof CONN_AUTH_REASON_REJECTED
| typeof CONN_AUTH_REASON_TIMEOUT
| typeof CONN_AUTH_REASON_TRANSPORT_ERROR;
| typeof CONNECTION_AUTH_REASON_NO_PROVIDER
| typeof CONNECTION_AUTH_REASON_CLOSED
| typeof CONNECTION_AUTH_REASON_REJECTED
| typeof CONNECTION_AUTH_REASON_TIMEOUT
| typeof CONNECTION_AUTH_REASON_TRANSPORT_ERROR;
readonly error?: unknown;
};
@ -274,10 +274,10 @@ export type ConnectionConnectResult =
readonly ok: false;
readonly state: ConnectionState;
readonly reason:
| typeof CONN_CONNECT_REASON_TRANSPORT_ERROR
| typeof CONN_CONNECT_REASON_AUTH_FAILED
| typeof CONN_CONNECT_REASON_DISPOSED
| typeof CONN_CONNECT_REASON_RECONNECT_EXHAUSTED;
| typeof CONNECTION_CONNECT_REASON_TRANSPORT_ERROR
| typeof CONNECTION_CONNECT_REASON_AUTH_FAILED
| typeof CONNECTION_CONNECT_REASON_DISPOSED
| typeof CONNECTION_CONNECT_REASON_RECONNECT_EXHAUSTED;
readonly error?: unknown;
};
@ -286,9 +286,9 @@ export type ConnectionChannelJoinResult =
| {
readonly ok: false;
readonly reason:
| typeof CONN_CHANNEL_JOIN_REASON_CLOSED
| typeof CONN_CHANNEL_JOIN_REASON_REJECTED
| typeof CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR;
| typeof CONNECTION_CHANNEL_JOIN_REASON_CLOSED
| typeof CONNECTION_CHANNEL_JOIN_REASON_REJECTED
| typeof CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR;
readonly error?: unknown;
};
@ -373,11 +373,11 @@ export interface EngineConnectionsOptions {
export type ActiveConnectionsOptions = EngineConnectionsOptions;
export type ConnectionAutoReauthTarget = typeof CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE;
export type ConnectionAutoReauthTarget = typeof CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE;
export type ConnectionAutoReauthOn =
| typeof CONN_AUTO_REAUTH_NONE
| typeof CONN_AUTO_REAUTH_STANDARD
| typeof CONNECTION_AUTO_REAUTH_NONE
| typeof CONNECTION_AUTO_REAUTH_STANDARD
| readonly ConnectionAutoReauthTarget[];
export interface ConnectionSessionChange {

@ -1,3 +0,0 @@
export const FMTS_MODULE = 'fmts';
export const FMTS_DEFAULT_LOCALE = 'en-US';
export const FMTS_AUTO_VALUE = 'auto';

@ -1 +0,0 @@
export const FMTS_DATES_MODULE = 'fmts.dates';

@ -1,5 +0,0 @@
import { FMTS_MODULE } from './consts.ts';
export const FORMATS_ERRORS = {
INVALID_LOCALE: (locale: string): string => `[${FMTS_MODULE}] Invalid locale: "${locale}".`
} as const;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save

Powered by TurnKey Linux.