# active-app `arts/active-app` is the **composition layer** of the ecosystem. It builds the fixed runtime core, composes opt-in services declared by the application, and exposes the orchestration engine that wires them together. ## Handoff 2026-05-13 `ActiveApp` no absorbe decisiones propias de UIX. Mantiene el core (`logger`, `bus`, `timers`, `orca`, `prefs`) y compone solo los servicios que la aplicacion declara en `services`. El antiguo servicio `frontend` fue retirado. Las preferencias transversales (`direction`, `motion`, `sound`, `haptic`) se proyectan mediante `createActivePrefsDomProjection(...)` cuando la app lo cablea con un `ActiveDom`. Las preferencias visuales (`theme`, `mode`, `density`) pertenecen a Eidos. Si una ruta UIX necesita un toggle claro/oscuro, debe pasarlo a `ActiveEidos.modeSource`; no debe declarar ni escribir `App.prefs.theme` salvo que sea una dimension custom de una app ajena a UIX. La tabla ejecutable de contratos entre `ActiveApp`, `ActiveUix` y las capas UIX vive en [`../../uix/contracts.ts`](../../uix/contracts.ts). ```ts import { createActiveApp } from '$active-app'; import { defineActiveCache, defineActiveLangs, defineActiveSession, defineEngineHttp } from '$active-app/services'; import { applyStandardOrca } from '$active-app/presets'; const App = createActiveApp({ logger: { level: LogLevel.INFO }, services: { langs: defineActiveLangs({ schema: appLang, defaultLocale: 'es' }), http: defineEngineHttp({ baseUrl: '/api' }), cache: defineActiveCache(), session: defineActiveSession({ onRefresh, onRevoke }) } }); applyStandardOrca(App); ``` ## Two layers, three import paths `active-app` is layered to keep bundles small and the contract obvious. | Layer | Path | Loaded when | | ------------------------- | ---------------------- | ---------------------------------------------------------- | | **Core** | `$active-app` | Always — every app needs `createActiveApp`. | | **Service factories** | `$active-app/services` | The app declares any service in `services: { … }`. | | **Orchestration presets** | `$active-app/presets` | The app opts into standard reactions or cherry-picks them. | Each layer is a separate barrel. An app that builds only the core never pulls service factories or presets into its bundle. ## Core vs services The composition has two layers: - **Core** — `Logger`, `Bus`, `Timers`, `Orca`. Always built, never declared as a service. Configurable via the `ActiveAppOptions` root. - **Services** — opt-in pieces that the application declares in `services: { … }`. If a service is not declared, it does not exist on `App`, and TypeScript reports an error when consumers try to access it. The legacy always-present service surface has been removed. `App.langs`, `App.cache`, `App.clipboard`, `App.format`, `App.dom`, `App.storage` and `App.http` exist only when the application declares those slots in `services`. There is no migration period; the project did not have external consumers when the cut happened. ## What the core provides ```ts interface ActiveAppCore { readonly logger: EngineLogger; readonly bus: EngineBus; readonly timers: ActiveTimers; readonly orca: EngineOrca; readonly prefs: ActivePrefs; dispose(): void; } ``` - `logger` defaults to engine defaults (`level: WARN`, `consoleTransport()`). Pass `{ level: NONE, transports: [] }` for silence. - `bus` and `timers` are App-wide singletons. Services that need them declare `'bus'` / `'timers'` in `coreDependencies`. - `orca` is always present, **inert until the application registers actions**. Apps that don't use orchestration pay only for the engine's empty maps. See the [orca README](../orca/README.md) for the supported surface. - `dispose()` publishes `APP_EVENT_DISPOSE_STARTING` first, then tears every constructed service down in reverse order, then the core. ## How services work A service is anything an `AppServiceFactory` produces. Factories live in `arts/active-app/service-factories/` and are exported from `$active-app/services`. ```ts interface AppServiceFactory { readonly name: TName; readonly coreDependencies: TCoreDeps; readonly serviceDependencies?: TServiceDeps; readonly initMode?: 'immediate' | 'lazy'; create(deps: { core: …; services: … }): TInstance; dispose?(instance: TInstance): void; } ``` The schema is just an object literal: ```ts services: { cache: defineActiveCache(), session: defineActiveSession({ onRefresh, onRevoke }) } ``` The builder validates the schema, computes a topological order, builds `immediate` services right away, and exposes `lazy` ones behind getters that materialise on first access. Construction order is dependency-first; disposal runs in reverse. ### Service init modes | Mode | When the service is built | | ---------------- | ------------------------------------------------- | | `lazy` (default) | First time `App.` is read. | | `immediate` | During `createActiveApp()`, after the core is up. | `immediate` is for services with construction-time side effects (subscribing to `BroadcastChannel`, hydrating from storage on boot, etc.). Everything else is `lazy`. ### Service status Every declared service has an observable status: ```ts type ServiceStatus = 'absent' | 'present' | 'failed'; App.services; // Readonly> ``` Mostly used by devtools and tests; application code rarely reads it. ### Failure handling If a factory's `create()` throws, the service status becomes `'failed'`. Subsequent reads of `App.` re-throw the original error wrapped in `AappServiceConstructionFailedError`. The first read sees the same wrapped error — the wrapping is cheap and uniform. ## Available services | Factory | Slot | Notes | | ---------------------------------------- | ------------- | ------------------------------------------------------------------------------------- | | `defineActiveLangs(options)` | `langs` | Schema is required; follows `core.prefs.language` when that dimension exists. | | `defineActiveStorage(options)` | `storage` | Memory adapter by default. | | `defineActiveClipboard(options)` | `clipboard` | Lazy capability wrapper around `navigator.clipboard.writeText` or an injected writer. | | `defineActiveDom(props)` | `dom` | Inert on the server. | | `defineActiveFormat(options)` | `format` | Reads `core.prefs.locale` when that dimension exists. | | `defineActiveCache(options)` | `cache` | Passive — invalidation is driven by orca presets. | | `defineActiveSession(options)` | `session` | Publishes `SESSION_EVENT_*` on the bus. | | `defineActivePerm(options)` | `perm` | Auto-invalidation is OFF; use orca preset. | | `defineActiveAuth(options)` | `auth` | Requires an HTTP client in `options`. | | `defineActiveConnections(options)` | `connections` | Identity tracking via orca preset. | | `defineEngineHttp(options)` | `http` | Engine only — no Active wrapper. | | `defineEngineSium(options)` | `sium` | Wires to `langs` automatically when declared. | ## Orchestration `App.orca` is always present and inert. Reactions are not pre-wired — apps register them explicitly through orca presets in `arts/active-app/presets/`. ```ts import { applyCacheClearOnRevoke, applyCacheClearOnIdentityChange, applyPermInvalidateOnIdentityChange, applyStandardOrca } from '$active-app/presets'; // Cherry-pick: applyCacheClearOnRevoke(App); applyPermInvalidateOnIdentityChange(App); // Or all standard presets at once: applyStandardOrca(App); ``` Each `apply*` returns a detach function for testing and hot-reload. ### Why presets live here, not inside arts An art (`arts/cache`, `arts/perm`, …) does not know about `arts/session` or `arts/orca`. That knowledge belongs to the composition layer. Putting presets in `arts/active-app/` keeps the inter-art dependency graph clean: every art depends only on `libs/` and on the core (`logger`, `bus`, `timers`, `orca`), never on a sibling art. ## Bus context bridge The Svelte-context helper `setBus` / `getBus` lives in `$bus`, not here. The bus is the semantic owner of the propagation pattern; App is just a consumer that calls `setBus(App.bus)` once near the layout root. ```svelte ``` ```svelte ``` `getBus()` throws `BusNoContextError` (from `$libs/bus`) if no bus is in scope — forgetting `setBus()` is a wiring bug, not a degraded mode. ## Events Only one event is owned by `arts/active-app`: ```ts export const APP_EVENT_DISPOSE_STARTING = 'app.dispose.starting'; ``` It fires once at the start of `App.dispose()`, before any service teardown, so subscribers can flush, persist or detach while their dependencies still exist. Everything else used to be a republication of module-level events; those republications have been removed in favour of orca presets that listen to the canonical events directly. `assertEventCanFire(type, where)` and `assertAppEventPayloadSafe(type, payload)` are the safety nets used by typed publishers like `publishAppDisposeStarting`. Both throw structured errors (`AappInvalidEventRuntimeError`, `AappUnsafeEventPayloadError`) that applications can catch. ## Errors | Error | When it fires | | ------------------------------------ | ------------------------------------------------------------------------- | | `AappServiceNameMismatchError` | Schema key !== `factory.name`. | | `AappServiceDependencyCycleError` | A cycle is detected in `serviceDependencies`. | | `AappServiceConstructionFailedError` | A factory's `create()` throws. | | `AappInvalidEventRuntimeError` | An `APP_EVENT_*` published in the wrong runtime. | | `AappUnsafeEventPayloadError` | A sensitive key (`token`, `password`, `cookie`, …) is found in a payload. | `getBus()` throws `BusNoContextError` (from `$libs/bus`) when no bus is in Svelte context — that error belongs to `arts/bus/`, not `active-app/`. All of them extend `CodeError` from `$libs/errs` and have type guards (`isAappServiceNameMismatchError`, …). ## Filesystem layout ``` src/arts/active-app/ ├── README.md ← this file ├── index.ts ← public entry point ($active-app) ├── consts.ts ├── errors.ts ├── events.ts ← APP_EVENT_DISPOSE_STARTING + safety helpers ├── services.ts ← AppServiceFactory contract ├── service-builder.ts ← topology, lazy proxies, dispose ├── active-app.svelte.ts ← createActiveApp() ├── service-factories/ ← $active-app/services │ ├── index.ts │ ├── cache.ts │ ├── clipboard.ts │ ├── langs.ts │ ├── storage.ts │ ├── dom.ts │ ├── format.ts │ ├── http.ts │ ├── session.ts │ ├── auth.ts │ ├── perm.ts │ ├── connections.ts │ └── sium.ts ├── presets/ ← $active-app/presets │ ├── index.ts │ ├── cache-clear-on-revoke.ts │ ├── cache-clear-on-identity-change.ts │ ├── perm-invalidate-on-identity-change.ts │ └── standard.ts └── test/ └── service-builder.test.ts ``` ## Adding a new service Three steps: 1. **Build the art** as a normal `arts//` module. The art does not know about `App` or `services`; it exposes a pure `createActive` or `createEngine` factory. 2. **Write the `define*` factory** in `arts/active-app/service-factories/.ts`. Declare which core deps you read (`coreDependencies: ['logger', 'bus']`) and which sibling services you optionally consume (`serviceDependencies: ['langs']`). Export it from `service-factories/index.ts`. 3. **Optional — add presets** in `arts/active-app/presets/-…ts` for any reactions the standard composition wants to ship. The service is then declarable from any application: ```ts services: { cart: defineActiveCart({ persistKey: 'cart' }); } ``` `App.cart` is now type-safe, lazy by default, and disposed in reverse order when `App.dispose()` runs. ## Test ```bash npx vitest run src/arts/active-app/test ``` The current suite covers the schema validation, topological ordering, lazy/immediate construction, status reporting, dispose order, idempotence, and core/service dependency injection.