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 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 **full English word** that doubles as filesystem name, alias and
bundler alias (`$buss`, `$conn`, `$sess`, …). canonical wire identifier:
The same alias is the canonical module identifier across the codebase: | Folder | Alias | Wire / value |
|---|---|---|
- **Path scoping in string values** (already enforced after audit | `arts/active-app/`, `libs/active-app/` | `$active-app` | `'app'` |
section 2): `'buss.event.published'`, `'conn.auth_failed'`, | `arts/auth/`, `libs/auth/`, `svrs/auth/` | `$auth` | `'auth'` |
`'sess.lifecycle.adopted'`, `'perm.client.remote_check_failed'`. | `arts/bus/`, `libs/bus/` | `$bus` | `'bus'` |
- **Constant name prefix**: `BUSS_*`, `CONN_*`, `SESS_*`, `PERM_*`, | `arts/cache/`, `libs/cache/`, `svrs/cache/` | `$cache` | `'cache'` |
`TIMR_*`, `LOGR_*`, `CACH_*`, `STOR_*`, `FMTS_*`, `FEND_*`, `ADOM_*`, | `arts/connection/` | `$connection` | `'connection'` |
`AAPP_*`, `AUTH_*`, `LANG_*`, `HTTP_*`, `SIUM_*`, `ERRS_*`. | `libs/dom/` | `$libs/dom` | `'dom'` |
| `libs/errs/` | `$libs/errs` | `'errs'` |
Forbidden: full English words like `BUS_*`, `CONNECTION_*`, `SESSION_*`, | `arts/frontend/` | `$frontend` | `'frontend'` |
`PERMISSION_*`, `TIMER_*`, `LOGGER_*`, `CACHE_*`, `STORAGE_*`, | `arts/formats/` (+ `currency/`, `numbers/`, `units/`, `dates/`) | `$formats` | `'formats'` |
`FORMATS_*`, `FRONTEND_*`. Drift on this axis is being closed in a | `arts/http/`, `libs/http/` | `$http` | `'http'` |
dedicated audit pass. | `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>_<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 - **`<CATEGORY>`** — what kind of constant it is. Picked from the
fixed vocabulary in section 3. fixed vocabulary in section 3.
- **`<NAME>`** — the specific value's identifier in - **`<NAME>`** — the specific value's identifier in
@ -51,20 +94,22 @@ Every categorical module-level constant follows the pattern:
```ts ```ts
// good // good
BUSS_DIAGNOSTIC_EVENTS BUS_DIAGNOSTIC_EVENTS
BUSS_DEFAULT_MAX_LISTENERS_PER_EVENT BUS_DEFAULT_MAX_LISTENERS_PER_EVENT
BUSS_ERR_DISPOSED BUS_ERR_DISPOSED
SESS_EVENT_LIFECYCLE_ADOPTED SESSION_EVENT_LIFECYCLE_ADOPTED
PERM_METHOD_CHECK PERMISSION_METHOD_CHECK
TIMR_STATE_PENDING TIMER_STATE_PENDING
CONN_REASON_AUTH_FAILED CONNECTION_REASON_AUTH_FAILED
CACH_LIMIT_DEFAULT_MAX_ENTRIES CACHE_LIMIT_DEFAULT_MAX_ENTRIES
STOR_MODULE STORAGE_MODULE // value: 'storage'
APP_MODULE // value: 'app'
// bad (will be flagged in audit) PERMISSION_MODULE // value: 'permissions'
BUS_DIAGNOSTIC_EVENTS // wrong module prefix (rule 1)
// bad
BUSS_DIAGNOSTIC_EVENTS // 4-letter alias is dead (rule 1)
DEFAULT_TIMER_SCOPE_SEPARATOR // category before module (rule 2) 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) AUTO_REAUTH_USER_IDENTITY_CHANGE // missing module prefix (rule 1)
``` ```
@ -75,13 +120,13 @@ in the `<NAME>` part, not in `<CATEGORY>`:
```ts ```ts
// good // good
BUSS_LISTENER_ERROR_MODE_THROW BUS_LISTENER_ERROR_MODE_THROW
SESS_EVENT_LIFECYCLE_ADOPTED SESSION_EVENT_LIFECYCLE_ADOPTED
SESS_EVENT_LIFECYCLE_REVOKED SESSION_EVENT_LIFECYCLE_REVOKED
PERM_DEFAULT_REMOTE_FAILURE_BACKOFF_MS PERMISSION_DEFAULT_REMOTE_FAILURE_BACKOFF_MS
// bad // 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 | | 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`) | | `ERR` | Error codes (`ErrCode` values from `$libs/errs`) |
| `EVENT` | Bus or lifecycle event names (string discriminators) | | `EVENT` | Bus or lifecycle event names (string discriminators) |
| `DIAGNOSTIC_EVENTS` | Event values published through `Logger` (catalog object) | | `DIAGNOSTIC_EVENTS` | Event values published through `Logger` (catalog object) |
@ -110,18 +155,18 @@ inside a module.
| `ID_PREFIX` | Prefix used by ID factories | | `ID_PREFIX` | Prefix used by ID factories |
| `LOG_MSG` | Log message strings | | `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_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 | | `CONTEXT_KEY` | Svelte context keys |
Categories that are **not** in this list (e.g. `AUTO_REAUTH`, Categories that are **not** in this list (e.g. `AUTO_REAUTH`,
`AUTO_INVALIDATE`, `BACKOFF`, `BUFFER_POLICY`, `CHANNEL_STATE`) must `AUTO_INVALIDATE`, `BACKOFF`, `BUFFER_POLICY`, `CHANNEL_STATE`) must
fold into one of the above. Examples: fold into one of the above. Examples:
- `CONNECTION_AUTO_REAUTH_*` → `CONN_REASON_AUTO_REAUTH_*` if the value - `CONNECTION_AUTO_REAUTH_*` → `CONNECTION_REASON_AUTO_REAUTH_*` if
drives a "why this happened" branch, or `CONN_MODE_AUTO_REAUTH_*` if the value drives a "why this happened" branch, or
it configures behavior. `CONNECTION_MODE_AUTO_REAUTH_*` if it configures behavior.
- `CACHE_AUTO_INVALIDATE_*` → `CACH_MODE_AUTO_INVALIDATE_*`. - `CACHE_AUTO_INVALIDATE_*` → `CACHE_MODE_AUTO_INVALIDATE_*`.
- `CONN_BUFFER_POLICY_*` → `CONN_MODE_BUFFER_*`. - `CONNECTION_BUFFER_POLICY_*` → `CONNECTION_MODE_BUFFER_*`.
When in doubt, propose the addition here before introducing it. When in doubt, propose the addition here before introducing it.
@ -137,21 +182,21 @@ appears as a literal in error declarations:
```ts ```ts
import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs'; import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs';
export const BUSS_ERR: ModuleSeed = moduleSeed('buss'); // 'buss::' export const BUS_ERR: ModuleSeed = moduleSeed('bus'); // 'bus::'
export const BUSS_ERR_DISPOSED: ErrCode = errCode(BUSS_ERR, 'disposed'); // 'buss::disposed' export const BUS_ERR_DISPOSED: ErrCode = errCode(BUS_ERR, 'disposed'); // 'bus::disposed'
export const BUSS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUSS_ERR, 'invalid_payload'); export const BUS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUS_ERR, 'invalid_payload');
export const BUSS_ERR_LISTENER: ErrCode = errCode(BUSS_ERR, 'listener'); // 'buss::listener' export const BUS_ERR_LISTENER: ErrCode = errCode(BUS_ERR, 'listener'); // 'bus::listener'
export const BUSS_ERR_LISTENER_FAILED: ErrCode = errCode(BUSS_ERR_LISTENER, 'failed'); // 'buss::listener.failed' 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 The constant's `<NAME>` part mirrors the path segments inside the
runtime value: `BUSS_ERR_LISTENER_FAILED` ↔ `'buss::listener.failed'`. runtime value: `BUS_ERR_LISTENER_FAILED` ↔ `'bus::listener.failed'`.
The module name lives in exactly one place: `moduleSeed('buss')`. The module name lives in exactly one place: `moduleSeed('bus')`.
The `::` separator distinguishes module from hierarchy. `errCode` The `::` separator distinguishes module from hierarchy. `errCode`
picks the right separator automatically — `::` after a seed, `.` picks the right separator automatically — `::` after a seed, `.`
between segments. A helper `codeToLangPath(c)` converts 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. path.
### Family matching ### Family matching
@ -159,41 +204,60 @@ path.
A single predicate `matches(err, family)` from `$libs/errs` covers A single predicate `matches(err, family)` from `$libs/errs` covers
both common cases. `family` can be: 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. module regardless of hierarchy.
- An **`ErrCode`** — matches the code itself or any hierarchical - An **`ErrCode`** — matches the code itself or any hierarchical
descendant within the same module. descendant within the same module.
```ts ```ts
matches(err, BUSS_ERR_DISPOSED) // exact code matches(err, BUS_ERR_DISPOSED) // exact code
matches(err, BUSS_ERR_LISTENER) // any descendant of buss::listener matches(err, BUS_ERR_LISTENER) // any descendant of bus::listener
matches(err, BUSS_ERR) // any error declared by buss 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 ## 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 ```ts
// good — value carries the module scope // good — value carries the module scope
export const CONN_DIAGNOSTIC_EVENTS = { export const CONNECTION_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'conn.auth_failed', AUTH_FAILED: 'connection.auth_failed',
RECONNECT_EXHAUSTED: 'conn.reconnect_exhausted' RECONNECT_EXHAUSTED: 'connection.reconnect_exhausted'
} as const; } as const;
// bad — bare value collides across modules // bad — bare value collides across modules
export const CONN_DIAGNOSTIC_EVENTS = { export const CONNECTION_DIAGNOSTIC_EVENTS = {
AUTH_FAILED: 'auth_failed', // collides with auth.* 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; } 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 ## 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. | | `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>/` | 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>/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. | | `arts/<mod>/test/` | Tests for engine and active wrappers. |
| `svrs/<mod>/` | Server-side counterparts (cookies, db adapters, route handlers). | | `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 **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, - A new category in section 3 requires a record-of-decision: who, when,
why. why.
- Module aliases are frozen: a new artifact picks a 4-letter alias - Module names are frozen: a new artifact picks a name not yet taken;
not yet taken; renaming an existing alias is a breaking change. renaming an existing module is a breaking change.
- Drift found in the codebase that violates these rules is filed as an - Drift found in the codebase that violates these rules is filed as an
audit finding and resolved in a dedicated commit. audit finding and resolved in a dedicated commit.
Last updated: `2026-05-02` — initial version after audit-1-5.md Last updated: `2026-05-02` — full English word convention, dropping
sections 1–2 closed. 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 - Runtime code must not throw inline string/template errors outside tests or
vendored code. vendored code.
- Public programmer errors use artifact-specific classes and guards: - 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 - Expected runtime failures should be returned as tagged data/results when the
artifact already has such a contract (`http`, `conn`, `perm`, `cach`). artifact already has such a contract (`http`, `conn`, `perm`, `cach`).
- Validation failures are data (`SiumValidationError.issues`) and diagnostics - 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) | | [`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` | | [`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) | | [`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 | | [`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 ## Composition
@ -193,7 +193,7 @@ adom ─── fend \ | / | conn
and `cach` through ports rather than owning their state. and `cach` through ports rather than owning their state.
- `perm` splits cleanly: `$svrs/perm` owns the authoritative engine/HTTP - `perm` splits cleanly: `$svrs/perm` owns the authoritative engine/HTTP
handlers, while `$perm` owns the active UI reflector and `<Can />`. 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 `$cach` owns the active Svelte wrapper and can consume `stor` through its
storage adapter. storage adapter.
- `adom` depends only on the pure helpers in `libs/dom` and on `libs/reactive`. - `adom` depends only on the pure helpers in `libs/dom` and on `libs/reactive`.
@ -216,9 +216,9 @@ alias: {
$aapp: 'src/arts/aapp', $aapp: 'src/arts/aapp',
$adom: 'src/arts/adom', $adom: 'src/arts/adom',
$auth: 'src/arts/auth', $auth: 'src/arts/auth',
$cach: 'src/arts/cach', $cache: 'src/arts/cach',
$conn: 'src/arts/conn', $conn: 'src/arts/conn',
$fend: 'src/arts/fend', $frontend: 'src/arts/fend',
$fmts: 'src/arts/fmts', $fmts: 'src/arts/fmts',
$http: 'src/arts/http', $http: 'src/arts/http',
$lang: 'src/arts/lang', $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 Server-authoritative engines that have a browser reflector live under
`$svrs/*`: use `$svrs/auth` for `createEngineAuth()` and auth HTTP handlers, `$svrs/*`: use `$svrs/auth` for `createEngineAuth()` and auth HTTP handlers,
`$svrs/perm` for `createEnginePermissions()` and authorization handlers, and `$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. load code. `aapp` composes only the active/client side.
`Sium` is **not** a member of App. Validation is page-scoped — pages with `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: Current identity flow:
```txt ```txt
sess emits SESS_EVENT_CHANGED sess emits SESSION_EVENT_CHANGED
aapp's session translator publishes APP_EVENT_USER_IDENTITY_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 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 | | 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` | | `dispose` | built-in: `App.dispose()` | `APP_EVENT_DISPOSE_STARTING` |
| `permissions-refresh` | typed public contract / explicit publish point | `APP_EVENT_PERMISSIONS_REFRESH_REQUESTED` | | `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` | | `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'>; cache?: Omit<ActiveCacheOptions, 'logger' | 'bus'>;
connections?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session' | 'bus'>; connections?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session' | 'bus'>;
permissions?: Omit<ActivePermissionsOptions, 'logger' | 'http' | '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[]; orchestration?: 'standard' | 'silent' | false | readonly ActiveAppOrchestrationTranslator[];
} }
@ -593,7 +593,7 @@ interface ActiveApp<S extends LangNode = LangNode> {
createActivePermissions( createActivePermissions(
options?: Partial<Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>> options?: Partial<Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>>
): ActivePermissions; ): 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 Sess: ActiveSession<unknown, unknown, unknown> | undefined;
readonly Permissions: ActivePermissions | undefined; readonly Permissions: ActivePermissions | undefined;

@ -2,27 +2,27 @@ import { createActiveDom } from '$adom/active-dom.svelte';
import { createActiveAuth } from '$auth/active-auth.svelte'; import { createActiveAuth } from '$auth/active-auth.svelte';
import { createEngineHttpAuthClient } from '$auth/client'; import { createEngineHttpAuthClient } from '$auth/client';
import type { ActiveAuth, ActiveAuthOptions } from '$auth/types'; import type { ActiveAuth, ActiveAuthOptions } from '$auth/types';
import { createSvelteEngineBus } from '$buss'; import { createSvelteEngineBus } from '$bus';
import { createActiveCache } from '$cach/active-cache.svelte'; import { createActiveCache } from '$cache/active-cache.svelte';
import { createActiveConnections as createActiveConnectionsRegistry } from '$conn/active-connections.svelte'; import { createActiveConnections as createActiveConnectionsRegistry } from '$connection/active-connections.svelte';
import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$conn/types'; import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection/types';
import { createActiveFrontend } from '$fend/active-frontend.svelte'; import { createActiveFrontend } from '$frontend/active-frontend.svelte';
import { createActiveFormats } from '$fmts/active-formats.svelte'; import { createActiveFormats } from '$formats/active-formats.svelte';
import { createEngineHttp } from '$http/engine-http'; import { createEngineHttp } from '$http/engine-http';
import type { ActiveLang, LangNode } from '$libs/lang'; import type { ActiveLang, LangNode } from '$libs/lang';
import { createActiveLang } from '$lang/active-lang.svelte'; import { createActiveLang } from '$lang/active-lang.svelte';
import { createActiveMonoLang } from '$lang/mono-lang.svelte'; import { createActiveMonoLang } from '$lang/mono-lang.svelte';
import { createEngineLogger } from '$logr/engine-logger'; import { createEngineLogger } from '$logger/engine-logger';
import { createActivePermissions as createActivePermissionsClient } from '$perm/active-permissions.svelte'; import { createActivePermissions as createActivePermissionsClient } from '$permissions/active-permissions.svelte';
import { PermInvalidEndpointError } from '$perm/errors'; import { PermInvalidEndpointError } from '$permissions/errors';
import type { ActivePermissions, ActivePermissionsOptions } from '$perm/types'; import type { ActivePermissions, ActivePermissionsOptions } from '$permissions/types';
import { createActiveSession } from '$sess/active-session.svelte'; import { createActiveSession } from '$session/active-session.svelte';
import { SessAlreadyCreatedError } from '$sess/errors'; import { SessionAlreadyCreatedError } from '$session/errors';
import type { ActiveSession, EngineSessionOptions } from '$sess/types'; import type { ActiveSession, EngineSessionOptions } from '$session/types';
import { createEngineSium } from '$sium/engine-sium'; import { createEngineSium } from '$sium/engine-sium';
import { createActiveStorage } from '$stor/active-storage.svelte'; import { createActiveStorage } from '$storage/active-storage.svelte';
import { createActiveTimers } from '$timr/active-timers.svelte'; import { createActiveTimers } from '$timer/active-timers.svelte';
import { publishAppDisposeStarting } from '$libs/aapp/events'; import { publishAppDisposeStarting } from '$libs/active-app/events';
import { import {
applyFrontendPreferenceSnapshot, applyFrontendPreferenceSnapshot,
@ -32,18 +32,18 @@ import {
import { createAuthCacheInvalidator } from './integrations/auth-cache'; import { createAuthCacheInvalidator } from './integrations/auth-cache';
import { wireSessionTranslator } from './integrations/session-translator'; import { wireSessionTranslator } from './integrations/session-translator';
import { import {
AAPP_ERROR_ALREADY_CREATED_PERMISSIONS, APP_ERROR_ALREADY_CREATED_PERMISSIONS,
AAPP_ERROR_ALREADY_CREATED_AUTH, APP_ERROR_ALREADY_CREATED_AUTH,
AAPP_ERROR_ALREADY_CREATED_SESSION, APP_ERROR_ALREADY_CREATED_SESSION,
AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED, APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED,
AAPP_ORCHESTRATION_SILENT, APP_ORCHESTRATION_SILENT,
AAPP_ORCHESTRATION_STANDARD, APP_ORCHESTRATION_STANDARD,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE, APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
AAPP_MODULE APP_MODULE
} from './consts.ts'; } from './consts.ts';
import { AappAlreadyCreatedError } from './errors.ts'; import { AappAlreadyCreatedError } from './errors.ts';
import type { import type {
@ -121,7 +121,7 @@ export function createActiveApp<S extends LangNode = LangNode>(
clock: Timers.clock clock: Timers.clock
}); });
const orchestration = resolveActiveAppOrchestration(options.orchestration); const orchestration = resolveActiveAppOrchestration(options.orchestration);
const detachSessionTranslator = orchestration.has(AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY) const detachSessionTranslator = orchestration.has(APP_ORCHESTRATION_TRANSLATOR_IDENTITY)
? wireSessionTranslator(Bus) ? wireSessionTranslator(Bus)
: undefined; : undefined;
@ -178,7 +178,7 @@ export function createActiveApp<S extends LangNode = LangNode>(
sessOptions: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger' | 'bus'> = {} sessOptions: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger' | 'bus'> = {}
): ActiveSession<TUser, TCredential, TData> { ): ActiveSession<TUser, TCredential, TData> {
if (Sess !== undefined) { 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>({ const built = createActiveSession<TUser, TCredential, TData>({
...sessOptions, ...sessOptions,
@ -210,14 +210,14 @@ export function createActiveApp<S extends LangNode = LangNode>(
permissionOptions: Partial<Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>> = {} permissionOptions: Partial<Omit<ActivePermissionsOptions, 'logger' | 'http' | 'bus'>> = {}
): ActivePermissions { ): ActivePermissions {
if (Permissions !== undefined) { if (Permissions !== undefined) {
throw new AappAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_PERMISSIONS); throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_PERMISSIONS);
} }
const merged = { const merged = {
...options.permissions, ...options.permissions,
...permissionOptions ...permissionOptions
}; };
if (!merged.endpoint) { if (!merged.endpoint) {
throw new PermInvalidEndpointError(AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED); throw new PermInvalidEndpointError(APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED);
} }
const built = createActivePermissionsClient({ const built = createActivePermissionsClient({
...merged, ...merged,
@ -231,17 +231,17 @@ export function createActiveApp<S extends LangNode = LangNode>(
}, },
createActiveAuth( createActiveAuth(
authOptions: Omit<ActiveAuthOptions, 'http' | 'cach' | 'logger'> = {} authOptions: Omit<ActiveAuthOptions, 'http' | 'cache' | 'logger'> = {}
): ActiveAuth { ): ActiveAuth {
if (Auth !== undefined) { if (Auth !== undefined) {
throw new AappAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_AUTH); throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_AUTH);
} }
const built = createActiveAuth({ const built = createActiveAuth({
...options.auth, ...options.auth,
...authOptions, ...authOptions,
logger: Logger, logger: Logger,
http: createEngineHttpAuthClient(Http), http: createEngineHttpAuthClient(Http),
cach: createAuthCacheInvalidator({ cache: createAuthCacheInvalidator({
cache: Cache, cache: Cache,
permissions: () => Permissions permissions: () => Permissions
}) })
@ -253,8 +253,8 @@ export function createActiveApp<S extends LangNode = LangNode>(
dispose() { dispose() {
if (disposed) return; if (disposed) return;
disposed = true; disposed = true;
if (orchestration.has(AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE)) { if (orchestration.has(APP_ORCHESTRATION_TRANSLATOR_DISPOSE)) {
publishAppDisposeStarting(Bus, { cause: AAPP_MODULE }); publishAppDisposeStarting(Bus, { cause: APP_MODULE });
} }
Auth?.dispose(); Auth?.dispose();
Auth = undefined; Auth = undefined;
@ -284,19 +284,19 @@ export function createActiveApp<S extends LangNode = LangNode>(
function resolveActiveAppOrchestration( function resolveActiveAppOrchestration(
orchestration: ActiveAppOrchestrationOptions | undefined orchestration: ActiveAppOrchestrationOptions | undefined
): ReadonlySet<ActiveAppOrchestrationTranslator> { ): ReadonlySet<ActiveAppOrchestrationTranslator> {
if (orchestration === false || orchestration === AAPP_ORCHESTRATION_SILENT) { if (orchestration === false || orchestration === APP_ORCHESTRATION_SILENT) {
return new Set(); 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(STANDARD_ORCHESTRATION_TRANSLATORS);
} }
return new Set(orchestration); return new Set(orchestration);
} }
const STANDARD_ORCHESTRATION_TRANSLATORS = [ const STANDARD_ORCHESTRATION_TRANSLATORS = [
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE APP_ORCHESTRATION_TRANSLATOR_DISPOSE
] as const; ] 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 { 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 { export class AappAlreadyCreatedError extends CodeError {
constructor(message: string) { 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 { createActiveApp } from './active-app.svelte';
export { export {
AAPP_ORCHESTRATION_SILENT, APP_ORCHESTRATION_SILENT,
AAPP_ORCHESTRATION_STANDARD, APP_ORCHESTRATION_STANDARD,
AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY,
AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE, APP_ORCHESTRATION_TRANSLATOR_DISPOSE,
AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, APP_ORCHESTRATION_TRANSLATOR_IDENTITY,
AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH,
AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED,
AAPP_MODULE APP_MODULE
} from './consts'; } from './consts';
export { AappAlreadyCreatedError, isAappAlreadyCreatedError } from './errors'; export { AappAlreadyCreatedError, isAappAlreadyCreatedError } from './errors';
export type { export type {
@ -23,18 +23,18 @@ export type {
FrontendPersistKeyOverride FrontendPersistKeyOverride
} from './types'; } 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`. // that import the production barrel. See `aapp/testing/index.ts`.
// Common storage adapters re-exported for ergonomic single import. // Common storage adapters re-exported for ergonomic single import.
// `createMemoryAdapter` lives in `$stor` only — testing-specific. // `createMemoryAdapter` lives in `$storage` only — testing-specific.
export { localAdapter, sessionAdapter, cookieAdapter, withBroadcast } from '$stor'; export { localAdapter, sessionAdapter, cookieAdapter, withBroadcast } from '$storage';
export type { export type {
CookieAdapterOptions, CookieAdapterOptions,
ServerCookiesLike, ServerCookiesLike,
BroadcastAdapter, BroadcastAdapter,
BroadcastOptions BroadcastOptions
} from '$stor'; } from '$storage';
// `createActiveMonoLang` is intentionally NOT re-exported — callers that // `createActiveMonoLang` is intentionally NOT re-exported — callers that
// compose without `createActiveApp` should import it directly from // compose without `createActiveApp` should import it directly from

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

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

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

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

@ -12,8 +12,8 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { createActiveApp } from '../active-app.svelte'; import { createActiveApp } from '../active-app.svelte';
import { createMemoryAdapter, encodeEnvelope } from '$stor'; import { createMemoryAdapter, encodeEnvelope } from '$storage';
import { LogLevel } from '$logr'; import { LogLevel } from '$logger';
describe('App.Storage', () => { describe('App.Storage', () => {
it('is always present even without storage options', () => { it('is always present even without storage options', () => {
@ -61,7 +61,7 @@ describe('App.Storage', () => {
storage: { adapter: broken } storage: { adapter: broken }
}); });
App.Storage.entry('x', 'd').set('v'); 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(); App.dispose();
}); });
}); });

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

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

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

@ -55,13 +55,13 @@ export async function invalidateAuthClientCache(
diagnostics: AuthClientDiagnostics, diagnostics: AuthClientDiagnostics,
reason: AuthEventName reason: AuthEventName
): Promise<void> { ): Promise<void> {
if (!options.cach) return; if (!options.cache) return;
try { try {
await options.cach.invalidate({ tags: authCacheTagsForIdentity(), reason }); await options.cache.invalidate({ tags: authCacheTagsForIdentity(), reason });
} catch (error) { } catch (error) {
emitAuthClientDiagnostic( emitAuthClientDiagnostic(
diagnostics, diagnostics,
AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACH_INVALIDATION_FAILED, AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACHE_INVALIDATION_FAILED,
{ reason, error } { reason, error }
); );
options.onCacheError?.(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 = { export const AUTH_CLIENT_DIAGNOSTIC_EVENTS = {
OPERATION_FAILED: 'auth.client.operation_failed', OPERATION_FAILED: 'auth.client.operation_failed',
CACH_INVALIDATION_FAILED: 'auth.client.cache_invalidation_failed' CACHE_INVALIDATION_FAILED: 'auth.client.cache_invalidation_failed'
} as const; } as const;
export const AUTH_CLIENT_LOG_MESSAGE_OPERATION_FAILED = 'auth client operation failed'; export const AUTH_CLIENT_LOG_MESSAGE_OPERATION_FAILED = 'auth client operation failed';

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

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

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

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

@ -8,11 +8,11 @@
* intentionally tiny — anything richer (history, derived projections, * intentionally tiny — anything richer (history, derived projections,
* windowing) waits until real usage patterns emerge. * 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. * `$state` rune.
*/ */
import type { BusEnvelope, BusListener, EngineBus } from '$libs/buss'; import type { BusEnvelope, BusListener, EngineBus } from '$libs/bus';
export interface BusRecent<TPayload> { export interface BusRecent<TPayload> {
readonly lastEvent: BusEnvelope<string, TPayload> | undefined; readonly lastEvent: BusEnvelope<string, TPayload> | undefined;

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

@ -3,10 +3,10 @@
* *
* Only the runtime factory lives here. Interfaces, generic constants, * Only the runtime factory lives here. Interfaces, generic constants,
* error classes, the `SILENT_BUS` no-op, and pure helpers live in * 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 * 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 * reserved for the composition root (`aapp`) and tests that need the
* concrete engine. * concrete engine.
*/ */

@ -18,7 +18,7 @@
*/ */
import { untrack } from 'svelte'; 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'; import { createEngineBus } from '../engine-bus.ts';
export function createSvelteEngineBus<TEvents extends BusEventMap = BusEventMap>( export function createSvelteEngineBus<TEvents extends BusEventMap = BusEventMap>(

@ -1,18 +1,18 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { import {
BUSS_EVENT_ALL, BUS_EVENT_ALL,
BUSS_LISTENER_ERROR_MODE_COLLECT, BUS_LISTENER_ERROR_MODE_COLLECT,
BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE, BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE,
BUSS_LISTENER_ERROR_MODE_THROW, BUS_LISTENER_ERROR_MODE_THROW,
BusAggregateListenerError, BusAggregateListenerError,
BusDisposedError, BusDisposedError,
BusInvalidPayloadError, BusInvalidPayloadError,
BusReentrancyLimitError BusReentrancyLimitError
} from '$libs/buss'; } from '$libs/bus';
import { createEngineBus } from '../index.ts'; 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'; const EVENT_AUTH_SIGNED_OUT = 'auth.signed_out';
interface TestEvents { interface TestEvents {
@ -84,7 +84,7 @@ describe('createEngineBus', () => {
id: 'event-1', id: 'event-1',
type: EVENT_IDENTITY_CHANGED, type: EVENT_IDENTITY_CHANGED,
at: 42, at: 42,
source: 'buss', source: 'bus',
payload: { payload: {
previousActorId: 'actor-ada', previousActorId: 'actor-ada',
nextActorId: 'actor-linus' nextActorId: 'actor-linus'
@ -171,7 +171,7 @@ describe('createEngineBus', () => {
const { logger, entries } = createCapturedLogger(); const { logger, entries } = createCapturedLogger();
const Bus = createEngineBus<TestEvents>({ const Bus = createEngineBus<TestEvents>({
logger, logger,
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT
}); });
const afterFailure = vi.fn(); const afterFailure = vi.fn();
const error = new Error('boom'); const error = new Error('boom');
@ -197,7 +197,7 @@ describe('createEngineBus', () => {
const { logger, entries } = createCapturedLogger(); const { logger, entries } = createCapturedLogger();
const Bus = createEngineBus<TestEvents>({ const Bus = createEngineBus<TestEvents>({
logger, logger,
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE listenerErrorMode: BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE
}); });
Bus.on(EVENT_AUTH_SIGNED_OUT, () => { Bus.on(EVENT_AUTH_SIGNED_OUT, () => {
@ -207,7 +207,7 @@ describe('createEngineBus', () => {
const result = Bus.publish( const result = Bus.publish(
EVENT_AUTH_SIGNED_OUT, EVENT_AUTH_SIGNED_OUT,
{ actorId: 'actor-ada' }, { actorId: 'actor-ada' },
{ listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT } { listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT }
); );
expect(result.errors).toHaveLength(1); expect(result.errors).toHaveLength(1);
@ -218,7 +218,7 @@ describe('createEngineBus', () => {
const { logger, entries } = createCapturedLogger(); const { logger, entries } = createCapturedLogger();
const Bus = createEngineBus<TestEvents>({ const Bus = createEngineBus<TestEvents>({
logger, logger,
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE listenerErrorMode: BUS_LISTENER_ERROR_MODE_LOG_AND_CONTINUE
}); });
const afterFailure = vi.fn(); const afterFailure = vi.fn();
@ -234,15 +234,15 @@ describe('createEngineBus', () => {
expect(entries).toEqual([ expect(entries).toEqual([
expect.objectContaining({ expect.objectContaining({
level: 'error', level: 'error',
category: 'buss', category: 'bus',
message: 'buss.listener.failed' message: 'bus.listener.failed'
}) })
]); ]);
}); });
it('throws an aggregate listener error in throw mode after collecting failures', () => { it('throws an aggregate listener error in throw mode after collecting failures', () => {
const Bus = createEngineBus<TestEvents>({ const Bus = createEngineBus<TestEvents>({
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_THROW listenerErrorMode: BUS_LISTENER_ERROR_MODE_THROW
}); });
const afterFailure = vi.fn(() => { const afterFailure = vi.fn(() => {
throw new Error('second'); throw new Error('second');
@ -261,7 +261,7 @@ describe('createEngineBus', () => {
it('awaits async listeners and collects async failures', async () => { it('awaits async listeners and collects async failures', async () => {
const Bus = createEngineBus<TestEvents>({ const Bus = createEngineBus<TestEvents>({
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT
}); });
const calls: string[] = []; const calls: string[] = [];
@ -294,8 +294,8 @@ describe('createEngineBus', () => {
expect(entries).toEqual([ expect(entries).toEqual([
expect.objectContaining({ expect.objectContaining({
level: 'warn', level: 'warn',
category: 'buss', category: 'bus',
message: 'buss.listener.leak_warning' message: 'bus.listener.leak_warning'
}) })
]); ]);
}); });
@ -368,7 +368,7 @@ describe('createEngineBus', () => {
it('listener failures carry envelopeId, correlationId, and causationId', () => { it('listener failures carry envelopeId, correlationId, and causationId', () => {
const Bus = createEngineBus<TestEvents>({ const Bus = createEngineBus<TestEvents>({
listenerErrorMode: BUSS_LISTENER_ERROR_MODE_COLLECT listenerErrorMode: BUS_LISTENER_ERROR_MODE_COLLECT
}); });
Bus.on(EVENT_AUTH_SIGNED_OUT, () => { Bus.on(EVENT_AUTH_SIGNED_OUT, () => {
throw new Error('boom'); throw new Error('boom');
@ -423,7 +423,7 @@ describe('createEngineBus', () => {
Bus.onAny(listener); Bus.onAny(listener);
expect(Bus.listenerCount()).toBe(2); expect(Bus.listenerCount()).toBe(2);
Bus._clearForTesting(BUSS_EVENT_ALL); Bus._clearForTesting(BUS_EVENT_ALL);
expect(Bus.listenerCount()).toBe(1); expect(Bus.listenerCount()).toBe(1);
Bus._clearForTesting(); Bus._clearForTesting();
expect(Bus.listenerCount()).toBe(0); 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: El patrón sigue el resto de artefactos:
```ts ```ts
import { createEngineCache } from '$svrs/cach'; import { createEngineCache } from '$svrs/cache';
import { createActiveCache } from '$cach'; 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. - `createActiveCache()` vive en `$cach` y añade estado reactivo para Svelte.
- `App.Cache` está siempre presente cuando usas `createActiveApp()`. - `App.Cache` está siempre presente cuando usas `createActiveApp()`.
- El core puro vive en `$libs/cach` como `createCacheRuntime()`. - El core puro vive en `$libs/cach` como `createCacheRuntime()`.
@ -32,11 +32,11 @@ import { createActiveCache } from '$cach';
## Uso Mínimo ## Uso Mínimo
```ts ```ts
import { createEngineCache, memoryCacheAdapter, CACH_POLICY_INTERACTIVE } from '$svrs/cach'; import { createEngineCache, memoryCacheAdapter, CACHE_POLICY_INTERACTIVE } from '$svrs/cache';
const Cache = createEngineCache({ const Cache = createEngineCache({
adapter: memoryCacheAdapter(), adapter: memoryCacheAdapter(),
defaultPolicy: CACH_POLICY_INTERACTIVE defaultPolicy: CACHE_POLICY_INTERACTIVE
}); });
const project = await Cache.query({ const project = await Cache.query({
@ -178,10 +178,10 @@ Las políticas expresan intención:
```ts ```ts
import { import {
CACH_POLICY_INTERACTIVE, CACHE_POLICY_INTERACTIVE,
CACH_POLICY_CATALOG, CACHE_POLICY_CATALOG,
CACH_POLICY_PRIVATE_SESSION CACHE_POLICY_PRIVATE_SESSION
} from '$svrs/cach'; } from '$svrs/cache';
``` ```
Incluidas: Incluidas:
@ -195,7 +195,7 @@ Incluidas:
Puedes definir las tuyas: Puedes definir las tuyas:
```ts ```ts
import { createEngineCache } from '$svrs/cach'; import { createEngineCache } from '$svrs/cache';
const Cache = createEngineCache({ const Cache = createEngineCache({
policies: { policies: {
@ -204,7 +204,7 @@ const Cache = createEngineCache({
staleFor: '2m', staleFor: '2m',
staleIfErrorFor: '10m', staleIfErrorFor: '10m',
gcAfter: '30m', gcAfter: '30m',
mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true persist: true
} }
} }
@ -214,11 +214,11 @@ const Cache = createEngineCache({
## Modos ## Modos
```ts ```ts
CACH_READ_MODE_CACHE_FIRST; CACHE_READ_MODE_CACHE_FIRST;
CACH_READ_MODE_STALE_WHILE_REVALIDATE; CACHE_READ_MODE_STALE_WHILE_REVALIDATE;
CACH_READ_MODE_MUST_REVALIDATE; CACHE_READ_MODE_MUST_REVALIDATE;
CACH_READ_MODE_BYPASS_CACHE; CACHE_READ_MODE_BYPASS_CACHE;
CACH_READ_MODE_NO_STORE; CACHE_READ_MODE_NO_STORE;
``` ```
- `cache-first`: sirve caché mientras esté fresh. - `cache-first`: sirve caché mientras esté fresh.
@ -336,7 +336,7 @@ const adapter = memoryCacheAdapter({
``` ```
Soporta TTL, LRU aproximado, epochs e introspección. 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 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 desarrollo local; para producción multi-instancia usa un adapter intencional
o una composición tiered cuando esté disponible. 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: El engine emite eventos:
```ts ```ts
Cache.on(CACH_EVENT_ALL, (event) => { Cache.on(CACHE_EVENT_ALL, (event) => {
console.log(event.type, event.reason); console.log(event.type, event.reason);
}); });
``` ```

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

@ -1,32 +1,32 @@
import type { BusSubscription } from '$libs/buss'; import type { BusSubscription } from '$libs/bus';
import { import {
AAPP_EVENT_TENANT_SWITCHED, APP_EVENT_TENANT_SWITCHED,
AAPP_EVENT_USER_IDENTITY_CHANGED APP_EVENT_USER_IDENTITY_CHANGED
} from '$libs/aapp/events'; } from '$libs/active-app/events';
import { CACH_EVENT_ALL, type CacheClock, type CacheEvent } from '$libs/cach'; import { CACHE_EVENT_ALL, type CacheClock, type CacheEvent } from '$libs/cache';
import { createEngineCache } from '$svrs/cach'; import { createEngineCache } from '$svrs/cache';
import { import {
CACH_AUTO_INVALIDATE_NONE, CACHE_AUTO_INVALIDATE_NONE,
CACH_AUTO_INVALIDATE_STANDARD, CACHE_AUTO_INVALIDATE_STANDARD,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED, CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE, CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACH_ACTIVE_DIAGNOSTIC_EVENTS, CACHE_ACTIVE_DIAGNOSTIC_EVENTS,
CACH_METHOD_CLEAR, CACHE_METHOD_CLEAR,
CACH_METHOD_ENTRY, CACHE_METHOD_ENTRY,
CACH_METHOD_EXPLAIN, CACHE_METHOD_EXPLAIN,
CACH_METHOD_GET, CACHE_METHOD_GET,
CACH_METHOD_INVALIDATE, CACHE_METHOD_INVALIDATE,
CACH_METHOD_MUTATE, CACHE_METHOD_MUTATE,
CACH_METHOD_ON, CACHE_METHOD_ON,
CACH_METHOD_QUERY, CACHE_METHOD_QUERY,
CACH_METHOD_SET, CACHE_METHOD_SET,
CACH_METHOD_STATS CACHE_METHOD_STATS
} from './consts.ts'; } from './consts.ts';
import { disposedCacheMessage } from '$svrs/cach'; import { disposedCacheMessage } from '$svrs/cache';
import { createActiveCacheEntry } from './active-cache-entry.svelte.ts'; import { createActiveCacheEntry } from './active-cache-entry.svelte.ts';
import { normalizeActiveCacheError } from './active-cache-helpers.ts'; import { normalizeActiveCacheError } from './active-cache-helpers.ts';
import { createActiveCacheDiagnostics, emitActiveCacheDiagnostic } from './diagnostics.ts'; import { createActiveCacheDiagnostics, emitActiveCacheDiagnostic } from './diagnostics.ts';
import { CachDisposedError } from './errors.ts'; import { CacheDisposedError } from './errors.ts';
import type { import type {
ActiveCache, ActiveCache,
ActiveCacheEntry, ActiveCacheEntry,
@ -51,7 +51,7 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
(snapshot: ActiveCache['snapshot'] extends () => infer T ? T : never) => void (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; lastEventCell = event;
eventCountCell += 1; eventCountCell += 1;
notify(); notify();
@ -72,7 +72,7 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
return value; return value;
} catch (error) { } catch (error) {
lastErrorCell = normalizeActiveCacheError(error); lastErrorCell = normalizeActiveCacheError(error);
emitActiveCacheDiagnostic(diagnostics, CACH_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED, { emitActiveCacheDiagnostic(diagnostics, CACHE_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED, {
method, method,
error error
}); });
@ -84,16 +84,16 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
} }
function entry<T>(entryOptions: ActiveCacheEntryOptions<T>): ActiveCacheEntry<T> { function entry<T>(entryOptions: ActiveCacheEntryOptions<T>): ActiveCacheEntry<T> {
ensureLive(CACH_METHOD_ENTRY); ensureLive(CACHE_METHOD_ENTRY);
const built = createActiveCacheEntry( const built = createActiveCacheEntry(
entryOptions, entryOptions,
clock, clock,
{ {
query: (queryOptions) => track(CACH_METHOD_QUERY, () => engine.query(queryOptions)), query: (queryOptions) => track(CACHE_METHOD_QUERY, () => engine.query(queryOptions)),
set: (key, value, setOptions) => set: (key, value, setOptions) =>
track(CACH_METHOD_SET, () => engine.set(key, value, setOptions)), track(CACHE_METHOD_SET, () => engine.set(key, value, setOptions)),
invalidate: (invalidateOptions) => invalidate: (invalidateOptions) =>
track(CACH_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions)) track(CACHE_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions))
}, },
() => { () => {
entries.delete(built as ActiveCacheEntry<unknown>); entries.delete(built as ActiveCacheEntry<unknown>);
@ -104,7 +104,7 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
} }
function ensureLive(method: string): void { function ensureLive(method: string): void {
if (disposed) throw new CachDisposedError(disposedCacheMessage(method)); if (disposed) throw new CacheDisposedError(disposedCacheMessage(method));
} }
function snapshot() { function snapshot() {
@ -139,50 +139,50 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
return disposed; return disposed;
}, },
query(queryOptions) { query(queryOptions) {
ensureLive(CACH_METHOD_QUERY); ensureLive(CACHE_METHOD_QUERY);
return track(CACH_METHOD_QUERY, () => engine.query(queryOptions)); return track(CACHE_METHOD_QUERY, () => engine.query(queryOptions));
}, },
get(key, getOptions) { get(key, getOptions) {
ensureLive(CACH_METHOD_GET); ensureLive(CACHE_METHOD_GET);
return track(CACH_METHOD_GET, () => engine.get(key, getOptions)); return track(CACHE_METHOD_GET, () => engine.get(key, getOptions));
}, },
set(key, value, setOptions) { set(key, value, setOptions) {
ensureLive(CACH_METHOD_SET); ensureLive(CACHE_METHOD_SET);
return track(CACH_METHOD_SET, () => engine.set(key, value, setOptions)); return track(CACHE_METHOD_SET, () => engine.set(key, value, setOptions));
}, },
invalidate(invalidateOptions) { invalidate(invalidateOptions) {
ensureLive(CACH_METHOD_INVALIDATE); ensureLive(CACHE_METHOD_INVALIDATE);
return track(CACH_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions)); return track(CACHE_METHOD_INVALIDATE, () => engine.invalidate(invalidateOptions));
}, },
mutate(mutateOptions) { mutate(mutateOptions) {
ensureLive(CACH_METHOD_MUTATE); ensureLive(CACHE_METHOD_MUTATE);
return track(CACH_METHOD_MUTATE, () => engine.mutate(mutateOptions)); return track(CACHE_METHOD_MUTATE, () => engine.mutate(mutateOptions));
}, },
explain(key, explainOptions) { explain(key, explainOptions) {
ensureLive(CACH_METHOD_EXPLAIN); ensureLive(CACHE_METHOD_EXPLAIN);
return track(CACH_METHOD_EXPLAIN, () => engine.explain(key, explainOptions)); return track(CACHE_METHOD_EXPLAIN, () => engine.explain(key, explainOptions));
}, },
stats() { stats() {
ensureLive(CACH_METHOD_STATS); ensureLive(CACHE_METHOD_STATS);
return engine.stats(); return engine.stats();
}, },
on(type, handler) { on(type, handler) {
ensureLive(CACH_METHOD_ON); ensureLive(CACHE_METHOD_ON);
return engine.on(type, handler); return engine.on(type, handler);
}, },
clear() { clear() {
ensureLive(CACH_METHOD_CLEAR); ensureLive(CACHE_METHOD_CLEAR);
return track(CACH_METHOD_CLEAR, () => engine.clear()); return track(CACHE_METHOD_CLEAR, () => engine.clear());
}, },
entry, entry,
snapshot, snapshot,
clearError() { clearError() {
ensureLive(CACH_METHOD_CLEAR); ensureLive(CACHE_METHOD_CLEAR);
lastErrorCell = null; lastErrorCell = null;
notify(); notify();
}, },
onChange(listener) { onChange(listener) {
ensureLive(CACH_METHOD_ON); ensureLive(CACHE_METHOD_ON);
listeners.add(listener); listeners.add(listener);
listener(snapshot()); listener(snapshot());
return () => { return () => {
@ -206,19 +206,19 @@ export function createActiveCache(options: ActiveCacheOptions = {}): ActiveCache
if (bus === undefined) return []; if (bus === undefined) return [];
const targets = resolveAutoInvalidateTargets(options.autoInvalidateOn); const targets = resolveAutoInvalidateTargets(options.autoInvalidateOn);
const subscriptions: BusSubscription[] = []; const subscriptions: BusSubscription[] = [];
if (targets.has(CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE)) { if (targets.has(CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE)) {
subscriptions.push( subscriptions.push(
bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, () => { bus.on(APP_EVENT_USER_IDENTITY_CHANGED, () => {
if (disposed) return; 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( subscriptions.push(
bus.on(AAPP_EVENT_TENANT_SWITCHED, () => { bus.on(APP_EVENT_TENANT_SWITCHED, () => {
if (disposed) return; 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( function resolveAutoInvalidateTargets(
autoInvalidateOn: ActiveCacheOptions['autoInvalidateOn'] autoInvalidateOn: ActiveCacheOptions['autoInvalidateOn']
): ReadonlySet<string> { ): ReadonlySet<string> {
if (autoInvalidateOn === undefined || autoInvalidateOn === CACH_AUTO_INVALIDATE_NONE) { if (autoInvalidateOn === undefined || autoInvalidateOn === CACHE_AUTO_INVALIDATE_NONE) {
return new Set(); return new Set();
} }
if (autoInvalidateOn === CACH_AUTO_INVALIDATE_STANDARD) { if (autoInvalidateOn === CACHE_AUTO_INVALIDATE_STANDARD) {
return new Set([ return new Set([
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE, CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED CACHE_AUTO_INVALIDATE_TENANT_SWITCHED
]); ]);
} }
return new Set(autoInvalidateOn); 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 DiagnosticEvent,
type Diagnostics, type Diagnostics,
type Logger type Logger
} from '$libs/logr'; } from '$libs/logger';
import type { CacheKey } from '$libs/cach'; import type { CacheKey } from '$libs/cache';
import { import {
CACH_ACTIVE_DIAGNOSTIC_EVENTS, CACHE_ACTIVE_DIAGNOSTIC_EVENTS,
CACH_ACTIVE_LOG_MESSAGE_OPERATION_FAILED, CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED,
CACH_MODULE CACHE_MODULE
} from './consts.ts'; } from './consts.ts';
export type ActiveCacheDiagnosticType = 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 { export interface ActiveCacheDiagnosticMeta {
readonly method: string; readonly method: string;
@ -29,16 +29,16 @@ export type ActiveCacheDiagnosticEvent = DiagnosticEvent<
export type ActiveCacheDiagnostics = Diagnostics<ActiveCacheDiagnosticEvent>; export type ActiveCacheDiagnostics = Diagnostics<ActiveCacheDiagnosticEvent>;
const ACTIVE_CACHE_DIAGNOSTIC_LOGS: DiagnosticCatalog<ActiveCacheDiagnosticEvent> = { const ACTIVE_CACHE_DIAGNOSTIC_LOGS: DiagnosticCatalog<ActiveCacheDiagnosticEvent> = {
[CACH_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED]: { [CACHE_ACTIVE_DIAGNOSTIC_EVENTS.OPERATION_FAILED]: {
level: LogLevel.WARN, level: LogLevel.WARN,
message: CACH_ACTIVE_LOG_MESSAGE_OPERATION_FAILED message: CACHE_ACTIVE_LOG_MESSAGE_OPERATION_FAILED
} }
}; };
export function createActiveCacheDiagnostics(logger?: Logger): ActiveCacheDiagnostics { export function createActiveCacheDiagnostics(logger?: Logger): ActiveCacheDiagnostics {
return createCatalogDiagnostics({ return createCatalogDiagnostics({
logger, logger,
defaultCategory: CACH_MODULE, defaultCategory: CACHE_MODULE,
catalog: ACTIVE_CACHE_DIAGNOSTIC_LOGS catalog: ACTIVE_CACHE_DIAGNOSTIC_LOGS
}); });
} }
@ -49,7 +49,7 @@ export function emitActiveCacheDiagnostic(
meta: ActiveCacheDiagnosticMeta meta: ActiveCacheDiagnosticMeta
): void { ): void {
diagnostics.emit({ diagnostics.emit({
artifact: CACH_MODULE, artifact: CACHE_MODULE,
type, type,
meta 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 { describe, expect, it } from 'vitest';
import { createActiveApp } from '$aapp'; import { createActiveApp } from '$active-app';
import { import {
CACH_ACTIVE_STATUS_STALE, CACHE_ACTIVE_STATUS_STALE,
CACH_ACTIVE_STATUS_SUCCESS, CACHE_ACTIVE_STATUS_SUCCESS,
CACH_POLICY_INTERACTIVE, CACHE_POLICY_INTERACTIVE,
CACH_SCOPE_PUBLIC, CACHE_SCOPE_PUBLIC,
createActiveCache, createActiveCache,
isCachActiveEntryDisposedError, isCacheActiveEntryDisposedError,
memoryCacheAdapter, memoryCacheAdapter,
type CacheClock type CacheClock
} from '../index.ts'; } from '../index.ts';
@ -30,16 +30,16 @@ describe('createActiveCache', () => {
}); });
const entry = Cache.entry({ const entry = Cache.entry({
key: ['dashboard'], key: ['dashboard'],
scope: CACH_SCOPE_PUBLIC, scope: CACHE_SCOPE_PUBLIC,
fetcher: async () => ({ cards: 3 }) fetcher: async () => ({ cards: 3 })
}); });
await entry.load(); await entry.load();
expect(entry.data).toEqual({ cards: 3 }); 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(); await entry.invalidate();
expect(entry.status).toBe(CACH_ACTIVE_STATUS_STALE); expect(entry.status).toBe(CACHE_ACTIVE_STATUS_STALE);
expect(Cache.eventCount).toBeGreaterThan(0); expect(Cache.eventCount).toBeGreaterThan(0);
Cache.dispose(); Cache.dispose();
@ -53,8 +53,8 @@ describe('createActiveCache', () => {
}); });
const entry = Cache.entry<number>({ const entry = Cache.entry<number>({
key: ['project', 7], key: ['project', 7],
scope: CACH_SCOPE_PUBLIC, scope: CACHE_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE, policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: 'Project:v1', schemaVersion: 'Project:v1',
tags: [{ type: 'project', id: 7 }], tags: [{ type: 'project', id: 7 }],
fetcher: async () => 1 fetcher: async () => 1
@ -64,8 +64,8 @@ describe('createActiveCache', () => {
await expect( await expect(
Cache.get<number>(['project', 7], { Cache.get<number>(['project', 7], {
scope: CACH_SCOPE_PUBLIC, scope: CACHE_SCOPE_PUBLIC,
policy: CACH_POLICY_INTERACTIVE, policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: 'Project:v1' schemaVersion: 'Project:v1'
}) })
).resolves.toBe(2); ).resolves.toBe(2);
@ -78,7 +78,7 @@ describe('createActiveCache', () => {
const Cache = createActiveCache(); const Cache = createActiveCache();
const entry = Cache.entry({ const entry = Cache.entry({
key: ['entry-disposed'], key: ['entry-disposed'],
scope: CACH_SCOPE_PUBLIC, scope: CACHE_SCOPE_PUBLIC,
fetcher: async () => 'never' fetcher: async () => 'never'
}); });
@ -87,7 +87,7 @@ describe('createActiveCache', () => {
try { try {
await entry.load(); await entry.load();
} catch (error) { } catch (error) {
expect(isCachActiveEntryDisposedError(error)).toBe(true); expect(isCacheActiveEntryDisposedError(error)).toBe(true);
} }
Cache.dispose(); Cache.dispose();
@ -98,7 +98,7 @@ describe('createActiveCache', () => {
const Cache = createActiveCache(); const Cache = createActiveCache();
const entry = Cache.entry({ const entry = Cache.entry({
key: ['parent-disposed-entry'], key: ['parent-disposed-entry'],
scope: CACH_SCOPE_PUBLIC, scope: CACHE_SCOPE_PUBLIC,
fetcher: async () => 'never' fetcher: async () => 'never'
}); });
@ -107,7 +107,7 @@ describe('createActiveCache', () => {
try { try {
await entry.load(); await entry.load();
} catch (error) { } catch (error) {
expect(isCachActiveEntryDisposedError(error)).toBe(true); expect(isCacheActiveEntryDisposedError(error)).toBe(true);
} }
}); });

@ -1,21 +1,21 @@
import type { import type {
CACH_ACTIVE_ENTRY_EVENTS, CACHE_ACTIVE_ENTRY_EVENTS,
CACH_ACTIVE_STATUSES, CACHE_ACTIVE_STATUSES,
CACH_AUTO_INVALIDATE_NONE, CACHE_AUTO_INVALIDATE_NONE,
CACH_AUTO_INVALIDATE_STANDARD, CACHE_AUTO_INVALIDATE_STANDARD,
CACH_AUTO_INVALIDATE_TENANT_SWITCHED, CACHE_AUTO_INVALIDATE_TENANT_SWITCHED,
CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
} from './consts.ts'; } from './consts.ts';
import type { AppEventBus } from '$libs/aapp/events'; import type { AppEventBus } from '$libs/active-app/events';
import type { CacheClock, CacheEvent, CacheKey, QueryOptions, SetOptions } from '$libs/cach'; import type { CacheClock, CacheEvent, CacheKey, QueryOptions, SetOptions } from '$libs/cache';
import type { EngineCache, EngineCacheOptions } from '$svrs/cach'; import type { EngineCache, EngineCacheOptions } from '$svrs/cache';
import type { ActiveChangeListener, ActiveEngine } from '$libs/active'; 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> { export interface ActiveCacheEntrySnapshot<T> {
readonly key: CacheKey; readonly key: CacheKey;
@ -49,12 +49,12 @@ export interface ActiveCacheEntryOptions<T> extends QueryOptions<T> {
} }
export type ActiveCacheAutoInvalidateTarget = export type ActiveCacheAutoInvalidateTarget =
| typeof CACH_AUTO_INVALIDATE_USER_IDENTITY_CHANGE | typeof CACHE_AUTO_INVALIDATE_USER_IDENTITY_CHANGE
| typeof CACH_AUTO_INVALIDATE_TENANT_SWITCHED; | typeof CACHE_AUTO_INVALIDATE_TENANT_SWITCHED;
export type ActiveCacheAutoInvalidateOn = export type ActiveCacheAutoInvalidateOn =
| typeof CACH_AUTO_INVALIDATE_NONE | typeof CACHE_AUTO_INVALIDATE_NONE
| typeof CACH_AUTO_INVALIDATE_STANDARD | typeof CACHE_AUTO_INVALIDATE_STANDARD
| readonly ActiveCacheAutoInvalidateTarget[]; | readonly ActiveCacheAutoInvalidateTarget[];
export interface ActiveCacheOptions extends EngineCacheOptions { 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: Behavior:
- `createConnection(name, options)` creates and registers a connection. - `createConnection(name, options)` creates and registers a connection.
- Creating the same name twice should throw `ConnConnectionAlreadyExistsError`. - Creating the same name twice should throw `ConnectionAlreadyExistsError`.
- `connection(name)` returns an existing connection or throws `ConnConnectionNotFoundError`. - `connection(name)` returns an existing connection or throws `ConnectionNotFoundError`.
- `closeAll()` disconnects all registered connections. - `closeAll()` disconnects all registered connections.
- `dispose()` disconnects everything and clears listeners. - `dispose()` disconnects everything and clears listeners.
@ -1377,8 +1377,8 @@ Defaults:
enabled: true, enabled: true,
intervalMs: 25_000, intervalMs: 25_000,
timeoutMs: 10_000, timeoutMs: 10_000,
pingType: 'conn.ping', pingType: 'connection.ping',
pongType: 'conn.pong' pongType: 'connection.pong'
} }
``` ```
@ -1445,7 +1445,7 @@ Implementation may send a frame:
```ts ```ts
{ {
type: 'conn.auth', type: 'connection.auth',
payload: await getAuth(), payload: await getAuth(),
ack: true ack: true
} }
@ -1592,13 +1592,13 @@ Use custom errors for programmer errors, not normal runtime failures.
Examples: Examples:
```ts ```ts
ConnConnectionAlreadyExistsError; ConnectionAlreadyExistsError;
ConnConnectionNotFoundError; ConnectionNotFoundError;
ConnInvalidConnectionNameError; ConnectionInvalidNameError;
ConnInvalidFrameError; ConnectionInvalidFrameError;
ConnDisposedError; ConnectionDisposedError;
ConnChannelAlreadyExistsError; ConnectionChannelAlreadyExistsError;
ConnChannelNotFoundError; ConnectionChannelNotFoundError;
``` ```
Runtime network failures should generally return tagged results or emit state/error events, not throw unexpectedly. 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 ## 23. Public constants
```ts ```ts
export const CONN_STATE_IDLE = 'idle'; export const CONNECTION_STATE_IDLE = 'idle';
export const CONN_STATE_CONNECTING = 'connecting'; export const CONNECTION_STATE_CONNECTING = 'connecting';
export const CONN_STATE_OPEN = 'open'; export const CONNECTION_STATE_OPEN = 'open';
export const CONN_STATE_RECONNECTING = 'reconnecting'; export const CONNECTION_STATE_RECONNECTING = 'reconnecting';
export const CONN_STATE_CLOSING = 'closing'; export const CONNECTION_STATE_CLOSING = 'closing';
export const CONN_STATE_CLOSED = 'closed'; export const CONNECTION_STATE_CLOSED = 'closed';
export const CONN_STATE_FAILED = 'failed'; export const CONNECTION_STATE_FAILED = 'failed';
export const DEFAULT_RECONNECT_MIN_DELAY_MS = 500; export const DEFAULT_RECONNECT_MIN_DELAY_MS = 500;
export const DEFAULT_RECONNECT_MAX_DELAY_MS = 15_000; 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 LOGGER_CATEGORY = 'conn';
export const LOG_MSG_LISTENER_THREW_PREFIX = 'Listener threw on '; export const LOG_MSG_LISTENER_THREW_PREFIX = 'Listener threw on ';
export const ERROR_NAME_DISPOSED = 'ConnDisposedError'; export const ERROR_NAME_DISPOSED = 'ConnectionDisposedError';
export const ERROR_NAME_ALREADY_EXISTS = 'ConnConnectionAlreadyExistsError'; export const ERROR_NAME_ALREADY_EXISTS = 'ConnectionAlreadyExistsError';
export const ERROR_NAME_NOT_FOUND = 'ConnConnectionNotFoundError'; export const ERROR_NAME_NOT_FOUND = 'ConnectionNotFoundError';
export const ERROR_NAME_INVALID_NAME = 'ConnInvalidConnectionNameError'; export const ERROR_NAME_INVALID_NAME = 'ConnectionInvalidNameError';
export const ERROR_NAME_INVALID_FRAME = 'ConnInvalidFrameError'; export const ERROR_NAME_INVALID_FRAME = 'ConnectionInvalidFrameError';
``` ```
Prefer exported constants over magic strings/numbers. Engine bodies should not Prefer exported constants over magic strings/numbers. Engine bodies should not
@ -1681,7 +1681,7 @@ const outboundBuffer: ConnectionFrame[] = [];
On `connect()`: On `connect()`:
1. If disposed, throw `ConnDisposedError`. 1. If disposed, throw `ConnectionDisposedError`.
2. If open, return reused success. 2. If open, return reused success.
3. If connecting, return existing promise. 3. If connecting, return existing promise.
4. Create/open transport. 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 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 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 ## Send Y Request/Reply
@ -296,8 +296,8 @@ Connections.createConnection('main', {
enabled: true, enabled: true,
intervalMs: 25_000, intervalMs: 25_000,
timeoutMs: 10_000, timeoutMs: 10_000,
pingType: 'conn.ping', pingType: 'connection.ping',
pongType: 'conn.pong' pongType: 'connection.pong'
} }
}); });
``` ```
@ -315,7 +315,7 @@ Connections.createConnection('main', {
transport, transport,
auth: { auth: {
getAuth: () => ({ token }), getAuth: () => ({ token }),
authType: 'conn.auth', authType: 'connection.auth',
timeoutMs: 10_000 timeoutMs: 10_000
} }
}); });
@ -338,7 +338,7 @@ definido, el session source derivado de `App.Bus` puede:
El flujo completo es: El flujo completo es:
```txt ```txt
sess -> SESS_EVENT_CHANGED sess -> SESSION_EVENT_CHANGED
aapp -> APP_EVENT_USER_IDENTITY_CHANGED aapp -> APP_EVENT_USER_IDENTITY_CHANGED
conn -> reauth/disconnect si autoReauthOn + connection.session + connection.auth lo permiten 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 Internamente `conn` usa `createConnectionDiagnostics(logger)` y eventos
catalogados en `CONN_DIAGNOSTIC_EVENTS`: catalogados en `CONNECTION_DIAGNOSTIC_EVENTS`:
- `connect_failed` - `connect_failed`
- `transport_error` - `transport_error`
@ -401,14 +401,14 @@ Sentry, Loki o Datadog, inyecta un `EngineLogger` con el transporte adecuado;
Programmer errors lanzan clases tipadas: Programmer errors lanzan clases tipadas:
- `ConnDisposedError` - `ConnectionDisposedError`
- `ConnConnectionAlreadyExistsError` - `ConnectionAlreadyExistsError`
- `ConnConnectionNotFoundError` - `ConnectionNotFoundError`
- `ConnInvalidConnectionNameError` - `ConnectionInvalidNameError`
- `ConnInvalidFrameError` - `ConnectionInvalidFrameError`
- `ConnChannelAlreadyExistsError` - `ConnectionChannelAlreadyExistsError`
- `ConnChannelNotFoundError` - `ConnectionChannelNotFoundError`
- `ConnWebSocketUnavailableError` - `ConnectionWebSocketUnavailableError`
Fallos runtime de transporte/envío/auth/request devuelven resultados tagged Fallos runtime de transporte/envío/auth/request devuelven resultados tagged
para que el consumidor pueda decidir sin `try/catch` obligatorio. 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 { import {
CONN_ACK_REASON_CLOSED, CONNECTION_ACK_REASON_CLOSED,
CONN_ACK_REASON_REJECTED, CONNECTION_ACK_REASON_REJECTED,
CONN_ACK_REASON_TIMEOUT, CONNECTION_ACK_REASON_TIMEOUT,
CONN_ACK_REASON_TRANSPORT_ERROR, CONNECTION_ACK_REASON_TRANSPORT_ERROR,
CONN_SEND_REASON_CLOSED, CONNECTION_SEND_REASON_CLOSED,
CONN_TIMR_KEY_ACK CONNECTION_TIMER_KEY_ACK
} from './consts.ts'; } from './consts.ts';
import { timerKey } from './helpers.ts'; import { timerKey } from './helpers.ts';
import type { ConnectionAckResult, ConnectionFrame, ConnectionSendResult } from './types.ts'; import type { ConnectionAckResult, ConnectionFrame, ConnectionSendResult } from './types.ts';
@ -39,7 +39,7 @@ export function createConnectionAckRegistry(
return { return {
wait<TResult>(id: string, timeoutMs: number): Promise<ConnectionAckResult<TResult>> { 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) => { return new Promise<ConnectionAckResult<TResult>>((resolveWaiter) => {
pending.set(id, { pending.set(id, {
timer: ackTimer, timer: ackTimer,
@ -49,7 +49,7 @@ export function createConnectionAckRegistry(
ackTimer, ackTimer,
timeoutMs, timeoutMs,
() => { () => {
resolve(id, { ok: false, reason: CONN_ACK_REASON_TIMEOUT }); resolve(id, { ok: false, reason: CONNECTION_ACK_REASON_TIMEOUT });
}, },
{ replace: true } { replace: true }
); );
@ -64,7 +64,7 @@ export function createConnectionAckRegistry(
if (frame.error !== undefined) { if (frame.error !== undefined) {
resolve(frame.replyTo, { resolve(frame.replyTo, {
ok: false, ok: false,
reason: CONN_ACK_REASON_REJECTED, reason: CONNECTION_ACK_REASON_REJECTED,
error: frame.error error: frame.error
}); });
return; return;
@ -73,10 +73,10 @@ export function createConnectionAckRegistry(
}, },
mapSendFailure(result: ConnectionSendResult): ConnectionAckResult<unknown> { mapSendFailure(result: ConnectionSendResult): ConnectionAckResult<unknown> {
if (result.ok) return { ok: true, payload: undefined }; if (result.ok) return { ok: true, payload: undefined };
if (result.reason === CONN_SEND_REASON_CLOSED) { if (result.reason === CONNECTION_SEND_REASON_CLOSED) {
return { ok: false, reason: CONN_ACK_REASON_CLOSED, error: result.error }; 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 { createEngineConnections } from './engine-connections.ts';
import { import {
CONN_STATE_CLOSED, CONNECTION_STATE_CLOSED,
CONN_STATE_CONNECTING, CONNECTION_STATE_CONNECTING,
CONN_STATE_FAILED, CONNECTION_STATE_FAILED,
CONN_STATE_OPEN, CONNECTION_STATE_OPEN,
CONN_STATE_RECONNECTING CONNECTION_STATE_RECONNECTING
} from './consts.ts'; } from './consts.ts';
import type { import type {
ActiveConnections, ActiveConnections,
@ -61,37 +61,37 @@ export function createActiveConnections<TConnections extends ConnectionMap = Con
return states; return states;
}, },
get connectedNames() { get connectedNames() {
return namesBy(CONN_STATE_OPEN); return namesBy(CONNECTION_STATE_OPEN);
}, },
get connectingNames() { get connectingNames() {
return namesBy(CONN_STATE_CONNECTING); return namesBy(CONNECTION_STATE_CONNECTING);
}, },
get reconnectingNames() { get reconnectingNames() {
return namesBy(CONN_STATE_RECONNECTING); return namesBy(CONNECTION_STATE_RECONNECTING);
}, },
get failedNames() { get failedNames() {
return namesBy(CONN_STATE_FAILED); return namesBy(CONNECTION_STATE_FAILED);
}, },
get closedNames() { get closedNames() {
return namesBy(CONN_STATE_CLOSED); return namesBy(CONNECTION_STATE_CLOSED);
}, },
get allConnected() { get allConnected() {
return ( return (
activeNames.length > 0 && activeNames.length > 0 &&
activeNames.every((name) => states[name] === CONN_STATE_OPEN) activeNames.every((name) => states[name] === CONNECTION_STATE_OPEN)
); );
}, },
get anyConnected() { get anyConnected() {
return activeNames.some((name) => states[name] === CONN_STATE_OPEN); return activeNames.some((name) => states[name] === CONNECTION_STATE_OPEN);
}, },
get anyConnecting() { get anyConnecting() {
return activeNames.some((name) => states[name] === CONN_STATE_CONNECTING); return activeNames.some((name) => states[name] === CONNECTION_STATE_CONNECTING);
}, },
get anyReconnecting() { get anyReconnecting() {
return activeNames.some((name) => states[name] === CONN_STATE_RECONNECTING); return activeNames.some((name) => states[name] === CONNECTION_STATE_RECONNECTING);
}, },
get anyFailed() { 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>( createConnection<TChannels extends ConnectionChannelMap = ConnectionChannelMap>(
name: string, name: string,

@ -1,10 +1,10 @@
import { import {
CONN_BROWSER_EVENT_ONLINE, CONNECTION_BROWSER_EVENT_ONLINE,
CONN_BROWSER_EVENT_VISIBILITY_CHANGE, CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE,
CONN_DIAGNOSTIC_EVENTS, CONNECTION_DIAGNOSTIC_EVENTS,
CONN_DEFAULT_RECONNECT_ON_ONLINE, CONNECTION_DEFAULT_RECONNECT_ON_ONLINE,
CONN_DEFAULT_RECONNECT_ON_VISIBLE, CONNECTION_DEFAULT_RECONNECT_ON_VISIBLE,
CONN_DOCUMENT_VISIBILITY_VISIBLE CONNECTION_DOCUMENT_VISIBILITY_VISIBLE
} from './consts.ts'; } from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts'; import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionReconnectOptions } from './types.ts'; import type { ConnectionReconnectOptions } from './types.ts';
@ -21,9 +21,9 @@ export function wireBrowserReconnect(options: BrowserReconnectOptions): () => vo
if (options.disabled) return () => {}; if (options.disabled) return () => {};
const detachers: Array<() => void> = []; const detachers: Array<() => void> = [];
const reconnectOnOnline = const reconnectOnOnline =
options.reconnectOptions?.reconnectOnOnline ?? CONN_DEFAULT_RECONNECT_ON_ONLINE; options.reconnectOptions?.reconnectOnOnline ?? CONNECTION_DEFAULT_RECONNECT_ON_ONLINE;
const reconnectOnVisible = const reconnectOnVisible =
options.reconnectOptions?.reconnectOnVisible ?? CONN_DEFAULT_RECONNECT_ON_VISIBLE; options.reconnectOptions?.reconnectOnVisible ?? CONNECTION_DEFAULT_RECONNECT_ON_VISIBLE;
const target = globalThis as { const target = globalThis as {
addEventListener?: (type: string, listener: () => void) => void; addEventListener?: (type: string, listener: () => void) => void;
removeEventListener?: (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; if (!options.shouldReconnect()) return;
emitConnectionDiagnostic( emitConnectionDiagnostic(
options.diagnostics, options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT, CONNECTION_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT,
{ trigger: CONN_BROWSER_EVENT_ONLINE } { trigger: CONNECTION_BROWSER_EVENT_ONLINE }
); );
options.reconnect(); options.reconnect();
}; };
target.addEventListener(CONN_BROWSER_EVENT_ONLINE, onOnline); target.addEventListener(CONNECTION_BROWSER_EVENT_ONLINE, onOnline);
detachers.push(() => { 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 target.document.removeEventListener
) { ) {
const onVisible = (): void => { 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; if (!options.shouldReconnect()) return;
emitConnectionDiagnostic( emitConnectionDiagnostic(
options.diagnostics, options.diagnostics,
CONN_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT, CONNECTION_DIAGNOSTIC_EVENTS.BROWSER_RECONNECT,
{ trigger: CONN_BROWSER_EVENT_VISIBILITY_CHANGE } { trigger: CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE }
); );
options.reconnect(); options.reconnect();
}; };
target.document.addEventListener(CONN_BROWSER_EVENT_VISIBILITY_CHANGE, onVisible); target.document.addEventListener(CONNECTION_BROWSER_EVENT_VISIBILITY_CHANGE, onVisible);
detachers.push(() => { 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 { import {
AAPP_EVENT_USER_IDENTITY_CHANGED, APP_EVENT_USER_IDENTITY_CHANGED,
type AppEventBus type AppEventBus
} from '$libs/aapp/events'; } from '$libs/active-app/events';
import type { ConnectionSessionSource } from './types.ts'; import type { ConnectionSessionSource } from './types.ts';
export function createBusSessionSource(bus: AppEventBus): ConnectionSessionSource { export function createBusSessionSource(bus: AppEventBus): ConnectionSessionSource {
return { return {
onChange(listener) { 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); listener(event.payload);
}); });
return () => { return () => {

@ -1,15 +1,15 @@
import { import {
CONN_CHANNEL_JOIN_REASON_CLOSED, CONNECTION_CHANNEL_JOIN_REASON_CLOSED,
CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR, CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
CONN_CHANNEL_STATE_FAILED, CONNECTION_CHANNEL_STATE_FAILED,
CONN_CHANNEL_STATE_IDLE, CONNECTION_CHANNEL_STATE_IDLE,
CONN_CHANNEL_STATE_JOINED, CONNECTION_CHANNEL_STATE_JOINED,
CONN_CHANNEL_STATE_JOINING, CONNECTION_CHANNEL_STATE_JOINING,
CONN_CHANNEL_STATE_LEAVING, CONNECTION_CHANNEL_STATE_LEAVING,
CONN_CHANNEL_STATE_LEFT, CONNECTION_CHANNEL_STATE_LEFT,
CONN_FRAME_TYPE_JOIN, CONNECTION_FRAME_TYPE_JOIN,
CONN_FRAME_TYPE_LEAVE, CONNECTION_FRAME_TYPE_LEAVE,
CONN_SEND_REASON_CLOSED CONNECTION_SEND_REASON_CLOSED
} from './consts.ts'; } from './consts.ts';
import type { import type {
Connection, Connection,
@ -41,12 +41,12 @@ export interface ConnectionChannelRuntime<TEvents extends ConnectionEventMap = C
function mapSendToJoinResult(result: ConnectionSendResult): ConnectionChannelJoinResult { function mapSendToJoinResult(result: ConnectionSendResult): ConnectionChannelJoinResult {
if (result.ok) return { ok: true }; if (result.ok) return { ok: true };
if (result.reason === CONN_SEND_REASON_CLOSED) { if (result.reason === CONNECTION_SEND_REASON_CLOSED) {
return { ok: false, reason: CONN_CHANNEL_JOIN_REASON_CLOSED, error: result.error }; return { ok: false, reason: CONNECTION_CHANNEL_JOIN_REASON_CLOSED, error: result.error };
} }
return { return {
ok: false, ok: false,
reason: CONN_CHANNEL_JOIN_REASON_TRANSPORT_ERROR, reason: CONNECTION_CHANNEL_JOIN_REASON_TRANSPORT_ERROR,
error: result.error error: result.error
}; };
} }
@ -56,7 +56,7 @@ export function createConnectionChannel<TEvents extends ConnectionEventMap = Con
options: ConnectionChannelOptions = {}, options: ConnectionChannelOptions = {},
runtime: ConnectionChannelRuntime<TEvents> runtime: ConnectionChannelRuntime<TEvents>
): InternalConnectionChannel<TEvents> { ): InternalConnectionChannel<TEvents> {
let state: ConnectionChannelState = CONN_CHANNEL_STATE_IDLE; let state: ConnectionChannelState = CONNECTION_CHANNEL_STATE_IDLE;
let desiredJoined = options.autoJoin === true; let desiredJoined = options.autoJoin === true;
let disposed = false; let disposed = false;
@ -103,27 +103,27 @@ export function createConnectionChannel<TEvents extends ConnectionEventMap = Con
return desiredJoined && options.rejoinOnReconnect !== false; return desiredJoined && options.rejoinOnReconnect !== false;
}, },
async join(params) { async join(params) {
if (disposed) return { ok: false, reason: CONN_CHANNEL_JOIN_REASON_CLOSED }; if (disposed) return { ok: false, reason: CONNECTION_CHANNEL_JOIN_REASON_CLOSED };
setState(CONN_CHANNEL_STATE_JOINING); setState(CONNECTION_CHANNEL_STATE_JOINING);
const payload = await resolveJoinPayload(params); 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 topic: name
}); });
const mapped = mapSendToJoinResult(result); const mapped = mapSendToJoinResult(result);
if (mapped.ok) { if (mapped.ok) {
desiredJoined = true; desiredJoined = true;
setState(CONN_CHANNEL_STATE_JOINED); setState(CONNECTION_CHANNEL_STATE_JOINED);
} else { } else {
setState(CONN_CHANNEL_STATE_FAILED); setState(CONNECTION_CHANNEL_STATE_FAILED);
} }
return mapped; return mapped;
}, },
async leave() { async leave() {
if (disposed) return; if (disposed) return;
setState(CONN_CHANNEL_STATE_LEAVING); setState(CONNECTION_CHANNEL_STATE_LEAVING);
desiredJoined = false; desiredJoined = false;
await runtime.connection.send(CONN_FRAME_TYPE_LEAVE, undefined, { topic: name }); await runtime.connection.send(CONNECTION_FRAME_TYPE_LEAVE, undefined, { topic: name });
setState(CONN_CHANNEL_STATE_LEFT); setState(CONNECTION_CHANNEL_STATE_LEFT);
}, },
send(type, payload, sendOptions?: ConnectionSendOptions): Promise<ConnectionSendResult> { send(type, payload, sendOptions?: ConnectionSendOptions): Promise<ConnectionSendResult> {
return runtime.connection.send(type, payload, { ...sendOptions, topic: name }); return runtime.connection.send(type, payload, { ...sendOptions, topic: name });
@ -164,7 +164,7 @@ export function createConnectionChannel<TEvents extends ConnectionEventMap = Con
}, },
dispose() { dispose() {
desiredJoined = false; desiredJoined = false;
setState(CONN_CHANNEL_STATE_LEFT); setState(CONNECTION_CHANNEL_STATE_LEFT);
disposed = true; disposed = true;
anyListeners.clear(); anyListeners.clear();
typeListeners.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 { import {
CONN_ACK_REASON_CLOSED, CONNECTION_ACK_REASON_CLOSED,
CONN_STATE_CLOSED, CONNECTION_STATE_CLOSED,
CONN_STATE_CLOSING, CONNECTION_STATE_CLOSING,
CONN_STATE_FAILED, CONNECTION_STATE_FAILED,
CONN_STATE_RECONNECTING, CONNECTION_STATE_RECONNECTING,
CONN_TIMR_KEY_RECONNECT CONNECTION_TIMER_KEY_RECONNECT
} from './consts.ts'; } from './consts.ts';
import type { ConnectionAckRegistry } from './acks.ts'; import type { ConnectionAckRegistry } from './acks.ts';
import type { ConnectionStateTracker } from './connection-state.ts'; import type { ConnectionStateTracker } from './connection-state.ts';
@ -33,26 +33,26 @@ export function handleConnectionTransportClose(
): void { ): void {
if (runtime.isDisposed()) return; if (runtime.isDisposed()) return;
if (runtime.isIntentionalClose()) { if (runtime.isIntentionalClose()) {
runtime.markClosed(CONN_STATE_CLOSED); runtime.markClosed(CONNECTION_STATE_CLOSED);
return; return;
} }
runtime.state.setError(event); runtime.state.setError(event);
if (runtime.shouldReconnect()) { if (runtime.shouldReconnect()) {
runtime.markClosed(CONN_STATE_RECONNECTING, event); runtime.markClosed(CONNECTION_STATE_RECONNECTING, event);
runtime.scheduleReconnect(); runtime.scheduleReconnect();
return; return;
} }
runtime.markClosed(CONN_STATE_FAILED, event); runtime.markClosed(CONNECTION_STATE_FAILED, event);
} }
export function closeConnectionTransport(reason: string, runtime: ConnectionCloseRuntime): void { export function closeConnectionTransport(reason: string, runtime: ConnectionCloseRuntime): void {
runtime.setIntentionalClose(true); runtime.setIntentionalClose(true);
runtime.timers.cancel(CONN_TIMR_KEY_RECONNECT); runtime.timers.cancel(CONNECTION_TIMER_KEY_RECONNECT);
runtime.heartbeat.stop(); runtime.heartbeat.stop();
runtime.acks.resolveAll({ ok: false, reason: CONN_ACK_REASON_CLOSED }); runtime.acks.resolveAll({ ok: false, reason: CONNECTION_ACK_REASON_CLOSED });
runtime.state.transition(CONN_STATE_CLOSING); runtime.state.transition(CONNECTION_STATE_CLOSING);
runtime.transport.close(reason); runtime.transport.close(reason);
if (runtime.state.state !== CONN_STATE_CLOSED) { if (runtime.state.state !== CONNECTION_STATE_CLOSED) {
runtime.markClosed(CONN_STATE_CLOSED); runtime.markClosed(CONNECTION_STATE_CLOSED);
} }
} }

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

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

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

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

@ -1,8 +1,8 @@
import { import {
CONN_DIAGNOSTIC_EVENTS, CONNECTION_DIAGNOSTIC_EVENTS,
CONN_STATE_FAILED, CONNECTION_STATE_FAILED,
CONN_LOG_MSG_RECONNECT_EXHAUSTED, CONNECTION_LOG_MSG_RECONNECT_EXHAUSTED,
CONN_TIMR_KEY_RECONNECT CONNECTION_TIMER_KEY_RECONNECT
} from './consts.ts'; } from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts'; import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionReconnectPolicy } from './reconnect.ts'; import type { ConnectionReconnectPolicy } from './reconnect.ts';
@ -20,16 +20,16 @@ export interface ScheduleConnectionReconnectInput {
export function scheduleConnectionReconnect(input: ScheduleConnectionReconnectInput): void { export function scheduleConnectionReconnect(input: ScheduleConnectionReconnectInput): void {
const nextReconnect = input.policy.next(input.state.reconnectAttempt); const nextReconnect = input.policy.next(input.state.reconnectAttempt);
if (!nextReconnect.ok) { if (!nextReconnect.ok) {
emitConnectionDiagnostic(input.diagnostics, CONN_DIAGNOSTIC_EVENTS.RECONNECT_EXHAUSTED, { emitConnectionDiagnostic(input.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.RECONNECT_EXHAUSTED, {
reconnectAttempt: nextReconnect.reconnectAttempt, reconnectAttempt: nextReconnect.reconnectAttempt,
maxAttempts: nextReconnect.maxAttempts 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.setError(exhaustedError);
input.state.transition(CONN_STATE_FAILED, exhaustedError); input.state.transition(CONNECTION_STATE_FAILED, exhaustedError);
return; return;
} }
input.state.setReconnectAttempt(nextReconnect.attempt); 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 { import {
CONN_ACK_REASON_CLOSED, CONNECTION_ACK_REASON_CLOSED,
CONN_AUTH_REASON_CLOSED, CONNECTION_AUTH_REASON_CLOSED,
CONN_AUTH_REASON_REJECTED, CONNECTION_AUTH_REASON_REJECTED,
CONN_DEFAULT_ACK_TIMEOUT_MS CONNECTION_DEFAULT_ACK_TIMEOUT_MS
} from './consts.ts'; } from './consts.ts';
import type { ConnectionAckRegistry } from './acks.ts'; import type { ConnectionAckRegistry } from './acks.ts';
import { import {
@ -45,7 +45,7 @@ export function createConnectionRequestRuntime(
} }
async function runAuth(): Promise<ConnectionAuthResult> { 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); const auth = resolveConnectionAuth(runtime.auth);
if (auth.provider === null) return missingConnectionAuthProvider(); if (auth.provider === null) return missingConnectionAuthProvider();
try { try {
@ -54,7 +54,7 @@ export function createConnectionRequestRuntime(
const result = await requestFrame(auth.authType, payload, { timeoutMs: auth.timeoutMs }); const result = await requestFrame(auth.authType, payload, { timeoutMs: auth.timeoutMs });
return mapConnectionAckToAuthResult(result); return mapConnectionAckToAuthResult(result);
} catch (err) { } 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, payload: TPayload,
requestOptions: ConnectionRequestOptions = {} requestOptions: ConnectionRequestOptions = {}
): Promise<ConnectionAckResult<TResult>> { ): 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 id = requestOptions.id ?? runtime.nextId();
const frame = createFrame({ const frame = createFrame({
id, id,
@ -73,7 +73,7 @@ export function createConnectionRequestRuntime(
ack: true, ack: true,
ts: requestOptions.ts 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 promise = runtime.acks.wait<TResult>(id, timeoutMs);
const result = await runtime.sender.sendFrame(frame, requestOptions.buffer === true); const result = await runtime.sender.sendFrame(frame, requestOptions.buffer === true);
if (!result.ok) runtime.acks.resolve(id, runtime.acks.mapSendFailure(result)); if (!result.ok) runtime.acks.resolve(id, runtime.acks.mapSendFailure(result));

@ -1,8 +1,8 @@
import { import {
CONN_STATE_CLOSED, CONNECTION_STATE_CLOSED,
CONN_STATE_FAILED, CONNECTION_STATE_FAILED,
CONN_STATE_IDLE, CONNECTION_STATE_IDLE,
CONN_STATE_OPEN CONNECTION_STATE_OPEN
} from './consts.ts'; } from './consts.ts';
import type { ConnectionState, ConnectionStateChange } from './types.ts'; import type { ConnectionState, ConnectionStateChange } from './types.ts';
@ -28,7 +28,7 @@ export function createConnectionStateTracker(input: {
readonly now: () => number; readonly now: () => number;
readonly emitState: (change: ConnectionStateChange) => void; readonly emitState: (change: ConnectionStateChange) => void;
}): ConnectionStateTracker { }): ConnectionStateTracker {
let state: ConnectionState = CONN_STATE_IDLE; let state: ConnectionState = CONNECTION_STATE_IDLE;
let generation = 0; let generation = 0;
let error: unknown | null = null; let error: unknown | null = null;
let openedAt: number | null = null; let openedAt: number | null = null;
@ -85,20 +85,20 @@ export function createConnectionStateTracker(input: {
}, },
isClosedLike() { isClosedLike() {
return ( return (
state === CONN_STATE_CLOSED || state === CONNECTION_STATE_CLOSED ||
state === CONN_STATE_FAILED || state === CONNECTION_STATE_FAILED ||
state === CONN_STATE_IDLE state === CONNECTION_STATE_IDLE
); );
}, },
transition, transition,
markOpen() { markOpen() {
if (state === CONN_STATE_OPEN) return false; if (state === CONNECTION_STATE_OPEN) return false;
generation += 1; generation += 1;
error = null; error = null;
openedAt = input.now(); openedAt = input.now();
closedAt = null; closedAt = null;
reconnectAttempt = 0; reconnectAttempt = 0;
return transition(CONN_STATE_OPEN); return transition(CONNECTION_STATE_OPEN);
}, },
markClosed(to, closeError, beforeEmit) { markClosed(to, closeError, beforeEmit) {
if (state === to) return false; 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'; import { timerKey } from './helpers.ts';
export interface ConnectionTimerControls { 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 { createConnectionAckRegistry } from './acks.ts';
import { wireBrowserReconnect } from './browser-reconnect.ts'; import { wireBrowserReconnect } from './browser-reconnect.ts';
import { createConnectionChannelRegistry } from './channel-registry.ts'; import { createConnectionChannelRegistry } from './channel-registry.ts';
@ -16,12 +16,12 @@ import { createConnectionRequestRuntime } from './connection-requests.ts';
import { createConnectionStateTracker } from './connection-state.ts'; import { createConnectionStateTracker } from './connection-state.ts';
import { createConnectionTimerControls } from './connection-timers.ts'; import { createConnectionTimerControls } from './connection-timers.ts';
import { import {
CONN_CLOSE_REASON_DISCONNECT, CONNECTION_CLOSE_REASON_DISCONNECT,
CONN_CLOSE_REASON_DISPOSE, CONNECTION_CLOSE_REASON_DISPOSE,
CONN_CLOSE_REASON_RECONNECT, CONNECTION_CLOSE_REASON_RECONNECT,
CONN_CLOSE_REASON_SESSION_EXPIRED, CONNECTION_CLOSE_REASON_SESSION_EXPIRED,
CONN_DIAGNOSTIC_EVENTS, CONNECTION_DIAGNOSTIC_EVENTS,
CONN_STATE_FAILED CONNECTION_STATE_FAILED
} from './consts.ts'; } from './consts.ts';
import { import {
createConnectionDiagnostics, createConnectionDiagnostics,
@ -41,7 +41,7 @@ import { createConnectionSender } from './sender.ts';
import { createFrame } from './serializer.ts'; import { createFrame } from './serializer.ts';
import { jsonConnectionSerializer } from './serializers/json.ts'; import { jsonConnectionSerializer } from './serializers/json.ts';
import { createConnectionTransportRuntime } from './connection-transport-runtime.ts'; import { createConnectionTransportRuntime } from './connection-transport-runtime.ts';
import type { Logger } from '$libs/logr'; import type { Logger } from '$libs/logger';
import type { import type {
Connection, Connection,
ConnectionChannel, ConnectionChannel,
@ -131,7 +131,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
function handleTransportError(err: unknown): void { function handleTransportError(err: unknown): void {
stateTracker.setError(err); stateTracker.setError(err);
emitConnectionDiagnostic(diagnostics, CONN_DIAGNOSTIC_EVENTS.TRANSPORT_ERROR, { emitConnectionDiagnostic(diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.TRANSPORT_ERROR, {
error: err error: err
}); });
} }
@ -211,7 +211,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
detachTransport: transportRuntime.detach, detachTransport: transportRuntime.detach,
markOpen: lifecycle.markOpen, markOpen: lifecycle.markOpen,
markClosedFailed: (error) => { markClosedFailed: (error) => {
lifecycle.markClosed(CONN_STATE_FAILED, error); lifecycle.markClosed(CONNECTION_STATE_FAILED, error);
}, },
joinConfiguredChannels, joinConfiguredChannels,
state: stateTracker, state: stateTracker,
@ -233,7 +233,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
emitScopedConnectionDiagnostic( emitScopedConnectionDiagnostic(
diagnostics, diagnostics,
scope, scope,
CONN_DIAGNOSTIC_EVENTS.LISTENER_THREW, CONNECTION_DIAGNOSTIC_EVENTS.LISTENER_THREW,
{ error: err, event } { error: err, event }
); );
} }
@ -270,11 +270,11 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
connect() { connect() {
return connectInternal(false); return connectInternal(false);
}, },
disconnect(reason = CONN_CLOSE_REASON_DISCONNECT) { disconnect(reason = CONNECTION_CLOSE_REASON_DISCONNECT) {
if (disposed) return; if (disposed) return;
closeTransport(reason); closeTransport(reason);
}, },
reconnect(reason = CONN_CLOSE_REASON_RECONNECT) { reconnect(reason = CONNECTION_CLOSE_REASON_RECONNECT) {
if (!disposed) { if (!disposed) {
closeTransport(reason); closeTransport(reason);
intentionalClose = false; intentionalClose = false;
@ -326,7 +326,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
dispose() { dispose() {
if (disposed) return; if (disposed) return;
disposed = true; disposed = true;
closeTransport(CONN_CLOSE_REASON_DISPOSE); closeTransport(CONNECTION_CLOSE_REASON_DISPOSE);
detachSession(); detachSession();
detachBrowserReconnect(); detachBrowserReconnect();
transportRuntime.detach(); 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 { createBusSessionSource } from './bus-session-source.ts';
import { createConnection } from './connection.ts'; import { createConnection } from './connection.ts';
import { import {
CONN_AUTO_REAUTH_NONE, CONNECTION_AUTO_REAUTH_NONE,
CONN_AUTO_REAUTH_STANDARD, CONNECTION_AUTO_REAUTH_STANDARD,
CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE, CONNECTION_AUTO_REAUTH_USER_IDENTITY_CHANGE,
CONN_METHOD_CLOSE_ALL, CONNECTION_METHOD_CLOSE_ALL,
CONN_METHOD_CLOSE_CONNECTION, CONNECTION_METHOD_CLOSE_CONNECTION,
CONN_METHOD_CONNECTION, CONNECTION_METHOD_CONNECTION,
CONN_METHOD_CREATE_CONNECTION, CONNECTION_METHOD_CREATE_CONNECTION,
CONN_METHOD_OPEN_ALL, CONNECTION_METHOD_OPEN_ALL,
CONN_METHOD_OPEN_CONNECTION, CONNECTION_METHOD_OPEN_CONNECTION,
CONN_METHOD_RECONNECT_ALL, CONNECTION_METHOD_RECONNECT_ALL,
CONN_METHOD_RECONNECT_CONNECTION, CONNECTION_METHOD_RECONNECT_CONNECTION,
CONN_METHOD_REAUTHENTICATE_ALL CONNECTION_METHOD_REAUTHENTICATE_ALL
} from './consts.ts'; } from './consts.ts';
import { import {
ConnConnectionAlreadyExistsError, ConnectionAlreadyExistsError,
ConnConnectionNotFoundError, ConnectionNotFoundError,
ConnDisposedError, ConnectionDisposedError,
ConnInvalidConnectionNameError ConnectionInvalidNameError
} from './errors.ts'; } from './errors.ts';
import { import {
alreadyExistsErrorMessage, alreadyExistsErrorMessage,
@ -37,7 +37,7 @@ import type {
function validateConnectionName(name: unknown): asserts name is string { function validateConnectionName(name: unknown): asserts name is string {
if (typeof name !== 'string' || name.trim().length === 0) { 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; let disposed = false;
function ensureLive(method: string): void { 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 { function getConnection(name: string, method: string): Connection {
@ -100,7 +100,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
validateConnectionName(name); validateConnectionName(name);
const connection = connections.get(name); const connection = connections.get(name);
if (connection === undefined) { if (connection === undefined) {
throw new ConnConnectionNotFoundError(notFoundErrorMessage(name), name); throw new ConnectionNotFoundError(notFoundErrorMessage(name), name);
} }
return connection; return connection;
} }
@ -110,10 +110,10 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
name: string, name: string,
connectionOptions: ConnectionOptions<TChannels> connectionOptions: ConnectionOptions<TChannels>
): Connection<TChannels> { ): Connection<TChannels> {
ensureLive(CONN_METHOD_CREATE_CONNECTION); ensureLive(CONNECTION_METHOD_CREATE_CONNECTION);
validateConnectionName(name); validateConnectionName(name);
if (connections.has(name)) { if (connections.has(name)) {
throw new ConnConnectionAlreadyExistsError(alreadyExistsErrorMessage(name), name); throw new ConnectionAlreadyExistsError(alreadyExistsErrorMessage(name), name);
} }
const connection = createConnection<TChannels>( const connection = createConnection<TChannels>(
name, name,
@ -130,7 +130,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
connection<TChannels extends ConnectionChannelMap = ConnectionChannelMap>( connection<TChannels extends ConnectionChannelMap = ConnectionChannelMap>(
name: string name: string
): Connection<TChannels> { ): Connection<TChannels> {
return getConnection(name, CONN_METHOD_CONNECTION) as Connection<TChannels>; return getConnection(name, CONNECTION_METHOD_CONNECTION) as Connection<TChannels>;
}, },
has(name) { has(name) {
if (disposed) return false; if (disposed) return false;
@ -141,34 +141,34 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
return [...connections.keys()]; return [...connections.keys()];
}, },
openConnection(name) { openConnection(name) {
return getConnection(name, CONN_METHOD_OPEN_CONNECTION).connect(); return getConnection(name, CONNECTION_METHOD_OPEN_CONNECTION).connect();
}, },
closeConnection(name, reason) { closeConnection(name, reason) {
getConnection(name, CONN_METHOD_CLOSE_CONNECTION).disconnect(reason); getConnection(name, CONNECTION_METHOD_CLOSE_CONNECTION).disconnect(reason);
}, },
reconnectConnection(name, reason) { reconnectConnection(name, reason) {
return getConnection(name, CONN_METHOD_RECONNECT_CONNECTION).reconnect(reason); return getConnection(name, CONNECTION_METHOD_RECONNECT_CONNECTION).reconnect(reason);
}, },
openAll() { openAll() {
ensureLive(CONN_METHOD_OPEN_ALL); ensureLive(CONNECTION_METHOD_OPEN_ALL);
return Promise.all([...connections.values()].map((connection) => connection.connect())); return Promise.all([...connections.values()].map((connection) => connection.connect()));
}, },
closeAll(reason) { closeAll(reason) {
ensureLive(CONN_METHOD_CLOSE_ALL); ensureLive(CONNECTION_METHOD_CLOSE_ALL);
for (const connection of connections.values()) connection.disconnect(reason); for (const connection of connections.values()) connection.disconnect(reason);
}, },
reconnectAll(reason) { reconnectAll(reason) {
ensureLive(CONN_METHOD_RECONNECT_ALL); ensureLive(CONNECTION_METHOD_RECONNECT_ALL);
return Promise.all( return Promise.all(
[...connections.values()].map((connection) => connection.reconnect(reason)) [...connections.values()].map((connection) => connection.reconnect(reason))
); );
}, },
reauthenticateAll() { reauthenticateAll() {
ensureLive(CONN_METHOD_REAUTHENTICATE_ALL); ensureLive(CONNECTION_METHOD_REAUTHENTICATE_ALL);
return Promise.all([...connections.values()].map((connection) => connection.reauthenticate())); return Promise.all([...connections.values()].map((connection) => connection.reauthenticate()));
}, },
close(name, reason) { close(name, reason) {
getConnection(name, CONN_METHOD_CLOSE_CONNECTION).disconnect(reason); getConnection(name, CONNECTION_METHOD_CLOSE_CONNECTION).disconnect(reason);
}, },
dispose() { dispose() {
if (disposed) return; if (disposed) return;
@ -184,7 +184,7 @@ export function createEngineConnections<TConnections extends ConnectionMap = Con
function shouldWireBusSessionSource( function shouldWireBusSessionSource(
autoReauthOn: EngineConnectionsOptions['autoReauthOn'] autoReauthOn: EngineConnectionsOptions['autoReauthOn']
): boolean { ): boolean {
if (autoReauthOn === undefined || autoReauthOn === CONN_AUTO_REAUTH_NONE) return false; if (autoReauthOn === undefined || autoReauthOn === CONNECTION_AUTO_REAUTH_NONE) return false;
if (autoReauthOn === CONN_AUTO_REAUTH_STANDARD) return true; if (autoReauthOn === CONNECTION_AUTO_REAUTH_STANDARD) return true;
return autoReauthOn.includes(CONN_AUTO_REAUTH_USER_IDENTITY_CHANGE); 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 { import {
CONN_BUFFER_POLICY_BUFFER, CONNECTION_BUFFER_POLICY_BUFFER,
CONN_BUFFER_POLICY_DROP, CONNECTION_BUFFER_POLICY_DROP,
CONN_SEND_REASON_BUFFER_FULL, CONNECTION_SEND_REASON_BUFFER_FULL,
CONN_DEFAULT_BUFFER_MAX_BYTES, CONNECTION_DEFAULT_BUFFER_MAX_BYTES,
CONN_DEFAULT_BUFFER_MAX_MESSAGES CONNECTION_DEFAULT_BUFFER_MAX_MESSAGES
} from './consts.ts'; } from './consts.ts';
import type { ConnectionBufferOptions, ConnectionFrame, ConnectionSendResult } from './types.ts'; import type { ConnectionBufferOptions, ConnectionFrame, ConnectionSendResult } from './types.ts';
@ -25,7 +25,7 @@ export function createConnectionFrameBuffer(
return { return {
get maxBytes() { get maxBytes() {
return options.maxBytes ?? CONN_DEFAULT_BUFFER_MAX_BYTES; return options.maxBytes ?? CONNECTION_DEFAULT_BUFFER_MAX_BYTES;
}, },
get length() { get length() {
return frames.length; return frames.length;
@ -33,15 +33,15 @@ export function createConnectionFrameBuffer(
canBuffer(frame: ConnectionFrame, allowBuffer: boolean): boolean { canBuffer(frame: ConnectionFrame, allowBuffer: boolean): boolean {
if (!allowBuffer) return false; if (!allowBuffer) return false;
if (frame.ack === true) return false; if (frame.ack === true) return false;
return options.policy === CONN_BUFFER_POLICY_BUFFER; return options.policy === CONNECTION_BUFFER_POLICY_BUFFER;
}, },
shouldDropClosedFrame(): boolean { shouldDropClosedFrame(): boolean {
return options.policy === CONN_BUFFER_POLICY_DROP; return options.policy === CONNECTION_BUFFER_POLICY_DROP;
}, },
push(frame: ConnectionFrame): ConnectionSendResult { 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) { 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); frames.push(frame);
return { ok: true, id: frame.id }; return { ok: true, id: frame.id };

@ -1,12 +1,12 @@
import { import {
CONN_DIAGNOSTIC_EVENTS, CONNECTION_DIAGNOSTIC_EVENTS,
CONN_CLOSE_REASON_HEARTBEAT_TIMEOUT, CONNECTION_CLOSE_REASON_HEARTBEAT_TIMEOUT,
CONN_FRAME_TYPE_PING, CONNECTION_FRAME_TYPE_PING,
CONN_DEFAULT_HEARTBEAT_ENABLED, CONNECTION_DEFAULT_HEARTBEAT_ENABLED,
CONN_DEFAULT_HEARTBEAT_INTERVAL_MS, CONNECTION_DEFAULT_HEARTBEAT_INTERVAL_MS,
CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS, CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS,
CONN_TIMR_KEY_HEARTBEAT, CONNECTION_TIMER_KEY_HEARTBEAT,
CONN_TIMR_KEY_HEARTBEAT_TIMEOUT CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT
} from './consts.ts'; } from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts'; import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import { createFrame } from './serializer.ts'; import { createFrame } from './serializer.ts';
@ -41,7 +41,7 @@ export function createConnectionHeartbeat(
runtime.heartbeatOptions === false ? undefined : runtime.heartbeatOptions?.pingType; runtime.heartbeatOptions === false ? undefined : runtime.heartbeatOptions?.pingType;
const result = await runtime.sendFrame( const result = await runtime.sendFrame(
createFrame({ createFrame({
type: pingType ?? CONN_FRAME_TYPE_PING, type: pingType ?? CONNECTION_FRAME_TYPE_PING,
payload: undefined payload: undefined
}), }),
false false
@ -49,29 +49,29 @@ export function createConnectionHeartbeat(
if (!result.ok) return; if (!result.ok) return;
const timeoutMs = const timeoutMs =
runtime.heartbeatOptions === false runtime.heartbeatOptions === false
? CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS ? CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS
: (runtime.heartbeatOptions?.timeoutMs ?? CONN_DEFAULT_HEARTBEAT_TIMEOUT_MS); : (runtime.heartbeatOptions?.timeoutMs ?? CONNECTION_DEFAULT_HEARTBEAT_TIMEOUT_MS);
runtime.scheduleTimer(CONN_TIMR_KEY_HEARTBEAT_TIMEOUT, timeoutMs, () => { runtime.scheduleTimer(CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT, timeoutMs, () => {
emitConnectionDiagnostic(runtime.diagnostics, CONN_DIAGNOSTIC_EVENTS.HEARTBEAT_TIMEOUT); emitConnectionDiagnostic(runtime.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.HEARTBEAT_TIMEOUT);
runtime.closeTransport(CONN_CLOSE_REASON_HEARTBEAT_TIMEOUT); runtime.closeTransport(CONNECTION_CLOSE_REASON_HEARTBEAT_TIMEOUT);
}); });
} }
return { return {
received(): void { received(): void {
runtime.cancelTimer(CONN_TIMR_KEY_HEARTBEAT_TIMEOUT); runtime.cancelTimer(CONNECTION_TIMER_KEY_HEARTBEAT_TIMEOUT);
}, },
start(): void { start(): void {
if (runtime.heartbeatOptions === false) return; if (runtime.heartbeatOptions === false) return;
if ((runtime.heartbeatOptions?.enabled ?? CONN_DEFAULT_HEARTBEAT_ENABLED) === false) return; if ((runtime.heartbeatOptions?.enabled ?? CONNECTION_DEFAULT_HEARTBEAT_ENABLED) === false) return;
const intervalMs = runtime.heartbeatOptions?.intervalMs ?? CONN_DEFAULT_HEARTBEAT_INTERVAL_MS; const intervalMs = runtime.heartbeatOptions?.intervalMs ?? CONNECTION_DEFAULT_HEARTBEAT_INTERVAL_MS;
runtime.scheduleInterval(CONN_TIMR_KEY_HEARTBEAT, intervalMs, () => { runtime.scheduleInterval(CONNECTION_TIMER_KEY_HEARTBEAT, intervalMs, () => {
void sendHeartbeat(); void sendHeartbeat();
}); });
}, },
stop(): void { stop(): void {
runtime.cancelTimer(CONN_TIMR_KEY_HEARTBEAT); runtime.cancelTimer(CONNECTION_TIMER_KEY_HEARTBEAT);
runtime.cancelTimer(CONN_TIMR_KEY_HEARTBEAT_TIMEOUT); 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 { import {
CONN_DEFAULT_RECONNECT_ENABLED, CONNECTION_DEFAULT_RECONNECT_ENABLED,
CONN_DEFAULT_RECONNECT_FACTOR, CONNECTION_DEFAULT_RECONNECT_FACTOR,
CONN_DEFAULT_RECONNECT_JITTER_MS, CONNECTION_DEFAULT_RECONNECT_JITTER_MS,
CONN_DEFAULT_RECONNECT_MAX_DELAY_MS, CONNECTION_DEFAULT_RECONNECT_MAX_DELAY_MS,
CONN_DEFAULT_RECONNECT_MIN_DELAY_MS CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_MS
} from './consts.ts'; } from './consts.ts';
import type { ConnectionOptions, ConnectionReconnectOptions } from './types.ts'; import type { ConnectionOptions, ConnectionReconnectOptions } from './types.ts';
@ -39,7 +39,7 @@ export function createConnectionReconnectPolicy(
isEnabled(input) { isEnabled(input) {
if (input.disposed || input.intentionalClose) return false; if (input.disposed || input.intentionalClose) return false;
if (disabled) return false; if (disabled) return false;
return options?.enabled ?? CONN_DEFAULT_RECONNECT_ENABLED; return options?.enabled ?? CONNECTION_DEFAULT_RECONNECT_ENABLED;
}, },
next(currentAttempt) { next(currentAttempt) {
const maxAttempts = options?.maxAttempts; const maxAttempts = options?.maxAttempts;
@ -51,10 +51,10 @@ export function createConnectionReconnectPolicy(
ok: true, ok: true,
attempt, attempt,
delayMs: computeBackoffDelay(attempt - 1, { delayMs: computeBackoffDelay(attempt - 1, {
minDelayMs: options?.minDelayMs ?? CONN_DEFAULT_RECONNECT_MIN_DELAY_MS, minDelayMs: options?.minDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MIN_DELAY_MS,
maxDelayMs: options?.maxDelayMs ?? CONN_DEFAULT_RECONNECT_MAX_DELAY_MS, maxDelayMs: options?.maxDelayMs ?? CONNECTION_DEFAULT_RECONNECT_MAX_DELAY_MS,
factor: options?.factor ?? CONN_DEFAULT_RECONNECT_FACTOR, factor: options?.factor ?? CONNECTION_DEFAULT_RECONNECT_FACTOR,
jitterMs: options?.jitterMs ?? CONN_DEFAULT_RECONNECT_JITTER_MS jitterMs: options?.jitterMs ?? CONNECTION_DEFAULT_RECONNECT_JITTER_MS
}) })
}; };
} }

@ -1,12 +1,12 @@
import { isPromiseLike } from '$libs/standard-schema'; import { isPromiseLike } from '$libs/standard-schema';
import { import {
CONN_DIAGNOSTIC_EVENTS, CONNECTION_DIAGNOSTIC_EVENTS,
CONN_SEND_REASON_BUFFER_FULL, CONNECTION_SEND_REASON_BUFFER_FULL,
CONN_SEND_REASON_CLOSED, CONNECTION_SEND_REASON_CLOSED,
CONN_SEND_REASON_INVALID_FRAME, CONNECTION_SEND_REASON_INVALID_FRAME,
CONN_SEND_REASON_SEND_NOT_SUPPORTED, CONNECTION_SEND_REASON_SEND_NOT_SUPPORTED,
CONN_SEND_REASON_SERIALIZE_FAILED, CONNECTION_SEND_REASON_SERIALIZE_FAILED,
CONN_SEND_REASON_TRANSPORT_ERROR CONNECTION_SEND_REASON_TRANSPORT_ERROR
} from './consts.ts'; } from './consts.ts';
import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts'; import { emitConnectionDiagnostic, type ConnectionDiagnostics } from './diagnostics.ts';
import type { ConnectionFrameBuffer } from './frame-buffer.ts'; import type { ConnectionFrameBuffer } from './frame-buffer.ts';
@ -40,21 +40,21 @@ export function createConnectionSender(runtime: ConnectionSenderRuntime): Connec
try { try {
createFrame(frame); createFrame(frame);
} catch (err) { } 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.isConnected()) {
if (runtime.frameBuffer.canBuffer(frame, allowBuffer)) return runtime.frameBuffer.push(frame); if (runtime.frameBuffer.canBuffer(frame, allowBuffer)) return runtime.frameBuffer.push(frame);
if (runtime.frameBuffer.shouldDropClosedFrame()) return { ok: true, id: frame.id }; 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(); const currentTransport = runtime.getTransport();
if (currentTransport === null || !currentTransport.canSend) { 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) { 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; let encoded: string | ArrayBuffer;
@ -63,10 +63,10 @@ export function createConnectionSender(runtime: ConnectionSenderRuntime): Connec
} catch (err) { } catch (err) {
emitConnectionDiagnostic( emitConnectionDiagnostic(
runtime.diagnostics, runtime.diagnostics,
CONN_DIAGNOSTIC_EVENTS.FRAME_ENCODE_FAILED, CONNECTION_DIAGNOSTIC_EVENTS.FRAME_ENCODE_FAILED,
{ error: err } { 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 { try {
@ -75,10 +75,10 @@ export function createConnectionSender(runtime: ConnectionSenderRuntime): Connec
return { ok: true, id: frame.id }; return { ok: true, id: frame.id };
} catch (err) { } catch (err) {
runtime.setError(err); runtime.setError(err);
emitConnectionDiagnostic(runtime.diagnostics, CONN_DIAGNOSTIC_EVENTS.SEND_FAILED, { emitConnectionDiagnostic(runtime.diagnostics, CONNECTION_DIAGNOSTIC_EVENTS.SEND_FAILED, {
error: err 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 { CONNECTION_ERROR_MSG_JSON_DECODE_FAILED } from '../consts.ts';
import { ConnInvalidFrameError } from '../errors.ts'; import { ConnectionInvalidFrameError } from '../errors.ts';
import { assertConnectionFrame, createFrame } from '../serializer.ts'; import { assertConnectionFrame, createFrame } from '../serializer.ts';
import type { ConnectionFrame, ConnectionSerializer } from '../types.ts'; import type { ConnectionFrame, ConnectionSerializer } from '../types.ts';
@ -18,7 +18,7 @@ export const jsonConnectionSerializer: ConnectionSerializer<ConnectionFrame> = {
try { try {
parsed = JSON.parse(rawToText(raw)); parsed = JSON.parse(rawToText(raw));
} catch (cause) { } catch (cause) {
throw new ConnInvalidFrameError(CONN_ERROR_MSG_JSON_DECODE_FAILED, { cause }); throw new ConnectionInvalidFrameError(CONNECTION_ERROR_MSG_JSON_DECODE_FAILED, { cause });
} }
assertConnectionFrame(parsed); assertConnectionFrame(parsed);
return createFrame(parsed); return createFrame(parsed);

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

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

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

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

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

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

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

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

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