22 KiB
arts — runtime artifacts
src/arts/ contains the runtime building blocks of the application. Each
artifact is independent, has its own README, and follows two consistent
naming conventions:
Engine*— public methods over private state (or no state at all). Pure factory; the locale, logger or any volatile input is passed as argument on every call. When an artifact has a true server-authoritative counterpart (perm,cache), the engine lives undersrc/svrs/.Active*— anEngine*that exposes public reactive state. Lives in a.svelte.tsfile because it owns$state. Imports must target the file directly, not the barrel, to keep the rest of the artifact runes-free.
ActiveEngine Contract
Root active artifacts implement the shared ActiveEngine<TSnapshot, TError>
contract from $libs/active:
interface ActiveEngine<TSnapshot, TError> {
readonly loading: boolean;
readonly lastError: TError | null;
readonly disposed: boolean;
snapshot(): TSnapshot;
clearError(): void;
onChange(listener: (snapshot: TSnapshot) => void): () => void;
dispose(): void;
}
Conventions:
- Use direct getters (
Auth.current,Cache.loading,Perms.lastError), not a module-specific.stateobject. - Use
loading, neverpending, for in-flight work. - Use
onChange()for snapshot subscriptions. Lower-level clients may expose their own event buses, but active roots keep this name. dispose()is idempotent, clears owned listeners/entries/resources, and subsequent public operations throw the artifact'sXxxDisposedError.- Root active artifacts that create entries (
ActiveCache.entry(),ActiveConnections.connection(), etc.) own those entries and dispose them when the root is disposed.
Logger And Diagnostics Contract
Every artifact that emits runtime information follows the same two-layer contract:
import type { DiagnosticEvent, Diagnostics, Logger } from '$libs/logger';
- Public options use
logger?: Logger. Do not create artifact-local logger interfaces or narrowed aliases for individual modules. - The root logger implementation is
EngineLoggerfrom$logger; it extends the sharedLoggercontract from$libs/logger. - Artifact code defines
<Artifact>Diagnosticsin a dedicateddiagnostics.tsfile — the canonical home for theDiagnosticCatalog,create<Artifact>Diagnostics(logger?)andemit<Artifact>Diagnostic(...). Every artifact that emits diagnostics ships this file. - Diagnostic event names live in the artifact
consts.tsas*_DIAGNOSTIC_EVENTS. Message strings are named constants inconsts.tsorerrors.ts(or co-located indiagnostics.tswhen only the catalog reads them) — never inline string literals in the catalog or runtime logic. Diagnostics<TEvent>always exposes{ logger, emit(event) }. Theloggerproperty is the commonLogger, so modules that need an ad-hocinfoorerrorstill have the full logger without inventing a second interface.- Level routing is controlled by the logger/transports via the existing per-level enablement map, not by module-specific severity systems.
Typical shape:
export const HTTP_DIAGNOSTIC_EVENTS = {
REQUEST: 'http.request',
NETWORK_ERROR: 'http.network_error'
} as const;
export function createHttpDiagnostics(logger?: Logger): HttpDiagnostics {
return createCatalogDiagnostics({
logger,
defaultCategory: LOGGER_CATEGORY,
catalog: HTTP_DIAGNOSTIC_LOGS
});
}
This gives every module the same path to Sentry, Loki, Datadog, console,
test-capture transports or any future sink: inject one Logger, emit typed
diagnostic events, let logger route.
Error Contract
Errors follow the same rule: strings are centralized, and public programmer errors are typed.
- Error messages and error names live in
errors.tsorconsts.ts. - Runtime code must not throw inline string/template errors outside tests or vendored code.
- Public programmer errors use artifact-specific classes and guards:
SessionDisposedError,ConnInvalidNameError,UnitsUnknownUnitError, etc. - Expected runtime failures should be returned as tagged data/results when the
artifact already has such a contract (
http,connection,perm,cache). - Validation failures are data (
SiumValidationError.issues) and diagnostics are emitted separately when a logger is injected.
Map
| Artifact | Layer(s) | Purpose | Depends on |
|---|---|---|---|
langs |
EngineLangs, ActiveLangs, ActiveMonoLangs |
i18n: type-safe translations, BCP 47 resolution, plurals, refs, JSON round-trip | — |
logger |
EngineLogger |
Structured logger: levels, transports, filters, vitals, dispose | — |
timer |
EngineTimers, ActiveTimers |
Deterministic timer scheduler: clock injection, one-shots, intervals, cancellation, snapshots, backoff | $libs/timers, $logger (optional) |
format |
EngineFormat, ActiveFormat |
Localized formatting: numbers, currency, units, dates | $logger (currency) |
adom |
ActiveDom |
Reactive DOM service: viewport, breakpoints, attribute writes, scroll lock, post-layout read scheduling (measure) |
$libs/dom, $reactive |
motion |
EngineMotion |
Animation runtime: registers + runs --state presets (CSS settle / JS drivers — spring / waapi / rect FLIP); the bridge BOTH UIX layers consume via uix.motion |
MotionDom port (injected; adom satisfies it) |
scene |
EngineScene |
Ambient-scene runtime: mounts WebGL/canvas-2D effects with the citizenship done once (frame loop, off-view pause, DPR cap, mandatory reduced-motion policy, context loss/restore, scene budget, teardown); effects = shared resources for the Ambient pack + the future Aura |
SceneDom port (injected; adom satisfies it) |
ethereal |
$ethereal (computePosition + middleware) |
In-house positioning engine: collision-aware placement (offset/shift/flip/arrow/size/hide), autoUpdate, native CSS-anchor strategy — our parity-verified subset of @floating-ui; the JS + CSS paths BOTH UIX layers consume |
$adom (DOM reads); no other art (@floating-ui = devDep parity baseline) |
clipboard |
ActiveClipboard |
Clipboard write capability with injectable writer and explicit unavailable errors | browser navigator.clipboard or injected writer |
sium |
EngineSium |
Validation contracts: schemas, issues, introspection, Standard Schema interop | $langs (optional), $logger (optional), $libs/days, $libs/color |
storage |
EngineStorage, ActiveStorage |
Reactive sync key/value: pluggable adapters, version+migrate, TTL, validation, intra-tab + cross-tab sync, reactive keys | $sium (Standard Schema interop, optional) |
http |
EngineHttp |
HTTP client: tagged HttpResult, Standard Schema validation, retry, timeouts, hooks, SvelteKit event.fetch integration |
$libs/http, $libs/standard-schema (type-only), $logger |
session |
EngineSession, ActiveSession |
Session lifecycle: adopt/revoke/refresh, auto-refresh, 401-rescue hook, SvelteKit SSR via adoptServer + cookie reader |
$storage, $timer, $http, $logger (optional) |
connection |
EngineConnections, ActiveConnections |
Realtime connection registry: transports, reconnect, heartbeat, request/reply, channels, session bridge | $timer, $logger (optional), $session bridge (optional) |
auth |
ActiveAuth (EngineAuth in $svrs/auth) |
Authentication: password flows, CSRF, current session reflector, devices, logout, server-authoritative auth handlers | $libs/auth, $http, $cache, $svrs/auth |
perm |
ActivePerms (EnginePerms in $svrs/perm) |
Authorization: policy runtime adapter, HTTP client/handlers, cache snapshot, <Can /> guard |
$libs/perm, $libs/svrs, $http, $logger (optional) |
cache |
ActiveCache (EngineCache in $svrs/cache) |
Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | $libs/cache, $storage (adapter), $logger (optional) |
color |
$color namespace (uix.color) |
Isomorphic colour math: OKLCH↔sRGB, APCA, scale/scheme generation, alpha. Pure + stateless — Engine-grade, no class |
— (zero-dep; consumed by eidos at build + runtime) |
perf |
ActivePerf (uix.perf) |
Dev forced-reflow detector: Long Animation Frames → attributed forcedStyleAndLayoutDuration reports; opt-in, inert in prod |
platform LoAF API (Chromium) — zero-dep |
bus |
EngineBus, ActiveBus |
Mechanical typed event bus: typed envelopes, deterministic order, explicit error policy, re-entrancy guard, observability hooks, Svelte adapter; catalog-agnostic, injected by active-app |
$libs/bus (pure contracts) |
orca |
EngineOrca, ActiveOrca |
Active orchestration kernel: runs declarative actions on bus events with order, dependencies, Result + tokens, failure policies, timers, optional transactions, queue policies + static validate() |
$bus, $timer, $logger (optional) |
prefs |
EnginePrefs, ActivePrefs |
Active preferences: resolves user intent × detected environment × effective value for a declared schema; feeds langs / format / direction; generic dimensions; optional DOM projection |
— (optional DOM projection) |
active-app |
ActiveApp |
App composition: core Logger + Bus + Timers + Orca + Prefs, plus declared services via factories | every artifact above |
Composition
Most apps consume the artifacts through active-app:
import { createActiveApp } from '$active-app';
import {
defineActiveClipboard,
defineActiveDom,
defineActiveFormat,
defineActiveLangs
} from '$active-app/service-factories';
const App = createActiveApp({
logger: { level: LogLevel.INFO, transports: [consoleTransport()] },
services: {
langs: defineActiveLangs({ schema, defaultLocale: 'es', fallbackChain: ['en'] }),
clipboard: defineActiveClipboard(),
format: defineActiveFormat(),
dom: defineActiveDom()
}
});
App.langs.t('common.ok');
App.format.currency.format(99.5);
App.prefs.language.set('es-MX'); // propagates to langs when wired by the factory
App always exposes the fixed core (logger, bus, timers, orca,
prefs). Feature services exist only when the application declares their
slot. For translations that means App.langs exists when services.langs
is declared with defineActiveLangs(...); otherwise the property is not part
of the typed surface.
Sium, Session, Connections, Auth and Perms are exposed as
factories because they are feature/page-scoped: App injects shared
services, but construction is explicit at the call site.
const App = createActiveApp({
services: {
sium: defineEngineSium({}),
connections: defineActiveConnections({}),
auth: defineActiveAuth({ initial: data.auth }),
perm: defineActivePerm({ endpoint: '/perm' })
}
});
applyStandardOrca(App);
See active-app/README.md for the full composition contract.
Cross-artifact dependencies
langs logger
\ / | \
\ / | \
format http timer
\ | /|\
adom ──────────────────\ | / | connection
\ \ | / | \
\ \ | / auth perm
───────────── active-app ─ storage ─ cache
:
sium
langsandloggerare the dependency-free roots (zero-deplibraries).formatconsumesloggeronly insidecurrency(rate fetcher diagnostics).siumacceptslangsandloggervia injection; without them it falls back to local message interpolation.timeris the deterministic scheduler consumed bysessionandconnection.httpacceptsloggervia injection (auto-wired throughactive-app) and keeps shared HTTP literals/types in$libs/http.sessionconsumesstoragefor persistence,timerfor auto-refresh, andhttpfor 401-rescue integration.connectionconsumestimerfor reconnect/heartbeat/ack timeouts and accepts the App session bridge when composed throughactive-app.authsplits cleanly:$svrs/authowns identity proof, CSRF and server handlers;$authowns the active client reflector. It feedssession,permandcachethrough ports rather than owning their state.permsplits cleanly:$svrs/permowns the authoritative engine/HTTP handlers, while$permowns the active UI reflector and<Can />.cachesplits cleanly:$svrs/cacheowns the imperative engine, while$cacheowns the active Svelte wrapper and can consumestoragethrough its storage adapter.adomdepends only on the pure helpers inlibs/domand onlibs/reactive.motionis the animation engine consumed by BOTH UIX layers viauix.motion(soma'sPresence+ eidos wrappers), which dissolves the would-be soma→eidos coupling. It imports no other art — the DOM dependency arrives injected via the structuralMotionDomport, whichadomsatisfies.etherealis the positioning engine consumed by BOTH UIX layers — soma'slayers/floating(the JS path) and eidos'srender-css(the native CSS-anchor path) — the same both-layers shape asmotion. It imports no other art; its DOM reads arrive injected via$adom.@floating-uiis a devDep-only parity baseline, never a runtime import.active-appcomposes always-present roots and exposes factories for scoped artifacts (sium,session,connection,auth,perm).
Shared types
| Module | Type | Used by |
|---|---|---|
$locale |
LocaleSource |
format.localeSource, active-app wiring |
$langs |
SupportedLocale (LangBase | ${LangBase}-${string}) |
langs, consumers that want type-safe locales |
Aliases
// svelte.config.js
alias: {
$active-app: 'src/arts/active-app',
$adom: 'src/arts/adom',
$auth: 'src/arts/auth',
$bus: 'src/arts/bus',
$cache: 'src/arts/cache',
$clipboard: 'src/arts/clipboard',
$color: 'src/arts/color',
$connection: 'src/arts/connection',
$ethereal: 'src/arts/ethereal',
$format: 'src/arts/format',
$http: 'src/arts/http',
$langs: 'src/arts/langs',
$logger: 'src/arts/logger',
$motion: 'src/arts/motion',
$orca: 'src/arts/orca',
$perf: 'src/arts/perf',
$perm: 'src/arts/perm',
$prefs: 'src/arts/prefs',
$session: 'src/arts/session',
$sium: 'src/arts/sium',
$storage: 'src/arts/storage',
$svrs: 'src/svrs',
$timer: 'src/arts/timer',
$libs: 'src/libs',
$locale: 'src/libs/locale',
$reactive: 'src/libs/reactive'
}
Bundle policy
Every artifact is designed to tree-shake cleanly:
package.jsondeclares"sideEffects": ["**/*.css", "**/*.svelte"], so any.ts/.svelte.tsmodule that the bundler does not statically reach is dropped from the production bundle.- All barrels use named re-exports (
export { a, b } from './x') instead ofexport *. This lets the bundler prove which symbols are reached from a given import and drop the rest of the source module. .svelte.tsfiles defer module-level state (viewport.svelte.ts,body-scroll-lock.svelte.ts) so importing the barrel does not allocate Svelte runes runtime for unused features.- External adapters (
$logger/adapters/*) and dev helpers ($active-app/testing,$sium/_examples/) live outside the main barrel. A consumer that does not reference them never pays for them.
A consumer that builds
createActiveApp({ services: { langs: defineActiveLangs({ schema }) } }) and
only calls App.langs.t(...) should land roughly in the 40–50 KB minified range.
A consumer that wires every artifact (Sium + Storage cookies + DOM runtime +
Web Vitals) lands in the ~120 KB range. The difference is the per-feature
surface, paid only when reached.
Test pages
Interactive docs now live under web/routes/active and web/routes/uix.
Older /test/* pages may still exist in local branches, but the canonical
artifact names are the directory names listed in the map above:
active-app— full composition end-to-endecosystem— total integration demo: auth, session, perm, cache, http, storage, sium, format, adom, timer, connection, langs and logger in one app flowlangs— i18n with reactive locale switching, plurals, BCP 47logger— log levels, transports, vitals, Sentry integrationformat— numbers / currency / units / dates with shared localeadom— viewport, breakpoints, scroll lock, roving focussium— login / signup / profile schemas with translated issuesstorage— adapters (memory / local / session / cookie), envelope versioning + migrate, raw mode, TTL, mergeDefaults, cross-tab synchttp— GET/POST with Sium validation, retry + Retry-After, timeout, cancellation, lifecycle hooks, taggedHttpResultsession— session lifecycle: adopt/revoke/refresh with generation guard + dedup, auto-refresh, taggedRevokeResult, permission checks, event streamtimer— scheduler snapshots, intervals, cancellation and deterministic clocksconnection— websocket chat and connection/channel lifecycleauth— server-authoritative auth surface: password flow, CSRF, devices, routes and security eventsperm— authorization checks,<Can />, HTTP handlers and client cachecache— cache policies, scopes, tags and active entries
Use /active for the current application/runtime docs and /uix for the UIX
component system docs.