You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3965 lines
137 KiB
3965 lines
137 KiB
import type { ArtifactDocModel } from '../_components/ArtifactDoc.svelte';
|
|
|
|
const artifactApis = {
|
|
auth: [
|
|
{
|
|
title: 'ActiveAuth',
|
|
body: [
|
|
'Client reflector from $auth. It talks to server routes and exposes UI state; it is not the security boundary.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'current',
|
|
purpose: 'Current AuthCurrentView.',
|
|
notes: 'Session status, actor snapshot, AAL/AMR metadata.'
|
|
},
|
|
{
|
|
name: 'authenticated',
|
|
purpose: 'Boolean shortcut.',
|
|
notes: 'True when current.session.status is authenticated.'
|
|
},
|
|
{
|
|
name: 'mfaRequired',
|
|
purpose: 'Boolean shortcut.',
|
|
notes: 'True when the server asks for MFA completion.'
|
|
},
|
|
{
|
|
name: 'loading / lastError / disposed',
|
|
purpose: 'ActiveEngine state.',
|
|
notes: 'Shared active contract used by client roots.'
|
|
},
|
|
{
|
|
name: 'loadCurrent()',
|
|
purpose: 'Reload /current from the server.',
|
|
notes: 'Use after SSR hydration, focus or explicit refresh.'
|
|
},
|
|
{
|
|
name: 'signInPassword(input)',
|
|
purpose: 'Password login through server route.',
|
|
notes: 'Fetches/sends CSRF before state-changing call.'
|
|
},
|
|
{
|
|
name: 'signUpPassword(input)',
|
|
purpose: 'Password signup through server route.',
|
|
notes: 'Returns the new current view.'
|
|
},
|
|
{
|
|
name: 'signOut() / signOutGlobal()',
|
|
purpose: 'End current session or every actor session.',
|
|
notes: 'Clears active current to anonymous after success.'
|
|
},
|
|
{
|
|
name: 'requestPasswordReset() / completePasswordReset()',
|
|
purpose: 'Password recovery flow.',
|
|
notes: 'Server owns token verification and mutation.'
|
|
},
|
|
{
|
|
name: 'requestEmailVerification() / completeEmailVerification()',
|
|
purpose: 'Email verification flow.',
|
|
notes: 'Server owns expiring flows.'
|
|
},
|
|
{
|
|
name: 'listDevices() / revokeDevice(input)',
|
|
purpose: 'Device/session management client calls.',
|
|
notes: 'Route wiring must expose the device endpoints explicitly.'
|
|
},
|
|
{
|
|
name: 'snapshot() / onChange() / clearError() / dispose()',
|
|
purpose: 'Active lifecycle.',
|
|
notes: 'Snapshot is safe to serialize; dispose stops listeners.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineAuth',
|
|
body: [
|
|
'Server authority from $svrs/auth. It owns identity proof, CSRF, flow state, session binding and security events.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'current(input)',
|
|
purpose: 'Read current auth view for a request.',
|
|
notes: 'Usually called from server hooks/load functions.'
|
|
},
|
|
{
|
|
name: 'signUpPassword() / signInPassword()',
|
|
purpose: 'Password credential flows.',
|
|
notes: 'Use ports.store, ports.actors, ports.sess and passwordHasher.'
|
|
},
|
|
{
|
|
name: 'signOut() / signOutGlobal()',
|
|
purpose: 'Session revocation flows.',
|
|
notes: 'Emits security events; session/cache side effects happen only through explicit ports.'
|
|
},
|
|
{
|
|
name: 'issueCsrf() / verifyCsrf()',
|
|
purpose: 'CSRF token lifecycle.',
|
|
notes: 'Uses configured security.csrf options.'
|
|
},
|
|
{
|
|
name: 'requestEmailVerification() / completeEmailVerification()',
|
|
purpose: 'Email verification server flow.',
|
|
notes: 'Requires mailer for delivery in real apps.'
|
|
},
|
|
{
|
|
name: 'requestPasswordReset() / completePasswordReset()',
|
|
purpose: 'Password reset server flow.',
|
|
notes: 'Tokens are server verified and single-use.'
|
|
},
|
|
{
|
|
name: 'listDevices() / revokeDevice()',
|
|
purpose: 'Device management primitives.',
|
|
notes: 'Engine methods exist even if default route map does not expose all endpoints.'
|
|
},
|
|
{
|
|
name: 'startOAuth() / completeOAuth()',
|
|
purpose: 'OAuth/OIDC primitives.',
|
|
notes: 'Provider adapters decide discovery/profile mapping.'
|
|
},
|
|
{
|
|
name: 'handlers / createSvelteKitHandle()',
|
|
purpose: 'Framework routing helpers.',
|
|
notes: 'Default handlers cover current, CSRF, password, recovery and sign-out.'
|
|
},
|
|
{
|
|
name: 'on(name, handler)',
|
|
purpose: 'Subscribe to auth events.',
|
|
notes: 'Security/audit hooks receive structured payloads.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineAuthOptions',
|
|
table: [
|
|
{
|
|
name: 'security',
|
|
purpose: 'CSRF, cookie and password policy.',
|
|
notes: 'CSRF signing key is required when CSRF is enabled.'
|
|
},
|
|
{
|
|
name: 'ports.store / ports.actors',
|
|
purpose: 'Auth persistence and actor lookup/creation.',
|
|
notes: 'Memory and DB adapters live under $svrs/auth.'
|
|
},
|
|
{
|
|
name: 'ports.sess',
|
|
purpose: 'Session lifecycle port.',
|
|
notes: 'Auth never owns the session cookie directly.'
|
|
},
|
|
{
|
|
name: 'ports.logr / ports.timer / ports.crypto',
|
|
purpose: 'Logging, clock and crypto primitives.',
|
|
notes: 'No direct Date.now() or random string shortcuts in flows.'
|
|
},
|
|
{
|
|
name: 'ports.passwordHasher / mailer / http / webauthn',
|
|
purpose: 'Optional mechanisms.',
|
|
notes: 'Injected only when the app enables those flows.'
|
|
},
|
|
{
|
|
name: 'ports.rateLimit',
|
|
purpose: 'Server-side abuse protection.',
|
|
notes: 'Cuts password, recovery and OAuth flows before touching store/provider/mailer.'
|
|
},
|
|
{
|
|
name: 'providers',
|
|
purpose: 'OAuth/OIDC provider adapters.',
|
|
notes: 'Provider tokens are not persisted unless an adapter does so explicitly.'
|
|
},
|
|
{
|
|
name: 'hooks.beforeEvent / hooks.afterEvent',
|
|
purpose: 'Security event interception.',
|
|
notes: 'Useful for audit, metrics and custom side effects.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
buss: [
|
|
{
|
|
title: 'EngineBus',
|
|
body: [
|
|
'The bus is a mechanical typed event engine. It creates envelopes, invokes listeners and reports listener failures; it does not decide application orchestration by itself.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'publish(type, payload, options?)',
|
|
purpose: 'Synchronously publish an event.',
|
|
notes: 'Returns the envelope and collected listener failures.'
|
|
},
|
|
{
|
|
name: 'publishAsync(type, payload, options?)',
|
|
purpose: 'Publish and await async listeners.',
|
|
notes: 'Useful when tests or orchestration need deterministic completion.'
|
|
},
|
|
{
|
|
name: 'on(type, listener, options?)',
|
|
purpose: 'Subscribe to one event type.',
|
|
notes: 'Options support signal, once and listener id.'
|
|
},
|
|
{
|
|
name: 'once(type, listener, options?)',
|
|
purpose: 'Subscribe for one delivery.',
|
|
notes: 'Removes the listener before invoking it.'
|
|
},
|
|
{
|
|
name: 'onAny(listener, options?)',
|
|
purpose: 'Subscribe to every event.',
|
|
notes: 'For diagnostics, test capture and devtools-like surfaces.'
|
|
},
|
|
{
|
|
name: 'listenerCount(type?)',
|
|
purpose: 'Inspect listener counts.',
|
|
notes: 'Helps detect leaks in long-lived apps and tests.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Remove listeners and close the bus.',
|
|
notes: 'Further public operations throw BusDisposedError.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineBusOptions',
|
|
table: [
|
|
{
|
|
name: 'logger',
|
|
purpose: 'Shared Logger contract.',
|
|
notes: 'App injects App.Logger into App.Bus.'
|
|
},
|
|
{
|
|
name: 'clock',
|
|
purpose: 'Timestamp source.',
|
|
notes: 'App injects Timers.clock for deterministic tests.'
|
|
},
|
|
{
|
|
name: 'idFactory',
|
|
purpose: 'Envelope id generation.',
|
|
notes: 'Defaults to incremental bus-* ids.'
|
|
},
|
|
{
|
|
name: 'maxListenersPerEvent',
|
|
purpose: 'Leak warning threshold.',
|
|
notes: 'Default is BUS_DEFAULT_MAX_LISTENERS_PER_EVENT.'
|
|
},
|
|
{
|
|
name: 'listenerErrorMode',
|
|
purpose: 'Default listener failure policy.',
|
|
notes: 'log-and-continue, throw or collect; publish options can override it per event.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'BusEnvelope',
|
|
table: [
|
|
{
|
|
name: 'id / type / payload / at',
|
|
purpose: 'Core event facts.',
|
|
notes: 'Every event is wrapped before listeners receive it.'
|
|
},
|
|
{
|
|
name: 'source',
|
|
purpose: 'Publisher identity.',
|
|
notes: 'Defaults to buss; app translators use their module source.'
|
|
},
|
|
{
|
|
name: 'correlationId / causationId',
|
|
purpose: 'Trace event chains.',
|
|
notes: 'Useful for cross-module integration tests and diagnostics.'
|
|
},
|
|
{
|
|
name: 'context / tags',
|
|
purpose: 'Non-secret metadata.',
|
|
notes: 'Do not put credentials, tokens or authorization headers in public app events.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
sess: [
|
|
{
|
|
title: 'EngineSession / ActiveSession',
|
|
body: [
|
|
'ActiveSession is the reactive facade over the same session contract; both expose the same lifecycle methods.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'current',
|
|
purpose: 'Current session object or null.',
|
|
notes: 'Contains user, credential and data slots.'
|
|
},
|
|
{ name: 'identity', purpose: 'Identity state.', notes: 'none, anonymous or identified.' },
|
|
{
|
|
name: 'generation',
|
|
purpose: 'Monotonic local change counter.',
|
|
notes: 'Useful for cache keys and UI invalidation.'
|
|
},
|
|
{
|
|
name: 'adopt(session)',
|
|
purpose: 'Adopt a client-provided session after schema validation.',
|
|
notes: 'Use for client-side session updates.'
|
|
},
|
|
{
|
|
name: 'adoptServer(session)',
|
|
purpose: 'Adopt SSR-trusted session.',
|
|
notes: 'Bypasses client schema distrust because server already validated.'
|
|
},
|
|
{
|
|
name: 'refresh()',
|
|
purpose: 'Deduped refresh operation.',
|
|
notes: 'Keeps current session if refresh throws; clears if refresh returns null.'
|
|
},
|
|
{
|
|
name: 'revoke(options?)',
|
|
purpose: 'End local/global session.',
|
|
notes: 'Calls onRevoke when configured.'
|
|
},
|
|
{
|
|
name: 'clearLocal(reason?)',
|
|
purpose: 'Clear local state without server revoke.',
|
|
notes: 'Use when server already invalidated the session.'
|
|
},
|
|
{
|
|
name: 'onChange(listener)',
|
|
purpose: 'Subscribe to lifecycle changes.',
|
|
notes: 'Local lifecycle stream. App.Bus integration uses safe sess.* events when bus is injected.'
|
|
},
|
|
{ name: 'dispose()', purpose: 'Stop timers/listeners.', notes: 'Called by App.dispose().' }
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineSessionOptions',
|
|
table: [
|
|
{
|
|
name: 'schemas',
|
|
purpose: 'Optional Standard Schema validation for user/credential/data.',
|
|
notes: 'Protects client-provided adoption.'
|
|
},
|
|
{
|
|
name: 'storage',
|
|
purpose: 'Storage entry config.',
|
|
notes: 'Typically App.storage with local/session/cookie adapter.'
|
|
},
|
|
{
|
|
name: 'onRefresh',
|
|
purpose: 'Server refresh callback.',
|
|
notes: 'Returns next session or null.'
|
|
},
|
|
{
|
|
name: 'onRevoke',
|
|
purpose: 'Server revoke callback.',
|
|
notes: 'Can degrade to local revoke if remote fails.'
|
|
},
|
|
{ name: 'logger', purpose: 'Shared Logger contract.', notes: 'Injected by App.' },
|
|
{
|
|
name: 'bus',
|
|
purpose: 'Optional EventPublisher<SessEventMap>.',
|
|
notes: 'App injects App.Bus so sess.* events can be translated to public app.* events.'
|
|
},
|
|
{
|
|
name: 'broadcastChannel',
|
|
purpose: 'Cross-tab session propagation.',
|
|
notes: 'Optional browser integration.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
cache: [
|
|
{
|
|
title: 'EngineCache / CacheRuntime',
|
|
body: [
|
|
'The server engine extends the pure CacheRuntime from $libs/cache and adds disposal/diagnostics.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'query(options)',
|
|
purpose: 'Read through cache with fetcher.',
|
|
notes: 'Evaluates freshness, scope, policy, epochs and stale-if-error.'
|
|
},
|
|
{
|
|
name: 'get(key, options)',
|
|
purpose: 'Read cached value only.',
|
|
notes: 'Returns undefined on miss/expired/invalid.'
|
|
},
|
|
{
|
|
name: 'set(key, value, options)',
|
|
purpose: 'Write an envelope.',
|
|
notes: 'Stores scope, tags, policy windows and schemaVersion.'
|
|
},
|
|
{
|
|
name: 'invalidate(options)',
|
|
purpose: 'Invalidate by key, keyPrefix or tag.',
|
|
notes: 'Uses epoch bumping instead of scanning every entry.'
|
|
},
|
|
{
|
|
name: 'mutate(options)',
|
|
purpose: 'Run commit plus cache updates/invalidations.',
|
|
notes: 'Useful after writes.'
|
|
},
|
|
{
|
|
name: 'explain(key, options)',
|
|
purpose: 'Debug cache decision.',
|
|
notes: 'Shows action, reason, scope, timings and epoch comparison.'
|
|
},
|
|
{
|
|
name: 'stats()',
|
|
purpose: 'Read event counters.',
|
|
notes: 'Hit/miss/stale/refresh/error counters.'
|
|
},
|
|
{
|
|
name: 'on(type, handler)',
|
|
purpose: 'Subscribe to cache events.',
|
|
notes: 'Use CACHE_EVENT_ALL for every event.'
|
|
},
|
|
{
|
|
name: 'clear()',
|
|
purpose: 'Clear adapter if supported.',
|
|
notes: 'Falls back according to adapter capability.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Close engine and reject future calls.',
|
|
notes: 'ActiveCache calls this automatically.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'ActiveCache',
|
|
table: [
|
|
{
|
|
name: 'orca presets',
|
|
purpose: 'Cross-module reactions to lifecycle events.',
|
|
notes: 'Cache itself is passive. Wire applyCacheClearOnIdentityChange / applyCacheClearOnRevoke (or applyStandardOrca) at the App level to clear on session events.'
|
|
},
|
|
{
|
|
name: 'lastEvent / eventCount',
|
|
purpose: 'Reactive event summary.',
|
|
notes: 'Useful for debug panels.'
|
|
},
|
|
{
|
|
name: 'loading / lastError / disposed',
|
|
purpose: 'ActiveEngine state.',
|
|
notes: 'Tracks active operations.'
|
|
},
|
|
{
|
|
name: 'entry(options)',
|
|
purpose: 'Create an ActiveCacheEntry.',
|
|
notes: 'Entry wraps query/set/invalidate with local status.'
|
|
},
|
|
{
|
|
name: 'snapshot() / onChange() / clearError()',
|
|
purpose: 'Active lifecycle.',
|
|
notes: 'Same convention as other active roots.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'ActiveCacheEntry',
|
|
table: [
|
|
{
|
|
name: 'data / error / status / loading / updatedAt',
|
|
purpose: 'Reactive entry state.',
|
|
notes: 'Status: idle, loading, success, stale, refreshing, degraded or error.'
|
|
},
|
|
{ name: 'load()', purpose: 'Initial query.', notes: 'Uses the entry QueryOptions.' },
|
|
{
|
|
name: 'refresh()',
|
|
purpose: 'Force reload through query.',
|
|
notes: 'Keeps entry state coordinated.'
|
|
},
|
|
{
|
|
name: 'set(value, options?)',
|
|
purpose: 'Write entry value.',
|
|
notes: 'Scope comes from the entry options.'
|
|
},
|
|
{
|
|
name: 'invalidate()',
|
|
purpose: 'Invalidate this key.',
|
|
notes: 'Keeps tags/prefix policies in the runtime.'
|
|
},
|
|
{
|
|
name: 'snapshot() / onChange() / dispose()',
|
|
purpose: 'Entry lifecycle.',
|
|
notes: 'Dispose removes it from the ActiveCache registry.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
stor: [
|
|
{
|
|
title: 'EngineStorage',
|
|
table: [
|
|
{
|
|
name: 'adapter',
|
|
purpose: 'Default SyncStorageAdapter.',
|
|
notes: 'localAdapter, sessionAdapter, cookieAdapter or custom.'
|
|
},
|
|
{
|
|
name: 'namespace',
|
|
purpose: 'Optional key prefix.',
|
|
notes: 'Entry options can override with namespace or false.'
|
|
},
|
|
{
|
|
name: 'entry(key, defaults, options?)',
|
|
purpose: 'Create a StorageEntry.',
|
|
notes: 'Defaults can be a value or factory.'
|
|
},
|
|
{
|
|
name: 'entries()',
|
|
purpose: 'List created entries.',
|
|
notes: 'Engine-owned registry, not adapter scan.'
|
|
},
|
|
{
|
|
name: 'clear()',
|
|
purpose: 'Remove known entries.',
|
|
notes: 'Does not blindly wipe unrelated storage.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Dispose root and entries.',
|
|
notes: 'Rejects future root operations.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'StorageEntry / ActiveStorageEntry',
|
|
table: [
|
|
{
|
|
name: 'key / fullKey',
|
|
purpose: 'Logical and adapter key.',
|
|
notes: 'fullKey includes namespace unless disabled.'
|
|
},
|
|
{
|
|
name: 'get()',
|
|
purpose: 'Read parsed value.',
|
|
notes: 'Applies envelope, ttl, version, migrate, mergeDefaults and validate.'
|
|
},
|
|
{
|
|
name: 'set(value)',
|
|
purpose: 'Serialize and write.',
|
|
notes: 'Active entry also updates current.'
|
|
},
|
|
{
|
|
name: 'update(fn)',
|
|
purpose: 'Read-modify-write.',
|
|
notes: 'Preferred over deep mutations.'
|
|
},
|
|
{
|
|
name: 'remove()',
|
|
purpose: 'Delete adapter value and memory returns to default.',
|
|
notes: 'Different from reset().'
|
|
},
|
|
{
|
|
name: 'reset()',
|
|
purpose: 'Write default value to adapter.',
|
|
notes: 'Useful for explicit user reset.'
|
|
},
|
|
{
|
|
name: 'has()',
|
|
purpose: 'Check whether adapter has a stored value.',
|
|
notes: 'Independent from current default.'
|
|
},
|
|
{
|
|
name: 'current',
|
|
purpose: 'Reactive ActiveStorageEntry value.',
|
|
notes: 'Not a deep persistence proxy; use set/update.'
|
|
},
|
|
{
|
|
name: 'onChange(listener)',
|
|
purpose: 'Subscribe to active value changes.',
|
|
notes: 'Active entries only.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Detach sync listeners.',
|
|
notes: 'Important for per-page entries.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'StorageEntryOptions',
|
|
table: [
|
|
{
|
|
name: 'adapter / namespace',
|
|
purpose: 'Override root storage per entry.',
|
|
notes: 'Use cookies for locale/theme, local for drafts, session for wizards.'
|
|
},
|
|
{
|
|
name: 'serializer',
|
|
purpose: 'Custom parse/stringify.',
|
|
notes: 'Auto-selected for primitives, Date, Set, Map and JSON objects.'
|
|
},
|
|
{
|
|
name: 'version / migrate',
|
|
purpose: 'Envelope versioning.',
|
|
notes: 'If version differs, migrate must return the new shape.'
|
|
},
|
|
{
|
|
name: 'validate',
|
|
purpose: 'Function or Standard Schema validation.',
|
|
notes: 'Rejected values fall back safely and report onError.'
|
|
},
|
|
{
|
|
name: 'mergeDefaults',
|
|
purpose: 'Evolve object shapes.',
|
|
notes: 'Boolean shallow merge or custom merge function.'
|
|
},
|
|
{
|
|
name: 'ttlMs / raw / writeDefaults / syncTabs',
|
|
purpose: 'Expiry and persistence behavior.',
|
|
notes: 'raw disables envelope features by design.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
http: [
|
|
{
|
|
title: 'EngineHttp',
|
|
table: [
|
|
{
|
|
name: 'with(options)',
|
|
purpose: 'Create scoped child client.',
|
|
notes: 'Use with event.fetch in SvelteKit server loads.'
|
|
},
|
|
{
|
|
name: 'get(url, options?) / head() / options()',
|
|
purpose: 'Read-oriented methods.',
|
|
notes: 'GET can use query, schema, timeout, hooks.'
|
|
},
|
|
{
|
|
name: 'post() / put() / patch() / delete()',
|
|
purpose: 'Mutation methods.',
|
|
notes: 'Body, bodySchema and schema are validated through Standard Schema.'
|
|
},
|
|
{
|
|
name: 'hooks',
|
|
purpose: 'Before/after request/result hooks.',
|
|
notes: 'Session rescue, tracing and auth headers live here.'
|
|
},
|
|
{
|
|
name: 'retry / timeout / totalTimeout',
|
|
purpose: 'Resilience controls.',
|
|
notes: 'Retry respects method/idempotency and Retry-After.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'HttpResult',
|
|
table: [
|
|
{
|
|
name: '{ ok: true, value, response }',
|
|
purpose: 'Successful validated response.',
|
|
notes: 'value is parsed/validated payload.'
|
|
},
|
|
{
|
|
name: 'http_error',
|
|
purpose: 'Non-2xx response.',
|
|
notes: 'Status, headers and parsed body stay available.'
|
|
},
|
|
{
|
|
name: 'validation_error',
|
|
purpose: 'Schema rejected payload.',
|
|
notes: 'Contains validation issues.'
|
|
},
|
|
{
|
|
name: 'network_error',
|
|
purpose: 'Fetch threw before response.',
|
|
notes: 'Original error normalized.'
|
|
},
|
|
{
|
|
name: 'timeout',
|
|
purpose: 'Abort by timeout.',
|
|
notes: 'Per-attempt and total timeouts are separate.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineHttpOptions',
|
|
table: [
|
|
{
|
|
name: 'baseUrl / headers / fetch',
|
|
purpose: 'Request defaults.',
|
|
notes: 'Pass SvelteKit event.fetch on the server.'
|
|
},
|
|
{
|
|
name: 'timeout / totalTimeout / retry',
|
|
purpose: 'Failure policy.',
|
|
notes: 'Avoid per-call magic constants.'
|
|
},
|
|
{
|
|
name: 'hooks',
|
|
purpose: 'Composable request/result middleware.',
|
|
notes: 'No direct coupling to sess/auth/perm.'
|
|
},
|
|
{ name: 'logger', purpose: 'Shared Logger contract.', notes: 'Injected by App.' }
|
|
]
|
|
}
|
|
],
|
|
fmts: [
|
|
{
|
|
title: 'EngineFormat / ActiveFormat',
|
|
table: [
|
|
{
|
|
name: 'numbers',
|
|
purpose: 'Numbers sub-engine.',
|
|
notes: 'format, parse, percent, compact and unit helpers.'
|
|
},
|
|
{
|
|
name: 'currency',
|
|
purpose: 'Currency sub-engine.',
|
|
notes: 'Currency resolution, formatting and optional conversion.'
|
|
},
|
|
{
|
|
name: 'units',
|
|
purpose: 'Units sub-engine.',
|
|
notes: 'System defaults, conversion and default-unit formatting.'
|
|
},
|
|
{
|
|
name: 'dates',
|
|
purpose: 'Dates sub-engine.',
|
|
notes: 'Date order, hour cycle and Intl DateTime formatting.'
|
|
},
|
|
{
|
|
name: 'getLocale() / setLocale(locale)',
|
|
purpose: 'Shared locale control.',
|
|
notes: 'Active version usually receives localeSource from App.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Release sub-engines/listeners.',
|
|
notes: 'Called by App.dispose().'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Numbers',
|
|
table: [
|
|
{
|
|
name: 'format() / formatPercent() / formatCompact()',
|
|
purpose: 'Intl number formatting.',
|
|
notes: 'Uses current locale unless options override.'
|
|
},
|
|
{
|
|
name: 'formatCurrency() / formatUnit()',
|
|
purpose: 'Number formatting delegated by currency/units.',
|
|
notes: 'Useful standalone.'
|
|
},
|
|
{ name: 'parse(input)', purpose: 'Locale-aware parse.', notes: 'Uses current separators.' },
|
|
{
|
|
name: 'get/set/clear/isAuto DecimalSeparator',
|
|
purpose: 'Decimal separator preference.',
|
|
notes: 'Manual overrides survive locale changes.'
|
|
},
|
|
{
|
|
name: 'get/set/clear/isAuto GroupSeparator',
|
|
purpose: 'Group separator preference.',
|
|
notes: 'Same auto/manual contract.'
|
|
},
|
|
{
|
|
name: 'get/set/clear/isAuto Grouping',
|
|
purpose: 'Grouping preference.',
|
|
notes: 'Same auto/manual contract.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Currency / Units / Dates',
|
|
table: [
|
|
{
|
|
name: 'currency.getCurrency() / setCurrency() / clearCurrency()',
|
|
purpose: 'Currency auto/manual state.',
|
|
notes: 'Auto derives from locale; manual does not change on locale updates.'
|
|
},
|
|
{
|
|
name: 'currency.format() / formatAs() / convert() / convertAs()',
|
|
purpose: 'Money formatting and conversion.',
|
|
notes: 'Conversion only works when rates are configured.'
|
|
},
|
|
{
|
|
name: 'units.getSystem() / setSystem() / clearSystem()',
|
|
purpose: 'Metric/imperial preference.',
|
|
notes: 'Auto derives from locale.'
|
|
},
|
|
{
|
|
name: 'units.getDefaultUnit() / formatDefault() / convertToDefault()',
|
|
purpose: 'Default units per measurement.',
|
|
notes: 'Defaults are marked by locale/system.'
|
|
},
|
|
{
|
|
name: 'dates.getDateOrder() / setDateOrder() / clearDateOrder()',
|
|
purpose: 'Date order preference.',
|
|
notes: 'Auto derives from locale.'
|
|
},
|
|
{
|
|
name: 'dates.getHourCycle() / setHourCycle() / clearHourCycle()',
|
|
purpose: '12/24h preference.',
|
|
notes: 'Uses default hour cycle resolver unless manual.'
|
|
},
|
|
{
|
|
name: 'dates.formatDate() / formatTime() / formatDateTime()',
|
|
purpose: 'Intl DateTime formatting.',
|
|
notes: 'Uses active locale and options.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
fend: [
|
|
{
|
|
title: 'ActiveFrontend',
|
|
table: [
|
|
{
|
|
name: 'getLocale() / setLocale(locale)',
|
|
purpose: 'Frontend locale source.',
|
|
notes: 'Usually bridged from App.lang.'
|
|
},
|
|
{
|
|
name: 'getDir() / setDir() / clearDir() / isDirAuto()',
|
|
purpose: 'Document direction.',
|
|
notes: 'Auto changes with locale; manual overrides do not.'
|
|
},
|
|
{
|
|
name: 'getTheme() / setTheme()',
|
|
purpose: 'Theme token.',
|
|
notes: 'Applied through Dom attrs.'
|
|
},
|
|
{
|
|
name: 'getMode() / setMode() / clearMode() / isModeAuto()',
|
|
purpose: 'Light/dark/system mode.',
|
|
notes: 'Auto can follow environment preference.'
|
|
},
|
|
{
|
|
name: 'getReducedMotion() / setReducedMotion() / clearReducedMotion() / isReducedMotionAuto()',
|
|
purpose: 'Motion preference.',
|
|
notes: 'Can be auto from media query.'
|
|
},
|
|
{
|
|
name: 'getReducedSound() / setReducedSound()',
|
|
purpose: 'Sound preference.',
|
|
notes: 'Explicit preference.'
|
|
},
|
|
{
|
|
name: 'getDensity() / setDensity()',
|
|
purpose: 'UI density.',
|
|
notes: 'Explicit preference.'
|
|
},
|
|
{
|
|
name: 'onPreferenceChange(listener)',
|
|
purpose: 'Subscribe to changes.',
|
|
notes: 'Used by storage persistence bridge.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Detach DOM/media listeners.',
|
|
notes: 'Called by App.dispose().'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'ActiveFrontendOptions',
|
|
table: [
|
|
{
|
|
name: 'locale / localeSource',
|
|
purpose: 'Initial or reactive locale.',
|
|
notes: 'App passes a source tied to Lang.'
|
|
},
|
|
{
|
|
name: 'dom / target / applyDom',
|
|
purpose: 'DOM writer configuration.',
|
|
notes: 'App passes App.dom by default.'
|
|
},
|
|
{
|
|
name: 'dir / theme / mode / density',
|
|
purpose: 'Initial preferences.',
|
|
notes: 'auto-capable keys follow the shared clear/isAuto convention.'
|
|
},
|
|
{
|
|
name: 'reducedMotion / reducedSound',
|
|
purpose: 'Accessibility preferences.',
|
|
notes: 'Can be persisted through App frontend.persist.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
adom: [
|
|
{
|
|
title: 'ActiveDom',
|
|
table: [
|
|
{
|
|
name: 'breakpoints',
|
|
purpose: 'Configured breakpoint map.',
|
|
notes: 'Default map is available when omitted.'
|
|
},
|
|
{
|
|
name: 'viewport',
|
|
purpose: 'Reactive viewport snapshot.',
|
|
notes: 'No browser tracking during SSR.'
|
|
},
|
|
{
|
|
name: 'currentBreakpoint',
|
|
purpose: 'Current named breakpoint.',
|
|
notes: 'Derived from viewport width.'
|
|
},
|
|
{
|
|
name: 'resolve(value)',
|
|
purpose: 'Resolve responsive maps.',
|
|
notes: 'Accepts scalar or breakpoint object.'
|
|
},
|
|
{
|
|
name: 'isAtLeast(name)',
|
|
purpose: 'Breakpoint comparison.',
|
|
notes: 'Useful for component behavior.'
|
|
},
|
|
{
|
|
name: 'matches(query)',
|
|
purpose: 'Media query helper.',
|
|
notes: 'Browser-only; safe fallback in SSR.'
|
|
},
|
|
{
|
|
name: 'apply(options)',
|
|
purpose: 'Apply attrs/classes/styles.',
|
|
notes: 'Returns a cleanup/remove handle.'
|
|
},
|
|
{
|
|
name: 'remove(handle)',
|
|
purpose: 'Remove applied DOM patch.',
|
|
notes: 'Used by Frontend and test pages.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Detach viewport/listeners.',
|
|
notes: 'Called by App.dispose().'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'DOM Helpers',
|
|
table: [
|
|
{
|
|
name: 'BodyScrollLock',
|
|
purpose: 'Reference-counted body scroll lock.',
|
|
notes: 'Used for modals/drawers.'
|
|
},
|
|
{
|
|
name: 'DOMContext',
|
|
purpose: 'Scoped DOM/focus context.',
|
|
notes: 'Useful for complex components.'
|
|
},
|
|
{
|
|
name: 'RovingFocusGroup',
|
|
purpose: 'Keyboard focus coordination.',
|
|
notes: 'Menus/tabs/toolbars can share it.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
sium: [
|
|
{
|
|
title: 'EngineSium',
|
|
table: [
|
|
{
|
|
name: 'string() / number() / boolean() / literal() / enumOf()',
|
|
purpose: 'Primitive schema builders.',
|
|
notes: 'Composable through pipe().'
|
|
},
|
|
{
|
|
name: 'optional() / nullable() / defaulted()',
|
|
purpose: 'Value wrappers.',
|
|
notes: 'Control absence/null/default behavior.'
|
|
},
|
|
{
|
|
name: 'object() / array() / union() / discriminated() / lazy()',
|
|
purpose: 'Structured schemas.',
|
|
notes: 'Nested issues preserve paths.'
|
|
},
|
|
{
|
|
name: 'pipe() / refine() / transform() / codec()',
|
|
purpose: 'Validation/effect composition.',
|
|
notes: 'Use for domain normalization.'
|
|
},
|
|
{
|
|
name: 'meta()',
|
|
purpose: 'Attach UI metadata.',
|
|
notes: 'Form generators can inspect it.'
|
|
},
|
|
{
|
|
name: 'min() / max() / length() / regex() / email() / url() / integer()',
|
|
purpose: 'Common constraints.',
|
|
notes: 'Return structured Sium issues.'
|
|
},
|
|
{
|
|
name: 'timeValue() / dateValue() / colorValue() and domain helpers',
|
|
purpose: 'days/color validators.',
|
|
notes: 'Uses copied libs/days and libs/color primitives.'
|
|
},
|
|
{
|
|
name: 'resolveIssue() / resolveIssues()',
|
|
purpose: 'Translate issues.',
|
|
notes: 'Uses injected Lang first, local resolver as fallback.'
|
|
},
|
|
{
|
|
name: 'validate() / validateSync()',
|
|
purpose: 'Run schema validation.',
|
|
notes: 'Returns ok/value or issues.'
|
|
},
|
|
{
|
|
name: 'serializeSchema() / walkSchema() / countLeafFields()',
|
|
purpose: 'Introspection.',
|
|
notes: 'Useful for UI/form generation.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineSiumOptions',
|
|
table: [
|
|
{
|
|
name: 'lang',
|
|
purpose: 'Optional EngineLang/ActiveLang-like resolver.',
|
|
notes: 'Injected by defineEngineSium({}).'
|
|
},
|
|
{
|
|
name: 'logger',
|
|
purpose: 'Shared Logger contract.',
|
|
notes: 'Debug validation diagnostics when configured.'
|
|
},
|
|
{ name: 'locale', purpose: 'Default issue locale.', notes: 'App uses current Lang locale.' }
|
|
]
|
|
}
|
|
],
|
|
logr: [
|
|
{
|
|
title: 'Logger contract from $libs/logger',
|
|
table: [
|
|
{
|
|
name: 'trace(category, message, input?)',
|
|
purpose: 'Trace log.',
|
|
notes: 'Modules depend on this minimal Logger interface.'
|
|
},
|
|
{
|
|
name: 'debug(category, message, input?)',
|
|
purpose: 'Debug log.',
|
|
notes: 'Useful for diagnostics in development.'
|
|
},
|
|
{
|
|
name: 'info(category, message, input?)',
|
|
purpose: 'Info log.',
|
|
notes: 'Normal domain/application information.'
|
|
},
|
|
{
|
|
name: 'warn(category, message, input?)',
|
|
purpose: 'Warning log.',
|
|
notes: 'Recoverable or suspicious conditions.'
|
|
},
|
|
{
|
|
name: 'error(category, message, input?)',
|
|
purpose: 'Error log.',
|
|
notes: 'Failed operations.'
|
|
},
|
|
{
|
|
name: 'fatal(category, message, input?)',
|
|
purpose: 'Fatal log.',
|
|
notes: 'Unrecoverable failures.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineLogger',
|
|
table: [
|
|
{
|
|
name: 'setLevel(level)',
|
|
purpose: 'Change enabled level map/runtime threshold.',
|
|
notes: 'Engine decides whether to emit; transports can filter too.'
|
|
},
|
|
{
|
|
name: 'getLogs() / clear() / serialize()',
|
|
purpose: 'In-memory history.',
|
|
notes: 'Useful in tests and debug pages.'
|
|
},
|
|
{
|
|
name: 'setMaxLogs(n)',
|
|
purpose: 'Bound memory history.',
|
|
notes: 'Prevents unbounded growth.'
|
|
},
|
|
{
|
|
name: 'setGlobalContext(context)',
|
|
purpose: 'Attach context to every entry.',
|
|
notes: 'App version, tenant, runtime, etc.'
|
|
},
|
|
{
|
|
name: 'addTransport(transport)',
|
|
purpose: 'Add sink.',
|
|
notes: 'Console, Sentry, Datadog, custom.'
|
|
},
|
|
{ name: 'removeAllTransports()', purpose: 'Clear sinks.', notes: 'Useful in tests.' },
|
|
{
|
|
name: 'subscribe(listener)',
|
|
purpose: 'Observe entries.',
|
|
notes: 'Debug panels and tests.'
|
|
},
|
|
{
|
|
name: 'child(context)',
|
|
purpose: 'Create contextual logger.',
|
|
notes: 'Keeps same transports/history policy.'
|
|
},
|
|
{
|
|
name: 'time(label) / timeEnd(label)',
|
|
purpose: 'Duration helper.',
|
|
notes: 'Emits structured timing log.'
|
|
},
|
|
{
|
|
name: 'flush() / dispose()',
|
|
purpose: 'Transport lifecycle.',
|
|
notes: 'Flush buffered transports before shutdown.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Transport',
|
|
table: [
|
|
{
|
|
name: 'write(entry)',
|
|
purpose: 'Required sink method.',
|
|
notes: 'Called for each accepted entry.'
|
|
},
|
|
{
|
|
name: 'writeBatch(entries)',
|
|
purpose: 'Optional batch sink.',
|
|
notes: 'Used by buffered transports.'
|
|
},
|
|
{
|
|
name: 'levels / filter',
|
|
purpose: 'Transport-level filtering.',
|
|
notes: 'Keeps routing per sink explicit.'
|
|
},
|
|
{
|
|
name: 'failureThrottleMs',
|
|
purpose: 'Avoid transport failure storms.',
|
|
notes: 'Works with deniedFor routing.'
|
|
},
|
|
{
|
|
name: 'flushIntervalMs / buffer',
|
|
purpose: 'Buffering config.',
|
|
notes: 'For remote sinks.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
timer: [
|
|
{
|
|
title: 'TimerScheduler / EngineTimers',
|
|
table: [
|
|
{ name: 'clock', purpose: 'Injected clock.', notes: 'Tests can use fake clocks.' },
|
|
{
|
|
name: 'size',
|
|
purpose: 'Number of active timers.',
|
|
notes: 'Reactive in ActiveTimers snapshots.'
|
|
},
|
|
{
|
|
name: 'schedule(key, delayMs, task, options?)',
|
|
purpose: 'One-shot timer.',
|
|
notes: 'Keyed replacement/cancellation.'
|
|
},
|
|
{
|
|
name: 'scheduleAt(key, at, task, options?)',
|
|
purpose: 'Run at absolute timestamp.',
|
|
notes: 'Uses injected clock.'
|
|
},
|
|
{
|
|
name: 'interval(key, everyMs, task, options?)',
|
|
purpose: 'Interval timer.',
|
|
notes: 'Supports awaitTask and replace.'
|
|
},
|
|
{
|
|
name: 'cancel(key) / cancelAll(scope?) / has(key)',
|
|
purpose: 'Timer control.',
|
|
notes: 'Cancel by key or group.'
|
|
},
|
|
{
|
|
name: 'keys() / entries() / entry(key)',
|
|
purpose: 'Snapshot/debug surface.',
|
|
notes: 'EngineTimers adds these registry methods.'
|
|
},
|
|
{
|
|
name: 'onChange(listener)',
|
|
purpose: 'Subscribe to timer changes.',
|
|
notes: 'ActiveTimers mirrors this into reactive state.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Cancel and close scheduler.',
|
|
notes: 'Called by App.dispose().'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'ActiveTimers',
|
|
table: [
|
|
{
|
|
name: 'entries()',
|
|
purpose: 'Reactive timer snapshots.',
|
|
notes: 'Debug/test pages can render active timers.'
|
|
},
|
|
{ name: 'disposed', purpose: 'Lifecycle flag.', notes: 'Shared active convention.' },
|
|
{
|
|
name: 'clearError()',
|
|
purpose: 'Clear active error state where present.',
|
|
notes: 'Follows ActiveEngine shape.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Backoff helpers',
|
|
table: [
|
|
{
|
|
name: 'computeBackoffDelay(options)',
|
|
purpose: 'Shared exponential/jitter delay.',
|
|
notes: 'Used by conn reconnect and other retry loops.'
|
|
},
|
|
{
|
|
name: 'DEFAULT_BACKOFF_*',
|
|
purpose: 'Shared constants.',
|
|
notes: 'Avoids magic retry numbers across modules.'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
conn: [
|
|
{
|
|
title: 'EngineConnections',
|
|
table: [
|
|
{
|
|
name: 'createConnection(name, options)',
|
|
purpose: 'Create/register a named connection.',
|
|
notes: 'Returns Connection.'
|
|
},
|
|
{
|
|
name: 'connection(name)',
|
|
purpose: 'Read registered connection.',
|
|
notes: 'Undefined when absent.'
|
|
},
|
|
{
|
|
name: 'has(name) / names()',
|
|
purpose: 'Registry inspection.',
|
|
notes: 'Active wrapper exposes derived state too.'
|
|
},
|
|
{
|
|
name: 'openConnection(name) / closeConnection(name) / reconnectConnection(name)',
|
|
purpose: 'Single connection lifecycle.',
|
|
notes: 'close(name) is alias for closeConnection.'
|
|
},
|
|
{
|
|
name: 'openAll() / closeAll() / reconnectAll()',
|
|
purpose: 'Registry-wide lifecycle.',
|
|
notes: 'Useful for app online/offline transitions.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Close and release registry.',
|
|
notes: 'Also disposes timers/listeners.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'ActiveConnections',
|
|
table: [
|
|
{
|
|
name: 'size / activeNames / states',
|
|
purpose: 'Reactive registry snapshots.',
|
|
notes: 'For debug panels and app indicators.'
|
|
},
|
|
{
|
|
name: 'connectedNames / connectingNames / reconnectingNames / failedNames / closedNames',
|
|
purpose: 'State buckets.',
|
|
notes: 'Derived from every registered connection.'
|
|
},
|
|
{
|
|
name: 'allConnected / anyConnected / anyConnecting / anyReconnecting / anyFailed',
|
|
purpose: 'Aggregate booleans.',
|
|
notes: 'Ready for UI status bars.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'EngineConnectionsOptions',
|
|
table: [
|
|
{
|
|
name: 'logger / timers / clock',
|
|
purpose: 'Shared runtime dependencies.',
|
|
notes: 'App injects Logger and Timers automatically.'
|
|
},
|
|
{
|
|
name: 'session',
|
|
purpose: 'ConnectionSessionSource (abstract { onChange } interface) for advanced/manual wiring.',
|
|
notes: 'Optional. The canonical pattern wires reauth/close as orca actions via applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke (or applyStandardOrca) — this option is for standalone / per-connection setups outside the App composition.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Connection',
|
|
table: [
|
|
{
|
|
name: 'state / connected / error / generation',
|
|
purpose: 'Connection state.',
|
|
notes: 'generation changes on lifecycle transitions.'
|
|
},
|
|
{
|
|
name: 'connect() / disconnect() / reconnect()',
|
|
purpose: 'Transport lifecycle.',
|
|
notes: 'Reconnect uses timer backoff.'
|
|
},
|
|
{
|
|
name: 'reauthenticate(payload?)',
|
|
purpose: 'Session/auth reauth operation.',
|
|
notes: 'Called when session changes if enabled.'
|
|
},
|
|
{
|
|
name: 'send(type, payload?)',
|
|
purpose: 'Fire-and-forget frame.',
|
|
notes: 'Buffer behavior depends on options.'
|
|
},
|
|
{
|
|
name: 'request(type, payload?, options?)',
|
|
purpose: 'Request/reply frame.',
|
|
notes: 'ACK registry handles timeout/reply.'
|
|
},
|
|
{
|
|
name: 'channel(name, options?) / channels() / hasChannel() / leaveChannel()',
|
|
purpose: 'Channel management.',
|
|
notes: 'Channels can rejoin after reconnect.'
|
|
},
|
|
{
|
|
name: 'onState(listener) / onAny(listener)',
|
|
purpose: 'Event subscriptions.',
|
|
notes: 'Use for logs/UI and tests.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Close connection and channels.',
|
|
notes: 'Registry calls this on dispose.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'ConnectionChannel',
|
|
table: [
|
|
{
|
|
name: 'state',
|
|
purpose: 'Channel lifecycle state.',
|
|
notes: 'join/left/failed-like state machine.'
|
|
},
|
|
{
|
|
name: 'join() / leave()',
|
|
purpose: 'Channel lifecycle.',
|
|
notes: 'Sends protocol frames through parent connection.'
|
|
},
|
|
{
|
|
name: 'send() / request()',
|
|
purpose: 'Channel-scoped frames.',
|
|
notes: 'Payload is tagged with channel name.'
|
|
},
|
|
{
|
|
name: 'on(type, listener) / onAny(listener)',
|
|
purpose: 'Channel event subscriptions.',
|
|
notes: 'Dispose removes listeners.'
|
|
},
|
|
{
|
|
name: 'dispose()',
|
|
purpose: 'Leave/cleanup channel.',
|
|
notes: 'State becomes terminal after disposal.'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
} satisfies Record<string, NonNullable<ArtifactDocModel['api']>>;
|
|
|
|
export const artifactDocs = {
|
|
auth: {
|
|
section: 'Identity & Security',
|
|
title: 'Auth',
|
|
alias: '$auth',
|
|
summary:
|
|
'Server-authoritative authentication with a reactive client reflector: password flows, CSRF, current session and cache invalidation.',
|
|
factories: ['createActiveAuth', 'createEngineAuth'],
|
|
dependsOn: ['$libs/auth', '$http', '$cache', '$svrs/auth'],
|
|
layer: 'ActiveAuth (client) / EngineAuth (server)',
|
|
status: {
|
|
variant: 'tip',
|
|
title: 'Boundary',
|
|
body: 'Auth proves identity. Session keeps continuity. Perms decide access. Storage must not persist secrets.'
|
|
},
|
|
overview: [
|
|
'Auth is split into a shared language package, a server-authoritative engine and an active Svelte client. The server engine owns identity proof, CSRF, password/recovery flows, OAuth/MFA primitives, device methods and security events. The active client currently exposes current, password, recovery, email verification, sign-out and device methods over HTTP.',
|
|
'Use Auth when the app needs to sign users in or out, load the current actor, request verification or reset flows, and bind that identity proof to session state. Do not use Auth to decide permissions or to store long-lived credentials in the browser.',
|
|
'In an App composition, Auth receives App.http for route calls, App.cache for invalidation and App.Logger for diagnostics.'
|
|
],
|
|
dynamics: [
|
|
'The server creates an EngineAuth with ports. Those ports are the real integration points: store persists auth records, actors maps credentials to actor refs, sess starts or ends sessions, cach clears identity-scoped data, and logr records security events.',
|
|
'The browser creates ActiveAuth only as a reflector. It loads /current, sends CSRF-protected commands to server routes, updates current after successful responses and emits local state changes for UI. A protected server action must never trust ActiveAuth state.',
|
|
'The normal request path is: server hook resolves current auth, page load serializes a safe AuthCurrentView, ActiveAuth hydrates that snapshot, user triggers sign-in/out, server mutates session, and app-event consumers react only when their own auto*On options opt in.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'checking Auth.authenticated on the server',
|
|
purpose: 'ActiveAuth is browser state and can be stale or manipulated.',
|
|
notes: 'Resolve current auth in server hooks/load/actions through $svrs/auth.'
|
|
},
|
|
{
|
|
name: 'putting permissions inside auth callbacks',
|
|
purpose: 'It mixes identity proof with authorization and becomes impossible to audit.',
|
|
notes: 'Auth returns actor/AAL/AMR; $perm decides access.'
|
|
},
|
|
{
|
|
name: 'persisting tokens in Storage',
|
|
purpose: 'Storage is intentionally client-readable and not a secret vault.',
|
|
notes: 'Use opaque HttpOnly session cookies or server-side refresh rotation.'
|
|
},
|
|
{
|
|
name: 'forgetting CSRF on custom routes',
|
|
purpose: 'State-changing browser calls become forgeable.',
|
|
notes: 'Use Auth CSRF helpers or the route handlers that already enforce them.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Client auth from App',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
auth: defineActiveAuth({ initial: data.auth })
|
|
}
|
|
});
|
|
|
|
await App.auth.signInPassword({
|
|
identifier: 'ada@example.com',
|
|
password: 'correct horse battery staple'
|
|
});
|
|
|
|
if (App.auth.authenticated) {
|
|
console.log(App.auth.current.actor?.primaryIdentifier);
|
|
}
|
|
|
|
await App.auth.signOut();`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineAuth(options)',
|
|
purpose: 'Creates the server-side authority for auth flows.',
|
|
notes:
|
|
'Lives in $svrs/auth and receives store, actors, sess, cach, logger, crypto and hasher ports.'
|
|
},
|
|
{
|
|
name: 'createActiveAuth(options)',
|
|
purpose: 'Creates the reactive browser client (raw factory).',
|
|
notes:
|
|
'Direct factory for tests or non-App contexts. App-wired apps use defineActiveAuth instead.'
|
|
},
|
|
{
|
|
name: 'defineActiveAuth(options)',
|
|
purpose: 'Service factory for the App schema.',
|
|
notes:
|
|
'Registered as services.auth in createActiveApp; the builder injects Http, Cache and Logger from the core/services automatically.'
|
|
}
|
|
],
|
|
api: artifactApis.auth,
|
|
sections: [
|
|
{
|
|
title: 'Creation and route wiring',
|
|
body: [
|
|
'Auth has two creation points. The server creates EngineAuth from $svrs/auth with ports for storage, actors, session, cache, crypto and logging. The browser declares the auth slot via defineActiveAuth in the App service schema, which talks to the server routes and mirrors the safe AuthCurrentView through App.auth.',
|
|
'Do not declare the auth service before the server routes exist. The client cannot prove identity by itself; every sign-in, sign-out, CSRF and recovery operation is a server command.'
|
|
],
|
|
code: {
|
|
title: 'Server plus client surface',
|
|
code: `// server
|
|
const Auth = createEngineAuth({
|
|
security,
|
|
ports: { store, actors, sess, cach, logger, timer, crypto, passwordHasher }
|
|
});
|
|
|
|
export const GET = Auth.handlers.current;
|
|
export const POST = Auth.handlers.signInPassword;
|
|
|
|
// client
|
|
const App = createActiveApp({
|
|
services: {
|
|
auth: defineActiveAuth({
|
|
initial: data.auth,
|
|
routes: { current: '/api/auth/current' }
|
|
})
|
|
}
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'State Surface',
|
|
body: [
|
|
'ActiveAuth follows the ActiveEngine convention: direct getters, snapshot(), onChange(), clearError() and dispose().'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'current',
|
|
purpose: 'Latest AuthCurrentView.',
|
|
notes: 'Contains session status and public actor snapshot.'
|
|
},
|
|
{
|
|
name: 'authenticated',
|
|
purpose: 'Convenience boolean.',
|
|
notes: 'Derived from current.session.status.'
|
|
},
|
|
{
|
|
name: 'loading',
|
|
purpose: 'True while a client operation is in flight.',
|
|
notes: 'Shared active-root naming.'
|
|
},
|
|
{
|
|
name: 'lastError',
|
|
purpose: 'Safe client error.',
|
|
notes: 'Secrets and raw backend errors are normalized.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Server Wiring',
|
|
body: [
|
|
'The server engine exposes route handlers and ports instead of importing a specific database, mailer or framework. That keeps auth portable and testable.'
|
|
],
|
|
code: {
|
|
title: 'Server engine shape',
|
|
code: `const Auth = createEngineAuth({
|
|
security,
|
|
ports: {
|
|
store,
|
|
actors,
|
|
sess,
|
|
cach,
|
|
logger: App.Logger,
|
|
timer: App.Timers,
|
|
crypto,
|
|
passwordHasher,
|
|
mailer
|
|
}
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Database Persistence',
|
|
body: [
|
|
'Production auth should not use memory adapters. Use createDbAuthAdapter(repos) from $svrs/auth and map your ORM or SQL repositories to the AuthRepository contract.',
|
|
'The reference PostgreSQL schema lives in src/svrs/auth/sql/postgres.sql. It models credentials, flows, linked accounts, devices, session bindings and refresh token rotation without forcing a concrete ORM.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'auth_credentials',
|
|
purpose: 'Credential and factor records.',
|
|
notes: 'Stores identifier hashes and password hashes, never raw passwords.'
|
|
},
|
|
{
|
|
name: 'auth_flows',
|
|
purpose: 'Expiring server flows.',
|
|
notes: 'Email verification, password reset, OAuth state and MFA/WebAuthn challenges.'
|
|
},
|
|
{
|
|
name: 'auth_session_bindings',
|
|
purpose: 'Auth-to-session binding.',
|
|
notes: 'Lets auth revoke by actor, device, password change, refresh reuse or logout.'
|
|
},
|
|
{
|
|
name: 'auth_refresh_families / auth_refresh_tokens',
|
|
purpose: 'Refresh rotation state.',
|
|
notes: 'Requires transactional row locks; tokens are stored only as hashes.'
|
|
}
|
|
],
|
|
code: {
|
|
title: 'Repository-backed store',
|
|
code: `const store = createDbAuthAdapter({
|
|
credentials,
|
|
flows,
|
|
linkedAccounts,
|
|
devices,
|
|
sessionBindings,
|
|
refreshFamilies,
|
|
refreshTokens,
|
|
transaction: (run) => db.transaction(run)
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Flows',
|
|
bullets: [
|
|
'Password sign-up and sign-in bind a successful identity proof to session state through the session port.',
|
|
'CSRF is requested and sent automatically by ActiveAuth for state-changing client calls.',
|
|
'Email verification and password reset use expiring server flows; tokens are verified on the server.',
|
|
'Device listing and revoke are exposed by the engine/client contracts; route wiring must expose AUTH_ROUTE_PATHS.DEVICES and DEVICE_REVOKE explicitly because the default handler map currently covers current, CSRF, password, recovery and sign-out routes.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Integration Rules',
|
|
bullets: [
|
|
'Auth proves identity and mutates session through explicit server ports; Cache and Perms react to public app events only when their own consumer options opt in.',
|
|
'Auth never stores refresh tokens, passwords, OTPs or CSRF secrets in Storage.',
|
|
'Perms receives actor context from Auth/Session but remains the authorization authority.',
|
|
'Client Auth is UX, not a security boundary.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/auth/test',
|
|
purpose: 'Active client behavior.',
|
|
notes: 'Load current, sign-in/out and safe client state.'
|
|
},
|
|
{
|
|
name: 'src/svrs/auth/test',
|
|
purpose: 'Server flows.',
|
|
notes: 'Password, CSRF, recovery, device primitives and handlers.'
|
|
},
|
|
{
|
|
name: '/test/auth',
|
|
purpose: 'Interactive auth lab.',
|
|
notes: 'Password flow, CSRF and event stream.'
|
|
}
|
|
]
|
|
},
|
|
|
|
buss: {
|
|
section: 'Infrastructure',
|
|
title: 'Bus',
|
|
alias: '$bus',
|
|
summary:
|
|
'Typed event engine used by App.Bus for cross-artifact facts. Pure contracts live in $libs/bus; the engine implementation lives in $bus.',
|
|
factories: ['createEngineBus'],
|
|
dependsOn: ['$libs/logger (Logger interface, optional)'],
|
|
layer: 'EngineBus',
|
|
status: {
|
|
variant: 'tip',
|
|
title: 'Two-layer split',
|
|
body: 'Modules import interfaces, error classes, SILENT_BUS and helpers from $libs/bus. The composition root (aapp) and tests import the createEngineBus implementation from $bus. Modules must never import from $bus directly — that mirrors the Logger / SILENT_LOGGER pattern in $libs/logger.'
|
|
},
|
|
overview: [
|
|
'Bus is the low-level event engine. It owns typed subscriptions, envelopes, listener error handling, listener leak warnings and disposal.',
|
|
'The contract surface (EngineBus, EventPublisher, BusEnvelope, BusListener, error classes, SILENT_BUS, generic constants) lives in $libs/bus as pure types and helpers — no runtime state. The engine factory createEngineBus() lives in $bus and imports its types from $libs/bus.',
|
|
'The only event App owns is APP_EVENT_DISPOSE_STARTING (in arts/active-app/events.ts). Every other public event is owned by its module — SESSION_EVENT_IDENTITY_CHANGED, SESSION_EVENT_REVOKED, etc. Modules publish their own events directly on App.Bus.',
|
|
'Cross-module reactions are explicit: cache.clear / perm.invalidate / connection.reauth on identity change live as orca actions registered through applyStandardOrca(App) or the cherry-picked apply* presets in $active-app/presets. The bus stays inert — it carries events; orca runs reactions.'
|
|
],
|
|
dynamics: [
|
|
'Modules do not create their own buses and do not import from $bus. They accept an injected EngineBus or EventPublisher (interfaces from $libs/bus) and publish/listen through constants exported by the event owner.',
|
|
'Module events are local facts such as session.identity-changed; the publishing module owns the constant and the payload type. There is no "translator" layer — the canonical event names are the module ones, never republished as APP_EVENT_*.',
|
|
'Consumers (UI components, orca actions) subscribe to module events directly via their canonical names. The "modules do not know each other" invariant holds because the module that subscribes is in arts/active-app/presets/, not inside another art.',
|
|
'App publishes APP_EVENT_DISPOSE_STARTING during App.dispose() so subscribers can flush before service teardown begins. That is the only App-owned event.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'importing from $bus in module code',
|
|
purpose: '$bus exposes the concrete engine; module code must depend only on the contract layer.',
|
|
notes: 'Import EngineBus, EventPublisher, BusSubscription, BusEnvelope, SILENT_BUS, etc. from $libs/bus. Only aapp and tests touch $bus.'
|
|
},
|
|
{
|
|
name: 'putting secrets in app events',
|
|
purpose: 'Public events can be logged, captured or inspected by diagnostics.',
|
|
notes: 'Use actor ids, tenant ids, causes and correlation ids; never tokens, passwords or authorization headers.'
|
|
},
|
|
{
|
|
name: 'expecting publish to clear data',
|
|
purpose: 'Publishing is observable and does not mutate other artifacts by default.',
|
|
notes: 'Register an orca action via applyStandardOrca(App) or a cherry-picked apply* preset in $active-app/presets so the desired reactions run on the published event.'
|
|
},
|
|
{
|
|
name: 'using bus for private in-module events',
|
|
purpose: 'It creates needless coupling and noise.',
|
|
notes: 'Keep private event emitters inside the artifact; use App.Bus for cross-artifact facts.'
|
|
},
|
|
{
|
|
name: 'creating createEngineBus() inside modules',
|
|
purpose: 'It fragments the event graph and makes cross-artifact behavior invisible.',
|
|
notes: 'Use App.Bus in application code; modules should accept an injected bus/publisher.'
|
|
},
|
|
{
|
|
name: 'publishing inline event strings',
|
|
purpose: 'Magic strings drift and break refactors.',
|
|
notes: 'Declare event constants in uppercase at the owner boundary — for example SESSION_EVENT_IDENTITY_CHANGED in arts/session/consts.ts.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Central App.Bus',
|
|
code: `import { SESSION_EVENT_IDENTITY_CHANGED } from '$session';
|
|
|
|
// Direct subscription — UI-style reactions
|
|
const sub = App.Bus.on(SESSION_EVENT_IDENTITY_CHANGED, (event) => {
|
|
console.log(event.payload.identity.to);
|
|
});
|
|
|
|
// Cross-module reactions go through orca presets
|
|
import { applyStandardOrca } from '$active-app/presets';
|
|
applyStandardOrca(App); // wires cache.clear / perm.invalidate
|
|
|
|
sub.unsubscribe();`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: '$libs/bus (interfaces)',
|
|
purpose: 'Pure contract surface — what every module imports.',
|
|
notes: 'Exports EngineBus, EventPublisher, BusEnvelope, BusListener, BusSubscription, BusPublishOptions, BusListenerErrorMode, error classes, SILENT_BUS, generic constants. No runtime state.'
|
|
},
|
|
{
|
|
name: 'SILENT_BUS',
|
|
purpose: 'No-op bus that satisfies EngineBus.',
|
|
notes: 'Use as a default for modules that take an optional bus, mirror of SILENT_LOGGER. Importable from $libs/bus.'
|
|
},
|
|
{
|
|
name: 'createEngineBus(options?)',
|
|
purpose: 'Build a real bus engine. The implementation factory.',
|
|
notes: 'Importable only from $bus (the artifact). Used by App and tests; never inside an artifact module.'
|
|
},
|
|
{
|
|
name: 'App.Bus',
|
|
purpose: 'Application bus instance, always-present.',
|
|
notes: 'Created by createActiveApp with Logger and Timers.clock injected. Modules receive it through their factory options.'
|
|
}
|
|
],
|
|
api: artifactApis.buss,
|
|
sections: [
|
|
{
|
|
title: 'Layer split — Where to import from',
|
|
body: [
|
|
'libs/bus owns the pure contract: interfaces, generic constants, error classes, SILENT_BUS, helpers. Nothing here has runtime state. Module code (cache, session, perm, connection, …) imports types from $libs/bus exclusively — that is the rule.',
|
|
'arts/bus owns the engine implementation (createEngineBus, createSvelteEngineBus) and the Svelte-context bridge (setBus / getBus, exported from $bus). The composition root (active-app) and tests import the engine from $bus; nothing else does. The split mirrors the Logger / SILENT_LOGGER pattern in $libs/logger.',
|
|
'A module that needs to publish or subscribe accepts a bus through its factory options, typed against the interface from $libs/bus. The App service builder passes App.Bus when constructing services that declare bus as a coreDependency; otherwise the module falls back to SILENT_BUS so the publish path stays unconditional.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'arts/<module> code',
|
|
purpose: 'Always import from $libs/bus.',
|
|
notes: 'Types, SILENT_BUS, error classes, helpers. Never createEngineBus.'
|
|
},
|
|
{
|
|
name: 'arts/active-app/active-app.svelte.ts',
|
|
purpose: 'Imports createSvelteEngineBus from $bus.',
|
|
notes: 'The single legitimate consumer of the engine implementation in production code.'
|
|
},
|
|
{
|
|
name: 'Tests that build their own bus',
|
|
purpose: 'Import createEngineBus from $bus.',
|
|
notes: 'Acceptable; bus engines are cheap and self-contained.'
|
|
},
|
|
{
|
|
name: 'arts/active-app/events.ts',
|
|
purpose: 'Imports interfaces from $libs/bus.',
|
|
notes: 'Defines APP_EVENT_DISPOSE_STARTING + safety helpers; never touches the engine.'
|
|
}
|
|
],
|
|
code: {
|
|
title: 'Canonical module factory pattern',
|
|
code: `// arts/sess/types.ts
|
|
import type { EventPublisher } from '$libs/bus';
|
|
|
|
export interface EngineSessionOptions<TUser, TCredential, TData> {
|
|
readonly bus?: EventPublisher; // contract, not engine
|
|
readonly logger?: Logger;
|
|
// ...
|
|
}
|
|
|
|
// arts/sess/engine-session.ts
|
|
import { SILENT_BUS } from '$libs/bus';
|
|
|
|
export function createEngineSession(options: EngineSessionOptions) {
|
|
const bus = options.bus ?? SILENT_BUS;
|
|
// ...
|
|
bus.publish(SESSION_EVENT_IDENTITY_CHANGED, payload);
|
|
}`
|
|
}
|
|
},
|
|
{
|
|
title: 'Naming Convention — Scoped event values',
|
|
body: [
|
|
'Every event constant value across the framework is scoped with the artifact prefix to disambiguate aggregated logs, devtools and any future cross-bus serialization. A bare "delete" or "hit" leaves observers guessing which module emitted it; "cache.delete" or "cache.hit" does not.',
|
|
'Format: <artifact>.<concept> with snake_case for compound terms inside a level (cach.stale_if_error) and dots for hierarchy (cach.refresh.start). Same convention auth has used since v0 with AUTH_EVENT_NAMES.',
|
|
'Method labels passed to ensureLive(method) follow the same rule: sess.adopt, cach.invalidate, auth.signOut. Bare names like "adopt" or "invalidate" never appear in error messages.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'CACHE_EVENT_*',
|
|
purpose: 'Cache engine events.',
|
|
notes: '"cache.hit", "cache.delete", "cache.refresh.start", "cache.stale_if_error", …'
|
|
},
|
|
{
|
|
name: 'CONNECTION_EVENT_*',
|
|
purpose: 'Connection lifecycle events.',
|
|
notes: '"connection.state", "connection.message", "connection.channels", "connection.disposed".'
|
|
},
|
|
{
|
|
name: 'TIMER_EVENT_*',
|
|
purpose: 'Timer scheduler events.',
|
|
notes: '"timer.scheduled", "timer.running", "timer.completed", "timer.cancelled", …'
|
|
},
|
|
{
|
|
name: 'SESSION_EVENT_*',
|
|
purpose: 'Session bus events.',
|
|
notes: '"session.changed", "session.identity.changed", "session.revoked", …'
|
|
},
|
|
{
|
|
name: 'EVENT_* (sess lifecycle)',
|
|
purpose: 'Discriminant values inside SessionChange payloads.',
|
|
notes: '"session.lifecycle.initial", "session.lifecycle.adopted", "session.lifecycle.revoked", …'
|
|
},
|
|
{
|
|
name: 'AUTH_EVENT_NAMES.*',
|
|
purpose: 'Auth bus events.',
|
|
notes: '"auth.sign_in.succeeded", "auth.csrf.issued", "auth.password.changed", …'
|
|
},
|
|
{
|
|
name: 'APP_EVENT_*',
|
|
purpose: 'Public app contract events.',
|
|
notes: '"app.user.identity.changed", "app.tenant.switched", …'
|
|
},
|
|
{
|
|
name: 'PERM_METHOD_* / CACHE_METHOD_* / AUTH_METHOD_* / ENGINE_METHOD_*',
|
|
purpose: 'Method labels for ensureLive(method) error messages.',
|
|
notes: '"perm.check", "cache.invalidate", "auth.signOut", "session.adopt", …'
|
|
},
|
|
{
|
|
name: 'BUS_EVENT_ALL',
|
|
purpose: 'Framework bus engine wildcard.',
|
|
notes: 'Stays "*" because the framework bus owns the symbol. Per-emitter wildcards are scoped (CACHE_EVENT_ALL = "cache.*", etc.).'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'App-event contract',
|
|
body: [
|
|
'Only one event is owned by arts/active-app: APP_EVENT_DISPOSE_STARTING (in arts/active-app/events.ts). It fires once at the start of App.dispose() before any service teardown begins.',
|
|
'Every other public event is owned by its module. Examples: SESSION_EVENT_IDENTITY_CHANGED and SESSION_EVENT_REVOKED in arts/session/consts.ts; CONNECTION_* events in arts/connection. There is no longer a translator layer that re-publishes them as APP_EVENT_*.',
|
|
'App events should flow through their typed publisher (publishAppDisposeStarting, publish<Module>Event*) so payload typing, runtime guards and unsafe-payload checks stay in one place. Raw bus.on(...) is acceptable for low-level tests.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Centralization rule',
|
|
body: [
|
|
'There is one app-level bus per application: App.Bus, built by createActiveApp. Modules do not call createEngineBus() for their own private island. They accept an injected bus, EventPublisher or EventSubscriber from the composition root.',
|
|
'createEngineBus() remains public because App, isolated services and unit tests need to build a bus engine, but it is not the normal usage pattern inside framework modules.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Reactions live in orca, not on the bus',
|
|
body: [
|
|
'Earlier drafts of this art proposed a translator layer that turned module events into app events plus per-consumer autoInvalidateOn / autoReauthOn flags. That machinery was removed. Reactions now live as orca actions registered through the apply* presets in $active-app/presets.',
|
|
'applyStandardOrca(App) registers every standard preset whose required services are declared on App. Apps that want a tailored set cherry-pick individual apply* functions instead.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'applyCacheClearOnIdentityChange(App)',
|
|
purpose: 'Identity-change reaction.',
|
|
notes: 'Listens to SESSION_EVENT_IDENTITY_CHANGED and calls App.cache.clear().'
|
|
},
|
|
{
|
|
name: 'applyCacheClearOnRevoke(App)',
|
|
purpose: 'Revoke reaction.',
|
|
notes: 'Listens to SESSION_EVENT_REVOKED and calls App.cache.clear().'
|
|
},
|
|
{
|
|
name: 'applyPermInvalidateOnIdentityChange(App)',
|
|
purpose: 'Permission cache reaction.',
|
|
notes: 'Listens to SESSION_EVENT_IDENTITY_CHANGED and calls App.perm.invalidate().'
|
|
},
|
|
{
|
|
name: 'applyStandardOrca(App)',
|
|
purpose: 'Aggregator.',
|
|
notes: 'Registers every standard preset whose required services are declared on App.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Identity-change flow',
|
|
body: [
|
|
'This example shows the complete chain. The bus carries the SESSION_EVENT_IDENTITY_CHANGED event; orca runs every action registered for it; each action calls the imperative API of the affected service.'
|
|
],
|
|
code: {
|
|
title: 'User switch -> session event -> orca-driven reactions',
|
|
code: `import { createActiveApp } from '$active-app';
|
|
import {
|
|
defineActiveCache,
|
|
defineActiveConnections,
|
|
defineActivePerm,
|
|
defineActiveSession
|
|
} from '$active-app/services';
|
|
import { applyStandardOrca } from '$active-app/presets';
|
|
|
|
type User = { readonly id: string; readonly tenantId: string };
|
|
type ChatCredential = { readonly accessToken: string };
|
|
|
|
const App = createActiveApp({
|
|
services: {
|
|
cache: defineActiveCache({}),
|
|
perm: defineActivePerm({ endpoint: '/api/perm' }),
|
|
session: defineActiveSession<User, ChatCredential>({ onRefresh, onRevoke }),
|
|
connections: defineActiveConnections({})
|
|
}
|
|
});
|
|
|
|
applyStandardOrca(App);
|
|
|
|
const Chat = App.connections.createConnection('chat', {
|
|
transport: createWebSocketTransport({ url: '/ws/chat' }),
|
|
auth: () => {
|
|
const credential = App.session?.current?.credential;
|
|
return credential ? { accessToken: credential.accessToken } : null;
|
|
}
|
|
});
|
|
|
|
// Example transition: login, SSR hydration or actor switch received from server.
|
|
App.session.adoptServer(nextSessionFromServer);
|
|
|
|
// 1. App.session updates state, then publishes SESSION_EVENT_IDENTITY_CHANGED on App.Bus.
|
|
// 2. Orca runs the registered actions in a single trace:
|
|
// - cache-clear-on-identity -> App.cache.clear()
|
|
// - perm-invalidate-on-identity -> App.perm.invalidate()
|
|
// - connections-reauth-on-identity -> App.connections.reauthenticateAll()
|
|
// which calls each connection's auth() with the fresh credential.
|
|
// 3. If the session is revoked, the same orca pipeline runs
|
|
// cache-clear-on-revoke and connections-close-on-revoke, leaving no
|
|
// socket alive carrying the revoked credentials.`
|
|
}
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/buss/test',
|
|
purpose: 'Bus core behavior.',
|
|
notes: 'Publish, async publish, once, onAny, listener errors and disposal.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/active-app.test.ts',
|
|
purpose: 'App translator behavior.',
|
|
notes: 'Session changes publish public identity events and dispose publishes starting event.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/ecosystem.integration.test.ts',
|
|
purpose: 'Cross-artifact reactions.',
|
|
notes: 'Cache, Perms and Connections react only when their consumer options opt in.'
|
|
}
|
|
]
|
|
},
|
|
|
|
sess: {
|
|
section: 'Identity & Security',
|
|
title: 'Session',
|
|
alias: '$session',
|
|
summary:
|
|
'Session lifecycle primitive for adopt, revoke, refresh, auto-refresh, SSR adoption and HTTP 401 rescue.',
|
|
factories: ['createEngineSession', 'createActiveSession'],
|
|
dependsOn: ['$storage', '$timer', '$http', '$logger (optional)', '$bus (optional)'],
|
|
layer: 'EngineSession / ActiveSession',
|
|
overview: [
|
|
'Session is not authentication. It does not verify passwords, OAuth callbacks or permissions. It keeps continuity once another layer has established identity.',
|
|
'The session shape has three slots: user, credential and data. User is identity, credential is how the client can refresh or authenticate to the server, and data is session-scoped application state such as tenantId or cartId.',
|
|
'The engine is deterministic and testable: refresh can be deduped, storage is pluggable, timers can be injected and lifecycle changes can emit safe sess.* events when a bus is injected.'
|
|
],
|
|
dynamics: [
|
|
'Session starts from a trusted source: SSR data, an auth success response or a refresh callback. adoptServer() is for data already validated on the server; adopt() validates client-provided values through configured schemas.',
|
|
'Refresh is a controlled lifecycle operation. Multiple concurrent refresh calls dedupe into one remote call; a successful refresh replaces current, a null result clears the session, and a thrown refresh keeps the previous value while reporting the error.',
|
|
'Revoke is different from clearLocal(). revoke() calls the configured server revoke callback and then clears state. clearLocal() only wipes local memory/storage when the server already invalidated the session.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'using Session as Auth',
|
|
purpose:
|
|
'Session cannot prove identity; it only stores continuity after identity was proven.',
|
|
notes: 'Use $auth for login/proof, $session for lifecycle and propagation.'
|
|
},
|
|
{
|
|
name: 'calling adopt() with SSR data',
|
|
purpose:
|
|
'It treats trusted server data like untrusted browser input and can produce confusing validation paths.',
|
|
notes: 'Use adoptServer(data.session) for server-validated payloads.'
|
|
},
|
|
{
|
|
name: 'mutating nested current data directly',
|
|
purpose: 'Deep mutation can skip persistence and event propagation depending on shape.',
|
|
notes: 'Adopt a new session object or use the exposed lifecycle methods.'
|
|
},
|
|
{
|
|
name: 'not clearing caches on revoke',
|
|
purpose: 'Actor-scoped data can remain visible after logout.',
|
|
notes: 'Wire applyStandardOrca(App) (or applyCacheClearOnRevoke directly) so SESSION_EVENT_REVOKED triggers App.cache.clear() automatically.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Active session',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
session: defineActiveSession<User, JwtCredential, SessionData>({
|
|
storage: { adapter: localAdapter, key: 'session' },
|
|
onRefresh: async (current) => refreshSession(current),
|
|
onRevoke: async (current) => revokeSession(current)
|
|
})
|
|
}
|
|
});
|
|
|
|
App.session.adoptServer(data.session);
|
|
|
|
if (App.session.identity === 'identified') {
|
|
console.log(App.session.current?.user);
|
|
}`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineSession(options)',
|
|
purpose: 'Pure session engine.',
|
|
notes: 'No Svelte state; useful for services and tests.'
|
|
},
|
|
{
|
|
name: 'createActiveSession(options)',
|
|
purpose: 'Reactive Svelte wrapper (raw factory).',
|
|
notes: 'Direct factory for tests or non-App contexts.'
|
|
},
|
|
{
|
|
name: 'defineActiveSession<TUser, TCredential?, TData?>(options)',
|
|
purpose: 'Service factory for the App schema.',
|
|
notes: 'Registered as services.session in createActiveApp; the builder injects Logger and Bus from the core.'
|
|
}
|
|
],
|
|
api: artifactApis.sess,
|
|
sections: [
|
|
{
|
|
title: 'Creation and wiring',
|
|
body: [
|
|
'In an application, declare the session slot via defineActiveSession(...) in the createActiveApp({ services }) schema. The builder injects Logger and Bus from the core. Use createEngineSession()/createActiveSession() directly only in tests, isolated services or when you deliberately do not use App.',
|
|
'The storage option decides where the local session snapshot lives. The callbacks onRefresh and onRevoke are the only places that should call the server. Session itself does not know your endpoint shape.'
|
|
],
|
|
code: {
|
|
title: 'App-owned session',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
session: defineActiveSession({
|
|
storage: { adapter: localAdapter, key: 'session' },
|
|
onRefresh: (current) => App.http.post('/api/session/refresh', current),
|
|
onRevoke: (current, options) =>
|
|
App.http.post('/api/session/revoke', { current, options })
|
|
})
|
|
}
|
|
});
|
|
|
|
App.session.adoptServer(data.session);`
|
|
}
|
|
},
|
|
{
|
|
title: 'Identity States',
|
|
table: [
|
|
{ name: 'none', purpose: 'No local session exists.', notes: 'current is null.' },
|
|
{
|
|
name: 'anonymous',
|
|
purpose: 'Tracked but unidentified session.',
|
|
notes: 'Useful for carts or anonymous journeys.'
|
|
},
|
|
{
|
|
name: 'identified',
|
|
purpose: 'Session has a user.',
|
|
notes: 'Authorization can now evaluate an actor.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Lifecycle',
|
|
bullets: [
|
|
'adopt() validates client-provided sessions against optional Standard Schema contracts.',
|
|
'adoptServer() trusts the server and is the SSR hydration path.',
|
|
'refresh() preserves the current session if the refresh call throws, but expires it if refresh returns null.',
|
|
'revoke() can be local or global; global degrades to local if the server revocation fails.'
|
|
]
|
|
},
|
|
{
|
|
title: 'HTTP Integration',
|
|
body: [
|
|
'The HTTP integration can intercept 401 responses, trigger a deduped refresh and retry once with fresh credentials. A sentinel header prevents infinite retry loops.'
|
|
],
|
|
code: {
|
|
title: '401 rescue',
|
|
code: `const hook = createBeforeErrorHook(Sess, {
|
|
applyAuth: (request, session) => {
|
|
request.headers.set('authorization', 'Bearer ' + session.credential.accessToken);
|
|
}
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Bus integration',
|
|
body: [
|
|
'The session art publishes its own SESSION_EVENT_* events on App.Bus directly. There are no longer any republished APP_EVENT_* shadows; the canonical events are the module ones.',
|
|
'Cross-module reactions (cache.clear on revoke, perm.invalidate on identity change, connection reauth) live as orca actions registered through applyStandardOrca(App) or the cherry-picked apply* presets in $active-app/presets.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/sess/test',
|
|
purpose: 'Lifecycle and integrations.',
|
|
notes: 'Refresh, revoke, actor metadata, HTTP and auto-refresh.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/ecosystem.integration.test.ts',
|
|
purpose: 'Cross-module bus bridge.',
|
|
notes: 'Session events become app identity events and opted-in consumers react.'
|
|
},
|
|
{
|
|
name: '/test/sess',
|
|
purpose: 'Interactive page.',
|
|
notes: 'Adopt, revoke, refresh, events and permission demo.'
|
|
}
|
|
]
|
|
},
|
|
|
|
perm: {
|
|
section: 'Identity & Security',
|
|
title: 'Perms',
|
|
alias: '$perm',
|
|
summary:
|
|
'Authorization runtime for typed actor + action + resource + context decisions, with server authority and active client reflection.',
|
|
factories: [
|
|
'createEnginePerms',
|
|
'createActivePerms',
|
|
'createPermHttpHandlers',
|
|
'loadActivePermPolicies',
|
|
'createPermDatabaseProviders'
|
|
],
|
|
dependsOn: ['$libs/perm', '$svrs/perm', '$http', '$logger (optional)', '$bus (optional)'],
|
|
layer: 'ActivePerms (client) / EnginePerms (server)',
|
|
status: {
|
|
variant: 'warn',
|
|
title: 'Security boundary',
|
|
body: 'The server engine is the authority. ActivePerms is for UX, cache and rendering helpers only.'
|
|
},
|
|
overview: [
|
|
'Perms is not a simple RBAC helper. It models authorization as explicit decisions using roles, attributes, relations and context in the same runtime.',
|
|
'The server engine evaluates policies and returns rich decisions: allow, deny, not applicable or indeterminate. The client reflector batches remote checks, caches snapshots, can invalidate from public app events and exposes a Can component for UI gates.',
|
|
'Policies can be explained and list queries can be filtered or compiled into query plans when the provider supports it.'
|
|
],
|
|
quickStart: {
|
|
title: 'Policy runtime',
|
|
code: `const schema = definePermSchema({
|
|
actors: { user: { attributes: { role: 'string' } } },
|
|
resources: { project: { actions: ['update'], attributes: { locked: 'boolean' } } },
|
|
context: { risk: { mfa: 'boolean' } }
|
|
});
|
|
|
|
const engine = createEnginePerms({
|
|
schema,
|
|
policies: definePolicies(schema, [
|
|
allow('project.update').when(
|
|
and(attr('actor.role').eq('admin'), attr('context.risk.mfa').eq(true))
|
|
)
|
|
])
|
|
});`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEnginePerms(options)',
|
|
purpose: 'Authoritative policy engine.',
|
|
notes: 'Use on server routes, services and jobs.'
|
|
},
|
|
{
|
|
name: 'createPermHttpHandlers(engine, resolveActor)',
|
|
purpose: 'HTTP handlers for check/batch/what/explain.',
|
|
notes: 'Keeps client thin and server authoritative.'
|
|
},
|
|
{
|
|
name: 'loadActivePermPolicies(repository, query)',
|
|
purpose: 'Load active PolicyIR rows from an app-owned repository.',
|
|
notes: 'Normalizes namespace and returns deterministic PolicyIR[].'
|
|
},
|
|
{
|
|
name: 'createPermDatabaseProviders(options)',
|
|
purpose: 'Adapt DB relation lookups to PermProviders.',
|
|
notes: 'Returns unknown when tenant/resource scope is unsafe.'
|
|
},
|
|
{
|
|
name: 'createActivePerms(options)',
|
|
purpose: 'Reactive client reflector (raw factory).',
|
|
notes: 'Direct factory for tests or non-App contexts.'
|
|
},
|
|
{
|
|
name: 'defineActivePerm(options)',
|
|
purpose: 'Service factory for the App schema.',
|
|
notes: 'Registered as services.perm in createActiveApp; the builder injects http, logger and bus from the core/services.'
|
|
}
|
|
],
|
|
sections: [
|
|
{
|
|
title: 'Decision Model',
|
|
table: [
|
|
{
|
|
name: 'allow',
|
|
purpose: 'Access granted by a matching policy.',
|
|
notes: 'May include TTL and explanation.'
|
|
},
|
|
{ name: 'deny', purpose: 'Access explicitly denied.', notes: 'Deny overrides allow.' },
|
|
{
|
|
name: 'not_applicable',
|
|
purpose: 'No policy matched.',
|
|
notes: 'Fail closed in protected endpoints.'
|
|
},
|
|
{
|
|
name: 'indeterminate',
|
|
purpose: 'Provider or evaluation could not decide.',
|
|
notes: 'Treat as denied for security-sensitive paths.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Client usage',
|
|
code: {
|
|
title: 'Active permissions',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
session: defineActiveSession({ ... }),
|
|
perm: defineActivePerm({
|
|
endpoint: '/api/perm',
|
|
scopeKey: () => App.session?.current?.user?.id ?? 'anonymous'
|
|
})
|
|
}
|
|
});
|
|
|
|
const decision = await App.perm.check({
|
|
action: 'project.update',
|
|
resource: { type: 'project', id: 'p1', locked: false },
|
|
context: { risk: { mfa: true } }
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Reacting to identity changes',
|
|
body: [
|
|
'ActivePerms is a passive runtime. It does not subscribe to the bus on its own; cross-module reactions live in orca presets registered at the App level.',
|
|
'applyPermInvalidateOnIdentityChange(App) registers an orca action that calls App.perm.invalidate() when the session art emits SESSION_EVENT_IDENTITY_CHANGED. The standard aggregator applyStandardOrca(App) wires it together with the cache presets.',
|
|
'Tenant switches and "permissions refreshed" notifications are app-defined events; register a custom orca action that calls App.perm.invalidate() when those fire.'
|
|
],
|
|
code: {
|
|
title: 'Wire the standard reactions',
|
|
code: `import { applyStandardOrca } from '$active-app/presets';
|
|
|
|
applyStandardOrca(App); // runs perm.invalidate() on SESSION_EVENT_IDENTITY_CHANGED`
|
|
}
|
|
},
|
|
{
|
|
title: 'Database Persistence',
|
|
body: [
|
|
'$svrs/perm ships a reference PostgreSQL model in src/svrs/perm/sql/postgres.sql. It creates permission_policies, permission_relations and permission_decision_audit.',
|
|
'The framework does not own your ORM. Map the SQL to Prisma, Drizzle, Kysely or raw SQL and expose a tiny repository that returns PolicyIR rows and relation facts.',
|
|
'Policy rows are versioned per tenant and namespace. Store validated PolicyIR JSON, publish exactly one active version per policy id, and invalidate permission/cache scopes after publishing.'
|
|
],
|
|
code: {
|
|
title: 'Server DB wiring',
|
|
code: `const policies = await loadActivePermPolicies(policyRepository, {
|
|
tenantId,
|
|
namespace: 'default'
|
|
});
|
|
|
|
const Perms = createEnginePerms({
|
|
schema,
|
|
policies,
|
|
providers: createPermDatabaseProviders({
|
|
relations: relationRepository,
|
|
resolveTenant: () => tenantId
|
|
})
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Relation Repository',
|
|
body: [
|
|
'permission_relations is the generic ReBAC table. Use it for facts like project.owner, team.member or invoice.approver when the domain does not already have a stronger table.',
|
|
'Relation providers must fail closed. If tenant id, resource id or backend state is unavailable, return unknown instead of false unless the database definitely says the relation does not exist.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'hasRelation(input)',
|
|
purpose: 'Check one actor/resource/relation tuple.',
|
|
notes: 'Used by rel(...).is(actor()) policy conditions.'
|
|
},
|
|
{
|
|
name: 'listSubjects(input)',
|
|
purpose: 'Reverse lookup actors for a resource.',
|
|
notes: 'Used by who() and admin/audit views.'
|
|
},
|
|
{
|
|
name: 'listResources(input)',
|
|
purpose: 'Find resources reachable by a subject.',
|
|
notes: 'Useful for list pages and reverse queries.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Can Component',
|
|
body: [
|
|
'Can is a UI convenience component. It asks ActivePerms whether content should render, but it never replaces server checks. Protected mutations and reads must still call the server engine.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/svrs/perm/test',
|
|
purpose: 'Server engine and handlers.',
|
|
notes: 'Policy evaluation, HTTP handlers, diagnostics.'
|
|
},
|
|
{
|
|
name: 'src/arts/perm/test',
|
|
purpose: 'Active client.',
|
|
notes: 'Cache, remote checks and snapshot behavior.'
|
|
},
|
|
{
|
|
name: '/test/perm',
|
|
purpose: 'Interactive authorization lab.',
|
|
notes: 'Checks, Can, explains and role changes.'
|
|
}
|
|
]
|
|
},
|
|
|
|
cache: {
|
|
section: 'Data',
|
|
title: 'Cache',
|
|
alias: '$cache',
|
|
summary:
|
|
'Data coherence layer with deterministic keys, scopes, policies, stale-while-revalidate, tags and active entries.',
|
|
factories: ['createEngineCache', 'createActiveCache'],
|
|
dependsOn: ['$libs/cache', '$storage (adapter)', '$logger (optional)', '$bus (optional)'],
|
|
layer: 'ActiveCache (client) / EngineCache (server)',
|
|
overview: [
|
|
'Cache answers more than "do I have this value?". It decides freshness, scope safety, invalidation status, stale serving and what to do if the origin fails.',
|
|
'The pure runtime lives in libs/cache. Server and active wrappers compose that runtime with adapters, diagnostics and Svelte state.',
|
|
'Scopes are part of the key. Private data should be cached under tenant, actor or permission scopes, never as public.'
|
|
],
|
|
dynamics: [
|
|
'A query normalizes its key, resolves scope, reads the adapter, evaluates policy windows and tag epochs, then decides whether to return fresh data, serve stale data while refreshing, serve stale-if-error or call the fetcher.',
|
|
'Invalidation does not have to delete every entry immediately. Tag epochs mark entries as stale/invalidated lazily, which keeps distributed adapters cheap while explain() can still tell why a value was rejected.',
|
|
'ActiveCache wraps the same runtime with entry state. The UI reads data/status/loading/error from ActiveCacheEntry; the server or service layer uses EngineCache for request handlers, jobs and SSR.',
|
|
'ActiveCache is passive: it does not subscribe to the bus on its own. Cross-module reactions live in orca presets registered at the App level — applyCacheClearOnIdentityChange, applyCacheClearOnRevoke, or applyStandardOrca for the bundled set.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'public scope for private data',
|
|
purpose:
|
|
'Different users or tenants can observe cached values that were not meant for them.',
|
|
notes: 'Use scope: actor, tenant or permission and provide a scopeResolver.'
|
|
},
|
|
{
|
|
name: 'using raw strings as keys',
|
|
purpose: 'Ad-hoc keys collide and cannot be invalidated by structure.',
|
|
notes: "Use deterministic array keys such as ['project', projectId]."
|
|
},
|
|
{
|
|
name: 'forgetting tags on queries',
|
|
purpose: 'Mutations cannot invalidate related reads cleanly.',
|
|
notes: 'Add stable tags to reads and invalidate those tags after writes.'
|
|
},
|
|
{
|
|
name: 'hiding cache bugs',
|
|
purpose: 'Stale data failures are difficult to debug from UI symptoms.',
|
|
notes: 'Use explain(), stats() and diagnostics when behavior surprises you.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Query cache',
|
|
code: `const project = await App.cache.query({
|
|
key: ['project', projectId],
|
|
scope: 'tenant',
|
|
policy: 'interactive',
|
|
tags: [{ type: 'project', id: projectId }],
|
|
fetcher: () => App.http.get('/api/projects/' + projectId)
|
|
});`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: '$libs/cache.createCacheRuntime(options)',
|
|
purpose: 'Pure cache runtime.',
|
|
notes: 'Not exported by the $cache barrel.'
|
|
},
|
|
{
|
|
name: 'createEngineCache(options)',
|
|
purpose: 'Imperative cache surface.',
|
|
notes: 'Use in server/services/workers.'
|
|
},
|
|
{
|
|
name: 'createActiveCache(options)',
|
|
purpose: 'Reactive Svelte wrapper.',
|
|
notes: 'Adds entry state and active loading/error surface.'
|
|
},
|
|
{
|
|
name: 'App.cache',
|
|
purpose: 'Schema-declared cache via defineActiveCache.',
|
|
notes:
|
|
'Memory adapter by default; configure scopeResolver and adapter for production. Reactions to identity/revoke events live in orca presets, not here.'
|
|
}
|
|
],
|
|
api: artifactApis.cache,
|
|
sections: [
|
|
{
|
|
title: 'Creation and adapter wiring',
|
|
body: [
|
|
'App.cache is always present and defaults to an in-memory adapter. That is safe for first use, tests and local UI state, but production private data should configure scopeResolver and an intentional adapter strategy.',
|
|
'Server-side cache roots should be created from $svrs/cache. Client active cache roots are UI helpers and should not become the source of truth for permission-sensitive data.'
|
|
],
|
|
code: {
|
|
title: 'Scoped cache root',
|
|
code: `const Cache = createEngineCache({
|
|
namespace: 'app',
|
|
adapter: memoryCacheAdapter({ maxEntries: 5_000 }),
|
|
defaultPolicy: 'interactive',
|
|
scopeResolver: () => ({
|
|
tenantId: Sess.current?.data.tenantId,
|
|
actorId: Sess.current?.user?.id,
|
|
permissionHash: Perms.snapshot().version
|
|
}),
|
|
logger: App.Logger
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Server and Client Boundary',
|
|
body: [
|
|
'$libs/cache contains the pure runtime. $svrs/cache is the server-side entry point for request handlers, jobs and shared services. $cache/createActiveCache is the Svelte-facing wrapper for UI state.',
|
|
'The same policy/key/scope semantics apply in both places, but server caches normally use process/distributed adapters while client caches use memory or Storage-backed adapters.'
|
|
],
|
|
table: [
|
|
{
|
|
name: 'defaultMemoryAdapter',
|
|
purpose: 'Options for the implicit memory adapter.',
|
|
notes:
|
|
'Only applies when adapter is omitted; demos/tests can suppress the production warning explicitly.'
|
|
},
|
|
{
|
|
name: 'server request cache',
|
|
purpose: 'Deduplicate work during one request.',
|
|
notes: 'Good for SSR loaders and backend services.'
|
|
},
|
|
{
|
|
name: 'process memory cache',
|
|
purpose: 'Fast L1 cache.',
|
|
notes: 'Use max entries/size and clear by actor/tenant on identity changes.'
|
|
},
|
|
{
|
|
name: 'storage/tiered/distributed adapter',
|
|
purpose: 'Durable or shared L2.',
|
|
notes: 'Redis/KV adapters should honor epochs, TTL and namespace boundaries.'
|
|
},
|
|
{
|
|
name: 'ActiveCacheEntry',
|
|
purpose: 'UI state around one query.',
|
|
notes: 'Status/loading/error live here; security decisions still belong server-side.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Policies',
|
|
table: [
|
|
{
|
|
name: 'interactive',
|
|
purpose: 'Normal UI data.',
|
|
notes: 'Short fresh window, stale-while-revalidate.'
|
|
},
|
|
{
|
|
name: 'catalog',
|
|
purpose: 'Stable catalog/config data.',
|
|
notes: 'Longer stale and stale-if-error windows.'
|
|
},
|
|
{
|
|
name: 'privateSession',
|
|
purpose: 'Private session data.',
|
|
notes: 'Short windows and no persistence by default.'
|
|
},
|
|
{
|
|
name: 'realtime',
|
|
purpose: 'Almost no cache.',
|
|
notes: 'Use for data that must always revalidate.'
|
|
},
|
|
{
|
|
name: 'immutable',
|
|
purpose: 'Versioned immutable data.',
|
|
notes: 'Can be cached indefinitely.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Invalidation',
|
|
body: [
|
|
'Entries can be invalidated by exact key, key prefix, tag, scope or predicate. Tags are the recommended high-level contract between mutations and cached reads.'
|
|
],
|
|
code: {
|
|
title: 'Tag invalidation',
|
|
code: `await App.cache.invalidate({
|
|
tags: [{ type: 'project', id: projectId }]
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Reacting to identity changes',
|
|
body: [
|
|
'ActiveCache is a passive runtime — it never subscribes to the bus on its own. Cross-module reactions live in orca presets registered at the App level.',
|
|
'applyCacheClearOnIdentityChange wires SESSION_EVENT_IDENTITY_CHANGED to App.cache.clear(); applyCacheClearOnRevoke does the same for SESSION_EVENT_REVOKED. applyStandardOrca(App) registers both at once when the corresponding services are declared.',
|
|
'Cache stays decoupled from session, auth and perm internals: it only exposes the imperative API (clear, invalidate); the orca preset decides when to call it.'
|
|
],
|
|
code: {
|
|
title: 'Wire reactions at the App level',
|
|
code: `import { createActiveApp } from '$active-app';
|
|
import { defineActiveCache, defineActiveSession }
|
|
from '$active-app/services';
|
|
import { applyStandardOrca } from '$active-app/presets';
|
|
|
|
const App = createActiveApp({
|
|
services: {
|
|
cache: defineActiveCache({
|
|
scopeResolver: () => ({
|
|
actorId: App.session?.current?.user?.id,
|
|
tenantId: App.session?.current?.data?.tenantId
|
|
})
|
|
}),
|
|
session: defineActiveSession({ ... })
|
|
}
|
|
});
|
|
|
|
applyStandardOrca(App);
|
|
// → SESSION_EVENT_IDENTITY_CHANGED runs App.cache.clear()
|
|
// → SESSION_EVENT_REVOKED runs App.cache.clear()`
|
|
}
|
|
},
|
|
{
|
|
title: 'Explain',
|
|
body: [
|
|
'explain() tells why the cache served, missed, refreshed or rejected an entry. This is intentionally part of the public surface because cache bugs are otherwise invisible.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/libs/cache/test',
|
|
purpose: 'Pure runtime.',
|
|
notes: 'Keys, policies, query, mutation, invalidation and refresh.'
|
|
},
|
|
{
|
|
name: 'src/arts/cach/test',
|
|
purpose: 'Active wrapper.',
|
|
notes: 'Reactive entries and operation state.'
|
|
},
|
|
{
|
|
name: '/test/cach',
|
|
purpose: 'Interactive cache lab.',
|
|
notes: 'Policies, scopes, tags and active entries.'
|
|
}
|
|
]
|
|
},
|
|
|
|
stor: {
|
|
section: 'Data',
|
|
title: 'Storage',
|
|
alias: '$storage',
|
|
summary:
|
|
'Reactive synchronous key/value storage with pluggable adapters, envelopes, versioning, TTL, validation and cross-tab sync.',
|
|
factories: ['createEngineStorage', 'createActiveStorage'],
|
|
dependsOn: ['$sium (Standard Schema interop, optional)', '$logger (optional)'],
|
|
layer: 'EngineStorage / ActiveStorage',
|
|
overview: [
|
|
'Storage is the framework primitive for client-safe persistence: preferences, drafts, non-secret state and SSR-readable cookies.',
|
|
'Each entry has defaults, serializer, optional version/migration, optional validation, TTL and remove/reset semantics. Active entries expose current as reactive state.',
|
|
'Storage is intentionally synchronous in v1. Async stores such as IndexedDB can be added later through a separate async contract.',
|
|
'Only createEngineStorage() and createActiveStorage() are root factories. Adapters are storage backends: they are passed to a root through options.adapter or overridden per entry.'
|
|
],
|
|
dynamics: [
|
|
'A root owns the adapter default, namespace, entry registry, bus and diagnostics. Every call to entry() creates a handle for one logical key and stores strings through the selected adapter.',
|
|
'Reads always pass through serializer, envelope, TTL, migration, mergeDefaults and validation. If a stored value is expired, corrupt or invalid, the entry falls back to its default and reports through diagnostics/onError.',
|
|
'Active entries add a reactive current property and an onChange stream. current is not a deep persistence proxy: assigning a nested field does not write to the adapter unless you reassign the value or call update().'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'treating adapters as root factories',
|
|
purpose:
|
|
'Adapters only store strings; they do not own entries, lifecycle, namespaces or diagnostics.',
|
|
notes:
|
|
'Create a root with createActiveStorage/createEngineStorage and pass adapters into it.'
|
|
},
|
|
{
|
|
name: 'storing secrets',
|
|
purpose: 'localStorage/sessionStorage/cookies used here are not a secret vault.',
|
|
notes: 'Do not store passwords, OTPs, refresh tokens or private provider tokens in $storage.'
|
|
},
|
|
{
|
|
name: 'entry.current.foo = value',
|
|
purpose: 'Nested mutation can skip persistence because current is not a deep proxy.',
|
|
notes:
|
|
'Use entry.update(prev => ({ ...prev, foo: value })) or assign entry.current to a new object.'
|
|
},
|
|
{
|
|
name: 'raw with version or ttlMs',
|
|
purpose: 'Raw mode deliberately bypasses the envelope where version/TTL live.',
|
|
notes: 'Use raw for readable cookies; use envelope mode for migration and TTL.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Active entry',
|
|
code: `import { createActiveStorage, localAdapter, cookieAdapter } from '$storage';
|
|
|
|
// Root created directly.
|
|
const Storage = createActiveStorage({
|
|
adapter: localAdapter,
|
|
namespace: 'app'
|
|
});
|
|
|
|
// In an application root, App.storage is already an ActiveStorage root.
|
|
const draft = App.storage.entry('profile-draft', () => ({
|
|
name: '',
|
|
bio: ''
|
|
}), {
|
|
ttlMs: 30 * 60_000,
|
|
version: 2,
|
|
mergeDefaults: true
|
|
});
|
|
|
|
draft.update((value) => ({ ...value, bio: 'Hello' }));
|
|
console.log(draft.current.bio);
|
|
|
|
// Adapter override for one entry only.
|
|
const locale = App.storage.entry('locale', 'es', {
|
|
adapter: cookieAdapter({ path: '/', maxAge: 31_536_000 }),
|
|
namespace: false,
|
|
raw: true
|
|
});`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineStorage(options)',
|
|
purpose: 'Root factory: creates an imperative EngineStorage.',
|
|
notes: 'Pass adapter, namespace, logger and onError here.'
|
|
},
|
|
{
|
|
name: 'createActiveStorage(options)',
|
|
purpose: 'Root factory: creates a reactive ActiveStorage.',
|
|
notes: 'Wraps EngineStorage and adds reactive entries.'
|
|
}
|
|
],
|
|
api: artifactApis.stor,
|
|
sections: [
|
|
{
|
|
title: 'Creation Model',
|
|
body: [
|
|
'Storage has one root and many entries. The root is created with createEngineStorage() or createActiveStorage(); entries are created through Storage.entry(key, defaults, options?).',
|
|
'Adapters are not roots. localAdapter, sessionAdapter, cookieAdapter and createMemoryAdapter() implement SyncStorageAdapter. They decide where strings are stored; the root decides namespaces, entry registry, diagnostics and lifecycle.',
|
|
'aapp creates App.storage internally with createActiveStorage(). Feature code should normally use App.storage.entry(...). Create your own root only in tests, SSR helpers or isolated subsystems.'
|
|
],
|
|
code: {
|
|
title: 'Root vs adapter',
|
|
code: `const Storage = createActiveStorage({
|
|
adapter: localAdapter, // backend used by default
|
|
namespace: 'app', // root-level key prefix
|
|
logger: App.Logger
|
|
});
|
|
|
|
const theme = Storage.entry('theme', 'base');
|
|
|
|
const locale = Storage.entry('locale', 'es', {
|
|
adapter: cookieAdapter({ path: '/' }), // per-entry backend override
|
|
namespace: false,
|
|
raw: true
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Adapters',
|
|
table: [
|
|
{
|
|
name: 'localAdapter',
|
|
purpose: 'Browser localStorage backend.',
|
|
notes: 'Singleton adapter; supports cross-tab sync through the storage event.'
|
|
},
|
|
{
|
|
name: 'sessionAdapter',
|
|
purpose: 'Browser sessionStorage backend.',
|
|
notes: 'Session-scoped backend; SSR-safe fallback outside browser.'
|
|
},
|
|
{
|
|
name: 'cookieAdapter(options)',
|
|
purpose: 'Browser cookie backend.',
|
|
notes: 'Factory because cookie attributes are configured per use.'
|
|
},
|
|
{
|
|
name: 'cookieAdapter.fromCookies(cookies, options)',
|
|
purpose: 'Server-side cookie backend.',
|
|
notes: 'Uses a SvelteKit-compatible Cookies object without depending on @sveltejs/kit.'
|
|
},
|
|
{
|
|
name: 'createMemoryAdapter(seed?)',
|
|
purpose: 'In-memory backend.',
|
|
notes: 'Factory because each call gets isolated storage for defaults, tests and SSR.'
|
|
},
|
|
{
|
|
name: 'custom SyncStorageAdapter',
|
|
purpose: 'Any synchronous string key/value backend.',
|
|
notes: 'Must implement getItem, setItem, removeItem and optional onChange.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Entry Semantics',
|
|
table: [
|
|
{
|
|
name: 'remove()',
|
|
purpose: 'Delete adapter value and return memory to default.',
|
|
notes: 'Storage is clean after remove.'
|
|
},
|
|
{
|
|
name: 'reset()',
|
|
purpose: 'Write the default value to the adapter.',
|
|
notes: 'Useful when default should be persisted.'
|
|
},
|
|
{
|
|
name: 'writeDefaults',
|
|
purpose: 'Persist defaults on first read.',
|
|
notes: 'False by default to avoid contaminating storage.'
|
|
},
|
|
{
|
|
name: 'raw',
|
|
purpose: 'Store plain value without envelope.',
|
|
notes: 'Useful for cookies such as locale/theme.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Versioning',
|
|
code: {
|
|
title: 'Migrate stored shape',
|
|
code: `const cart = App.storage.entry('cart', defaults, {
|
|
version: 3,
|
|
migrate: (previous, fromVersion) => {
|
|
if (fromVersion === 2) return migrateCartV2(previous);
|
|
return defaults;
|
|
},
|
|
validate: CartSchema
|
|
});`
|
|
}
|
|
},
|
|
{
|
|
title: 'Frontend Persistence',
|
|
body: [
|
|
'aapp uses Storage to persist Frontend preferences when frontend.persist is enabled. fend owns preference semantics; aapp only bridges them to storage entries.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/stor/test',
|
|
purpose: 'Entries, adapters and envelopes.',
|
|
notes: 'TTL, migrate, raw, validation, sync.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/storage-integration.test.ts',
|
|
purpose: 'App integration.',
|
|
notes: 'Frontend persistence and adapter overrides.'
|
|
},
|
|
{
|
|
name: '/test/stor',
|
|
purpose: 'Interactive storage lab.',
|
|
notes: 'Adapters, versioning, TTL and cross-tab behavior.'
|
|
}
|
|
]
|
|
},
|
|
|
|
http: {
|
|
section: 'Data',
|
|
title: 'Http',
|
|
alias: '$http',
|
|
summary:
|
|
'Typed HTTP client with tagged results, schema validation, retries, timeouts, hooks and SvelteKit event.fetch support.',
|
|
factories: ['createEngineHttp'],
|
|
dependsOn: ['$libs/http', '$libs/standard-schema', '$logger (optional)'],
|
|
layer: 'EngineHttp',
|
|
overview: [
|
|
'Http wraps fetch with a tagged result model. Callers receive ok/value for success and structured non-ok results for HTTP, network, timeout and validation failures.',
|
|
'It centralizes shared HTTP constants in libs/http so methods, headers and content types are not magic strings scattered through modules.',
|
|
'The engine supports retry, Retry-After, abort signals, request/response hooks, body validation and response validation through Standard Schema.'
|
|
],
|
|
dynamics: [
|
|
'Every call builds a request from root defaults plus per-call options, runs before hooks, serializes query/body, starts timeout control, executes fetch, parses the response, validates it when a schema is supplied and returns a tagged result instead of throwing for normal HTTP failures.',
|
|
'Retry is policy-driven. The engine can retry safe/idempotent calls, respect Retry-After and stop on total timeout. Callers still inspect the final tagged result.',
|
|
'with(options) creates a scoped child client. This is the preferred way to bind SvelteKit event.fetch, request-specific headers or tenant-specific baseUrl without mutating the root client.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'try/catch around every non-2xx',
|
|
purpose:
|
|
'Http returns tagged results for expected HTTP failures, so catch blocks hide useful status/body metadata.',
|
|
notes: 'Check response.ok and switch on response.type.'
|
|
},
|
|
{
|
|
name: 'using global fetch in SvelteKit server code',
|
|
purpose: 'Cookies and internal routing can be lost.',
|
|
notes: 'Create a scoped client with Http.with({ fetch: event.fetch }).'
|
|
},
|
|
{
|
|
name: 'retrying unsafe mutations blindly',
|
|
purpose: 'POST/PATCH side effects can be duplicated.',
|
|
notes:
|
|
'Configure retry explicitly and only for idempotent operations or idempotency-key protected calls.'
|
|
},
|
|
{
|
|
name: 'parsing bodies outside Http',
|
|
purpose: 'Validation and error normalization become inconsistent.',
|
|
notes: 'Pass schema/bodySchema and consume value from the tagged result.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'GET with schema',
|
|
code: `const response = await App.http.get('/api/projects', {
|
|
schema: ProjectsSchema,
|
|
query: { page: 1 }
|
|
});
|
|
|
|
if (response.ok) {
|
|
console.log(response.value);
|
|
} else {
|
|
App.Logger.warn('http', 'project request failed', { context: response });
|
|
}`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineHttp(options)',
|
|
purpose: 'Creates the HTTP engine.',
|
|
notes: 'No Active wrapper because request state belongs to callers.'
|
|
},
|
|
{
|
|
name: 'createEngineHttpAuthClient(Http)',
|
|
purpose: 'Auth adapter.',
|
|
notes: 'Adapts EngineHttp to ActiveAuth route calls.'
|
|
},
|
|
{
|
|
name: 'App.http',
|
|
purpose: 'App-wired engine.',
|
|
notes: 'Injects App.Logger and configured fetch/baseUrl.'
|
|
}
|
|
],
|
|
api: artifactApis.http,
|
|
sections: [
|
|
{
|
|
title: 'Creation and request scoping',
|
|
body: [
|
|
'App.http is the normal browser/client client. On the server, create a scoped child with event.fetch so SvelteKit cookies, internal routes and platform behavior are preserved.',
|
|
'Do not mutate a global Http instance with request-specific headers. Use with() to create a child client for one request, tenant or backend integration.'
|
|
],
|
|
code: {
|
|
title: 'SvelteKit scoped client',
|
|
code: `export const load = async (event) => {
|
|
const Http = App.http.with({
|
|
fetch: event.fetch,
|
|
headers: {
|
|
'x-request-id': event.locals.requestId
|
|
}
|
|
});
|
|
|
|
const projects = await Http.get('/api/projects', { schema: ProjectsSchema });
|
|
return { projects };
|
|
};`
|
|
}
|
|
},
|
|
{
|
|
title: 'Result Model',
|
|
table: [
|
|
{ name: 'ok', purpose: 'Validated success.', notes: 'value contains parsed payload.' },
|
|
{
|
|
name: 'http_error',
|
|
purpose: 'Non-2xx response.',
|
|
notes: 'Status, headers and parsed body are preserved.'
|
|
},
|
|
{ name: 'network_error', purpose: 'Fetch threw.', notes: 'Original error is attached.' },
|
|
{
|
|
name: 'timeout',
|
|
purpose: 'Request exceeded timeout.',
|
|
notes: 'AbortController is used where available.'
|
|
},
|
|
{
|
|
name: 'validation_error',
|
|
purpose: 'Schema rejected body.',
|
|
notes: 'Issues are returned as data.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Hooks',
|
|
body: [
|
|
'Hooks make session refresh, auth headers, tracing and custom diagnostics composable without hard-coding those concerns into the HTTP engine.'
|
|
]
|
|
},
|
|
{
|
|
title: 'SvelteKit',
|
|
body: [
|
|
'Pass event.fetch on the server to preserve cookies, platform fetch behavior and internal routing. On the client, default fetch is used.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/http/test',
|
|
purpose: 'Engine behavior.',
|
|
notes: 'Retry, timeout, schemas, hooks and tagged errors.'
|
|
},
|
|
{
|
|
name: 'src/arts/sess/test/http-integration.test.ts',
|
|
purpose: '401 rescue.',
|
|
notes: 'Session refresh integration.'
|
|
},
|
|
{
|
|
name: '/test/http',
|
|
purpose: 'Interactive HTTP lab.',
|
|
notes: 'GET/POST, validation, retry and timeout.'
|
|
}
|
|
]
|
|
},
|
|
|
|
fmts: {
|
|
section: 'I18n & Format',
|
|
title: 'Format',
|
|
alias: '$format',
|
|
summary:
|
|
'Locale-driven formatting root for numbers, currency, units and dates with a shared auto/manual contract.',
|
|
factories: [
|
|
'createEngineFormat',
|
|
'createActiveFormat',
|
|
'createEngineNumbers',
|
|
'createEngineCurrency',
|
|
'createEngineUnits',
|
|
'createEngineDates'
|
|
],
|
|
dependsOn: ['$locale', '$logger (currency diagnostics)'],
|
|
layer: 'EngineFormat / ActiveFormat',
|
|
overview: [
|
|
'Format centralizes everything that depends on locale but is not text translation: numeric separators, currency, units and date/time conventions.',
|
|
'It deliberately does not depend on Lang. Both consume the same LocaleSource when composed through App, so changing App locale updates translations and formats from one source of truth.',
|
|
'Each submodule can run as an engine or active wrapper. Auto values derive from locale until the user sets an explicit override.'
|
|
],
|
|
dynamics: [
|
|
'ActiveFormat listens to a LocaleSource. When the locale changes, every submodule recomputes values that are still auto: numeric separators, currency, unit system, date order and hour cycle.',
|
|
'Manual values are sticky. If a user calls setCurrency(), setSystem(), setDateOrder() or similar setters, later locale changes do not overwrite that choice. clearX() returns that setting to auto mode.',
|
|
'Numbers, Currency, Units and Dates can run independently, but the aggregate Format root is the normal application surface because it keeps one locale and one auto/manual contract across every formatter.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'deriving currency from base language',
|
|
purpose: 'Locales like es-AR and es-ES do not share currency.',
|
|
notes: 'Use explicit locale mappings and avoid locale.split("-")[0] currency fallbacks.'
|
|
},
|
|
{
|
|
name: 'overwriting manual user choices on locale change',
|
|
purpose: 'A user-selected currency/unit/date preference unexpectedly changes.',
|
|
notes: 'Respect isAuto/clear/set semantics.'
|
|
},
|
|
{
|
|
name: 'using Lang for formatting',
|
|
purpose: 'Translations and Intl formatting have different responsibilities.',
|
|
notes: 'Use $lang for text, $format for numbers/currency/units/dates.'
|
|
},
|
|
{
|
|
name: 'assuming conversion rates exist',
|
|
purpose: 'Formatting money is not the same as converting money.',
|
|
notes: 'Configure rates before using convert()/convertAs().'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'App formats',
|
|
code: `App.lang.setLocale('es-AR');
|
|
|
|
App.format.numbers.format(1234.5);
|
|
App.format.currency.getCurrency(); // ARS
|
|
App.format.units.getSystem(); // metric
|
|
App.format.dates.getDateOrder();`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineFormat(options)',
|
|
purpose: 'Pure aggregate engine.',
|
|
notes: 'Groups numbers, currency, units and dates.'
|
|
},
|
|
{
|
|
name: 'createActiveFormat(options)',
|
|
purpose: 'Reactive aggregate wrapper.',
|
|
notes: 'Subscribes to LocaleSource.'
|
|
},
|
|
{
|
|
name: 'createEngineNumbers/Currency/Units/Dates',
|
|
purpose: 'Standalone sub-engines.',
|
|
notes: 'Use when only one formatting domain is needed.'
|
|
}
|
|
],
|
|
api: artifactApis.fmts,
|
|
sections: [
|
|
{
|
|
title: 'Creation and locale source',
|
|
body: [
|
|
'When Format is created through App, it receives a LocaleSource backed by App.lang. That is the intended wiring: one locale change updates translations, numbers, currency, units, dates and Frontend direction.',
|
|
'Create standalone sub-engines only when a non-UI service needs one formatting domain. UI code should prefer App.format so auto/manual state stays consistent.'
|
|
],
|
|
code: {
|
|
title: 'App-owned locale propagation',
|
|
code: `App.lang.setLocale('es-AR');
|
|
|
|
App.lang.t('common.ok');
|
|
App.format.currency.getCurrency(); // ARS
|
|
App.format.dates.getDateOrder();
|
|
App.frontend.getDir();`
|
|
}
|
|
},
|
|
{
|
|
title: 'Submodules',
|
|
table: [
|
|
{
|
|
name: 'numbers',
|
|
purpose: 'Number, percent, compact and parse helpers.',
|
|
notes: 'Backed by Intl.NumberFormat.'
|
|
},
|
|
{
|
|
name: 'currency',
|
|
purpose: 'Currency selection, formatting and conversion rates.',
|
|
notes: 'Currency can be auto from locale or fixed.'
|
|
},
|
|
{
|
|
name: 'units',
|
|
purpose: 'Metric/imperial defaults and conversions.',
|
|
notes: 'Defaults are locale-aware but overridable.'
|
|
},
|
|
{
|
|
name: 'dates',
|
|
purpose: 'Date order, hour cycle and date/time formatting.',
|
|
notes: 'Uses locale conventions with explicit overrides.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Auto / Manual',
|
|
body: [
|
|
'If a setting is auto, locale changes can update it. If the user sets a value explicitly, locale changes do not override it. clearX() returns to auto.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Custom Currency',
|
|
code: {
|
|
title: 'Fixed currency',
|
|
code: `const Format = createActiveFormat({
|
|
locale: 'es-ES',
|
|
currency: { currency: 'USD' }
|
|
});
|
|
|
|
Format.setLocale('fr-FR');
|
|
Format.currency.getCurrency(); // USD, explicit user choice`
|
|
}
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/fmts/test',
|
|
purpose: 'Formatting engines.',
|
|
notes: 'Numbers, currency, units, dates and auto-state.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/active-app.test.ts',
|
|
purpose: 'Locale propagation.',
|
|
notes: 'App locale updates Format.'
|
|
},
|
|
{
|
|
name: '/test/fmts',
|
|
purpose: 'Interactive formats lab.',
|
|
notes: 'Locale switching and defaults.'
|
|
}
|
|
]
|
|
},
|
|
|
|
fend: {
|
|
section: 'UI Layer',
|
|
title: 'Frontend',
|
|
alias: '$frontend',
|
|
summary:
|
|
'Application-level frontend preferences: dir, theme, mode, density, reduced motion and reduced sound, applied through adom.',
|
|
factories: ['createActiveFrontend'],
|
|
dependsOn: ['$adom', '$locale'],
|
|
layer: 'ActiveFrontend',
|
|
overview: [
|
|
'Frontend is not a component system. It owns global presentation preferences and writes stable attributes to the configured DOM target.',
|
|
'dir, mode and reducedMotion can be auto. theme, density and reducedSound are explicit preferences. The same auto/manual dynamic used by Format applies here.',
|
|
'When built through App, Frontend consumes App.lang as LocaleSource and App.dom as the DOM writer.'
|
|
],
|
|
dynamics: [
|
|
'Frontend reads locale and environment preferences, resolves auto-capable values, and writes the result to DOM attributes through Dom.apply(). Components then style against those attributes instead of each component recalculating theme, direction or density.',
|
|
'The auto/manual dynamic matches Format. While dir/mode/reducedMotion are auto, locale or media-query changes can update them. Once the user sets a value explicitly, later auto sources stop overriding it until clearX() is called.',
|
|
'Persistence is not owned by Frontend. Frontend emits preference changes; aapp can bridge selected keys to Storage when frontend.persist is configured.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'setting document attributes by hand',
|
|
purpose: 'It bypasses the central writer and can fight Frontend updates.',
|
|
notes: 'Use Frontend setters or Dom.apply() through the framework.'
|
|
},
|
|
{
|
|
name: 'expecting manual dir to follow locale',
|
|
purpose: 'Manual values intentionally survive locale changes.',
|
|
notes: 'Call clearDir() to return to locale-derived direction.'
|
|
},
|
|
{
|
|
name: 'using Frontend as a component library',
|
|
purpose: 'It only owns global presentation state.',
|
|
notes: 'Use UI components separately; use $frontend for app-level preferences.'
|
|
},
|
|
{
|
|
name: 'persisting every preference blindly',
|
|
purpose: 'Auto values can become frozen user values unintentionally.',
|
|
notes: 'Persist explicit keys intentionally and preserve auto/manual metadata.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Direction and theme',
|
|
code: `const Frontend = App.frontend;
|
|
|
|
App.lang.setLocale('ar');
|
|
Frontend.getDir(); // rtl while dir is auto
|
|
|
|
Frontend.setDir('ltr'); // manual override
|
|
App.lang.setLocale('ar-EG');
|
|
Frontend.getDir(); // ltr
|
|
|
|
Frontend.clearDir();
|
|
Frontend.getDir(); // rtl`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createActiveFrontend(options)',
|
|
purpose: 'Creates the reactive frontend preference root.',
|
|
notes: 'Can own its own Dom or receive an App Dom.'
|
|
},
|
|
{
|
|
name: 'App.frontend',
|
|
purpose: 'Always-present App root.',
|
|
notes: 'Wired to App.lang locale and App.dom.'
|
|
}
|
|
],
|
|
api: artifactApis.fend,
|
|
sections: [
|
|
{
|
|
title: 'Creation and app wiring',
|
|
body: [
|
|
'Frontend should normally be created by App. App injects Lang as the locale source, Dom as the writer and Storage when persistence is enabled.',
|
|
'Create ActiveFrontend directly only for tests or embedded widgets that intentionally own their own DOM target.'
|
|
],
|
|
code: {
|
|
title: 'App-wired frontend',
|
|
code: `const App = createActiveApp({
|
|
lang: { schema, defaultLocale: 'es' },
|
|
frontend: {
|
|
theme: 'base',
|
|
mode: 'auto',
|
|
dir: 'auto',
|
|
persist: { keys: ['theme', 'mode', 'density'] }
|
|
}
|
|
});
|
|
|
|
App.lang.setLocale('ar');
|
|
App.frontend.getDir(); // rtl while dir remains auto`
|
|
}
|
|
},
|
|
{
|
|
title: 'DOM Output',
|
|
code: {
|
|
lang: 'html',
|
|
title: 'Applied attributes',
|
|
code: `<html
|
|
dir="rtl"
|
|
data-theme="base"
|
|
data-mode="light"
|
|
data-reduced-motion="false"
|
|
data-reduced-sound="false"
|
|
data-density="normal"
|
|
></html>`
|
|
}
|
|
},
|
|
{
|
|
title: 'Persisting Preferences',
|
|
body: [
|
|
'aapp can persist Frontend preferences through Storage. fend decides how to read user intent; aapp only bridges preferences to storage entries.'
|
|
],
|
|
code: {
|
|
title: 'Persist preferences',
|
|
code: `const App = createActiveApp({
|
|
storage: { adapter: localAdapter, namespace: 'app' },
|
|
frontend: {
|
|
theme: 'base',
|
|
persist: { keys: ['theme', 'mode', 'density'] }
|
|
}
|
|
});`
|
|
}
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/fend/test',
|
|
purpose: 'ActiveFrontend behavior.',
|
|
notes: 'DOM attrs, auto/manual and OS preferences.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/storage-integration.test.ts',
|
|
purpose: 'Persistence bridge.',
|
|
notes: 'Storage seeding and write-back.'
|
|
},
|
|
{
|
|
name: '/test/fend',
|
|
purpose: 'Interactive frontend lab.',
|
|
notes: 'Theme, mode, dir and density.'
|
|
}
|
|
]
|
|
},
|
|
|
|
adom: {
|
|
section: 'UI Layer',
|
|
title: 'Dom',
|
|
alias: '$adom',
|
|
summary:
|
|
'Reactive DOM service for viewport, breakpoints, attribute writes, scroll lock and focus-oriented helpers.',
|
|
factories: ['createActiveDom'],
|
|
dependsOn: ['$libs/dom', '$reactive'],
|
|
layer: 'ActiveDom',
|
|
overview: [
|
|
'Dom is the runtime DOM layer. It keeps browser-specific behavior out of formatting, frontend preferences and feature modules.',
|
|
'It can resolve responsive values, track viewport, apply attributes declaratively and coordinate scroll lock. In SSR, browser tracking is inert.',
|
|
'Frontend uses Dom.apply() to update application-level attrs, so theme/dir changes are centralized.'
|
|
],
|
|
dynamics: [
|
|
'In the browser, ActiveDom installs viewport/media listeners and updates a reactive viewport snapshot. During SSR those listeners are inert so imports stay safe.',
|
|
'Responsive values are resolved from the current breakpoint. Consumers can pass a scalar or a breakpoint map and receive the best value for the active viewport.',
|
|
'DOM writes are centralized through apply(). Frontend uses this to update html/body attributes, while feature modules can use the same writer for controlled attribute/class/style updates.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'reading window directly in modules',
|
|
purpose: 'SSR breaks and tests become non-deterministic.',
|
|
notes: 'Use App.dom viewport/responsive helpers and keep browser access inside $adom.'
|
|
},
|
|
{
|
|
name: 'multiple scroll locks without ownership',
|
|
purpose: 'One modal can unlock scroll while another is still open.',
|
|
notes: 'Use the scroll lock API so locks are reference-counted/coordinated.'
|
|
},
|
|
{
|
|
name: 'duplicating breakpoint logic in components',
|
|
purpose: 'Responsive behavior drifts across the app.',
|
|
notes: 'Resolve responsive maps through Dom.resolve().'
|
|
},
|
|
{
|
|
name: 'manual attrs fighting Frontend',
|
|
purpose: 'Theme/dir/mode can be overwritten by the next preference update.',
|
|
notes: 'Let Frontend write app-level attrs through Dom.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Responsive value and attrs',
|
|
code: `const Dom = App.dom;
|
|
|
|
const size = Dom.resolve({ base: 'compact', md: 'comfortable' });
|
|
|
|
Dom.apply({
|
|
target: document.documentElement,
|
|
attrs: {
|
|
dir: App.frontend.getDir(),
|
|
'data-theme': App.frontend.getTheme()
|
|
}
|
|
});`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createActiveDom(options)',
|
|
purpose: 'Creates the active DOM service.',
|
|
notes: 'Used directly or through App.dom.'
|
|
},
|
|
{
|
|
name: 'App.dom',
|
|
purpose: 'Always-present App root.',
|
|
notes: 'Shared by Frontend and consumers.'
|
|
}
|
|
],
|
|
api: artifactApis.adom,
|
|
sections: [
|
|
{
|
|
title: 'Creation and target ownership',
|
|
body: [
|
|
'App.dom is the shared DOM service for the application. It should own viewport tracking, responsive resolution and global writes that other artifacts depend on.',
|
|
'When using createActiveDom() directly, decide the target boundary explicitly: document-level app shell, an embedded widget root, or a test DOM. Do not let unrelated feature modules write global attributes independently.'
|
|
],
|
|
code: {
|
|
title: 'Isolated DOM root',
|
|
code: `const Dom = createActiveDom({
|
|
target: document.documentElement,
|
|
breakpoints: {
|
|
base: 0,
|
|
sm: 480,
|
|
md: 768,
|
|
lg: 1024
|
|
}
|
|
});
|
|
|
|
const layout = Dom.resolve({ base: 'stack', md: 'split' });`
|
|
}
|
|
},
|
|
{
|
|
title: 'Features',
|
|
table: [
|
|
{
|
|
name: 'viewport',
|
|
purpose: 'Reactive viewport snapshot.',
|
|
notes: 'Browser only; inert in SSR.'
|
|
},
|
|
{
|
|
name: 'resolve()',
|
|
purpose: 'Resolve breakpoint maps.',
|
|
notes: 'Useful for responsive component logic.'
|
|
},
|
|
{
|
|
name: 'apply()',
|
|
purpose: 'Apply attrs/styles/classes declaratively.',
|
|
notes: 'Used by Frontend.'
|
|
},
|
|
{
|
|
name: 'scroll lock',
|
|
purpose: 'Coordinate body scroll locks.',
|
|
notes: 'Useful for modals/drawers.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'SSR',
|
|
body: [
|
|
'Dom is safe to import during SSR. Viewport tracking and browser APIs activate only when the runtime has document/window.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/adom/test',
|
|
purpose: 'DOM primitives.',
|
|
notes: 'Viewport, attrs, scroll and responsive resolution.'
|
|
},
|
|
{
|
|
name: 'src/arts/fend/test',
|
|
purpose: 'Frontend integration.',
|
|
notes: 'Frontend applies attrs through Dom.'
|
|
},
|
|
{
|
|
name: '/test/adom',
|
|
purpose: 'Interactive DOM lab.',
|
|
notes: 'Responsive, scroll lock and focus examples.'
|
|
}
|
|
]
|
|
},
|
|
|
|
sium: {
|
|
section: 'Validation',
|
|
title: 'Sium',
|
|
alias: '$sium',
|
|
summary:
|
|
'Validation engine with schemas, issues, metadata, Standard Schema interop and optional Lang/Logger injection.',
|
|
factories: ['createEngineSium'],
|
|
dependsOn: ['$lang (optional)', '$logger (optional)', '$libs/days', '$libs/color'],
|
|
layer: 'EngineSium',
|
|
overview: [
|
|
'Sium is page-scoped by design. Forms live in pages and features, so App exposes createSiumEngine() instead of keeping a global validator alive for every route.',
|
|
'The engine creates schemas, validates values, returns structured issues and carries metadata for UI generation. It supports Standard Schema interoperability.',
|
|
'When created from App, Sium receives App.lang and App.Logger. If Lang is not provided, its local resolver is only a fallback.'
|
|
],
|
|
dynamics: [
|
|
'Create a Sium engine close to the form or feature that needs it. Define schemas once, then call validate() for submitted values or Standard Schema consumers such as HTTP body validation.',
|
|
'Validation returns structured issues with paths. UI code should render issues by path instead of flattening everything into one string, otherwise nested forms become hard to explain.',
|
|
'Translations flow through the injected Lang engine when present. Sium should not copy Lang behavior; its local resolver exists only so validation still has fallback messages without i18n.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'creating one global validator for every page',
|
|
purpose: 'Forms are page/feature scoped and global validators load unnecessary schemas.',
|
|
notes: 'Use defineEngineSium({}) where the form lives.'
|
|
},
|
|
{
|
|
name: 'throwing on normal validation failure',
|
|
purpose: 'Invalid user input is data, not an exception path.',
|
|
notes: 'Return/inspect result.ok and render result.issues.'
|
|
},
|
|
{
|
|
name: 'hard-coded issue messages',
|
|
purpose: 'Messages bypass Lang and cannot localize consistently.',
|
|
notes: 'Use injected Lang and constants for message keys/fallbacks.'
|
|
},
|
|
{
|
|
name: 'discarding issue paths',
|
|
purpose: 'The UI cannot attach messages to fields.',
|
|
notes: 'Keep structured issues and group/render by path.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Page validator',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
lang: defineActiveLang({ schema, defaultLocale: 'es' }),
|
|
sium: defineEngineSium({})
|
|
}
|
|
});
|
|
|
|
const ProfileSchema = App.sium.object({
|
|
name: App.sium.pipe(App.sium.string(), App.sium.min(2)),
|
|
email: App.sium.pipe(App.sium.string(), App.sium.email())
|
|
});
|
|
|
|
const result = await App.sium.validate(ProfileSchema, formData);
|
|
|
|
if (!result.ok) {
|
|
console.log(result.issues);
|
|
}`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineSium(options)',
|
|
purpose: 'Creates a validation engine (raw factory).',
|
|
notes: 'Direct factory for tests or non-App contexts.'
|
|
},
|
|
{
|
|
name: 'defineEngineSium(options)',
|
|
purpose: 'Service factory for the App schema.',
|
|
notes: 'Registered as services.sium; the builder injects logger from the core and lang from services when both are declared.'
|
|
}
|
|
],
|
|
api: artifactApis.sium,
|
|
sections: [
|
|
{
|
|
title: 'Creation and schema ownership',
|
|
body: [
|
|
'Sium is declared as a service via defineEngineSium({}) in the createActiveApp({ services }) schema. The builder injects logger from the core and lang from services automatically when both are declared.',
|
|
'Schemas should be owned by the feature that validates the data. Share schemas only when multiple boundaries validate the exact same shape, for example a form and an HTTP endpoint.'
|
|
],
|
|
code: {
|
|
title: 'Feature-owned validator',
|
|
code: `// app.svelte.ts — schema declared once at the App level
|
|
const App = createActiveApp({
|
|
services: {
|
|
lang: defineActiveLang({ schema, defaultLocale: 'es' }),
|
|
sium: defineEngineSium({})
|
|
}
|
|
});
|
|
|
|
// any feature that needs validation reaches App.sium
|
|
export const ProfileSchema = App.sium.object({
|
|
name: App.sium.pipe(App.sium.string(), App.sium.min(2)),
|
|
email: App.sium.pipe(App.sium.string(), App.sium.email())
|
|
});
|
|
|
|
const result = await Sium.validate(ProfileSchema, formValue);`
|
|
}
|
|
},
|
|
{
|
|
title: 'Schema Model',
|
|
table: [
|
|
{
|
|
name: 'primitive schemas',
|
|
purpose: 'string, number, boolean, date and similar checks.',
|
|
notes: 'Composable through pipe.'
|
|
},
|
|
{
|
|
name: 'object/array schemas',
|
|
purpose: 'Structured validation.',
|
|
notes: 'Nested issues keep paths.'
|
|
},
|
|
{ name: 'meta()', purpose: 'Attach UI metadata.', notes: 'Useful for form generation.' },
|
|
{
|
|
name: 'Standard Schema',
|
|
purpose: 'Interop contract.',
|
|
notes: 'Can validate external consumers and HTTP bodies.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Translations',
|
|
body: [
|
|
'Issue messages go through the injected Lang engine when available. The local resolver exists as fallback, not as a parallel copy of Lang behavior.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/sium/test',
|
|
purpose: 'Core schemas and engine.',
|
|
notes: 'Validation, pipes, metadata and translations.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/create-sium-engine.test.ts',
|
|
purpose: 'App injection.',
|
|
notes: 'Lang and Logger are wired into Sium.'
|
|
},
|
|
{
|
|
name: '/test/sium',
|
|
purpose: 'Interactive validation lab.',
|
|
notes: 'Forms, translated issues and schemas.'
|
|
}
|
|
]
|
|
},
|
|
|
|
logr: {
|
|
section: 'Infrastructure',
|
|
title: 'Logger',
|
|
alias: '$logger',
|
|
summary:
|
|
'Structured logger with shared Logger contract, EngineLogger runtime, transports, filters, failure routing and diagnostics support.',
|
|
factories: ['createEngineLogger'],
|
|
dependsOn: ['$libs/logger'],
|
|
layer: 'EngineLogger / Logger contract',
|
|
overview: [
|
|
'The minimal Logger interface lives in libs/logger and is what all modules receive. EngineLogger lives in arts/logr and extends that contract with transports, history, child loggers, timers and lifecycle.',
|
|
'Diagnostics are a cataloged layer above Logger. They map internal framework events to normal logger calls without forcing every log to become an event.',
|
|
'Transports can be filtered per level, buffered, throttled on failure and adapted to Sentry, Datadog, Loki, Logtail or OpenTelemetry.'
|
|
],
|
|
dynamics: [
|
|
'Application code and modules receive the small Logger contract from $libs/logger: trace, debug, info, warn, error and fatal. They do not need to know whether the concrete logger is EngineLogger, Sentry, a test spy or a custom adapter.',
|
|
'EngineLogger is the runtime implementation. It normalizes entries, applies level enablement, routes to transports, handles transport failures and emits synthetic failure entries to the remaining transports without cascading into the failed one.',
|
|
'Diagnostics sit above Logger. A module can define a catalog of internal events and map each one to a normal logger call. Free-form info/debug logs still go directly through Logger.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'defining local Logger interfaces',
|
|
purpose: 'Every module drifts and integrations become incompatible.',
|
|
notes: 'Import Logger and LogFn from $libs/logger.'
|
|
},
|
|
{
|
|
name: 'hard-coded categories/messages',
|
|
purpose: 'Search, routing and audits become unreliable.',
|
|
notes: 'Keep categories, diagnostic names and standard messages in consts.ts.'
|
|
},
|
|
{
|
|
name: 'turning every log into a diagnostic event',
|
|
purpose: 'Normal info/debug logging becomes boilerplate.',
|
|
notes:
|
|
'Use diagnostics for cataloged framework events; use Logger directly for normal logs.'
|
|
},
|
|
{
|
|
name: 'letting a transport log its own failure',
|
|
purpose: 'Failure cascades can loop indefinitely.',
|
|
notes: 'Use denied/deniedFor routing and transport failure throttling.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Create logger',
|
|
code: `import { createEngineLogger, consoleTransport, LogLevel } from '$logger';
|
|
import { sentryTransport } from '$logger/adapters/sentry';
|
|
|
|
const Logger = createEngineLogger({
|
|
level: LogLevel.INFO,
|
|
transports: [
|
|
consoleTransport(),
|
|
sentryTransport(Sentry, { level: LogLevel.ERROR })
|
|
],
|
|
globalContext: { appVersion: '1.0.0' }
|
|
});
|
|
|
|
Logger.info('checkout', 'payment completed', {
|
|
context: { orderId },
|
|
traceId
|
|
});`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineLogger(options)',
|
|
purpose: 'Creates the full logger runtime.',
|
|
notes: 'Use directly or through App.Logger.'
|
|
},
|
|
{
|
|
name: '$libs/logger.createCatalogDiagnostics(options)',
|
|
purpose: 'Maps typed diagnostic events to logger calls.',
|
|
notes: 'Shared helper, not an EngineLogger factory.'
|
|
},
|
|
{
|
|
name: '$libs/logger.createLoggerDiagnostics(options)',
|
|
purpose: 'Lower-level diagnostic resolver.',
|
|
notes: 'Shared helper, not an EngineLogger factory.'
|
|
}
|
|
],
|
|
api: artifactApis.logr,
|
|
sections: [
|
|
{
|
|
title: 'Creation and injection',
|
|
body: [
|
|
'Create one EngineLogger at the application root and inject its Logger contract into other artifacts. Modules should depend on $libs/logger.Logger, not on EngineLogger internals.',
|
|
'Use child/context helpers for module scopes if needed, but keep category names and standard messages in module consts.ts.'
|
|
],
|
|
code: {
|
|
title: 'Shared logger contract',
|
|
code: `import type { Logger } from '$libs/logger';
|
|
|
|
export function createFeature(options: { logger?: Logger }) {
|
|
const logger = options.logger ?? App.Logger;
|
|
logger.info('feature.started', { context: { source: 'profile' } });
|
|
}`
|
|
}
|
|
},
|
|
{
|
|
title: 'Levels',
|
|
body: [
|
|
'The framework uses an explicit per-level enablement map for routing, not module-specific severity systems. EngineLogger still performs final filtering and transport dispatch.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Failure Routing',
|
|
body: [
|
|
'If a transport fails, EngineLogger emits a synthetic failure entry to the remaining transports and marks deniedFor so the failing sink does not receive its own failure. Throttling prevents cascades.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Diagnostics',
|
|
code: {
|
|
title: 'Diagnostic catalog from $libs/logger',
|
|
code: `import { createCatalogDiagnostics, LogLevel } from '$libs/logger';
|
|
|
|
const diagnostics = createCatalogDiagnostics({
|
|
logger,
|
|
defaultCategory: 'conn',
|
|
catalog: {
|
|
reconnect_exhausted: {
|
|
level: LogLevel.WARN,
|
|
message: 'reconnect attempts exhausted'
|
|
}
|
|
}
|
|
});
|
|
|
|
diagnostics.emit({
|
|
artifact: 'conn',
|
|
type: 'reconnect_exhausted',
|
|
meta: { attempts: 5 }
|
|
});`
|
|
}
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/logr/test',
|
|
purpose: 'Engine logger.',
|
|
notes: 'Levels, transports, failures, buffers and adapters.'
|
|
},
|
|
{
|
|
name: 'src/libs/logger/test',
|
|
purpose: 'Shared diagnostics.',
|
|
notes: 'Catalog routing and error normalization.'
|
|
},
|
|
{
|
|
name: '/test/logr',
|
|
purpose: 'Interactive logger lab.',
|
|
notes: 'Levels, transports and web vitals.'
|
|
}
|
|
]
|
|
},
|
|
|
|
timer: {
|
|
section: 'Infrastructure',
|
|
title: 'Timers',
|
|
alias: '$timer',
|
|
summary:
|
|
'Deterministic timer scheduler with injectable clock, one-shots, intervals, cancellation, snapshots and backoff helpers.',
|
|
factories: ['createEngineTimers', 'createActiveTimers'],
|
|
dependsOn: ['$libs/timer', '$logger (optional)'],
|
|
layer: 'EngineTimers / ActiveTimers',
|
|
overview: [
|
|
'Timers centralizes scheduling so modules do not scatter setTimeout and setInterval logic. Reconnect, heartbeat, auto-refresh and cache-like workflows can all share a deterministic scheduler.',
|
|
'The engine supports keyed timers, replacement, cancellation by scope, intervals, snapshots and injected clocks for deterministic tests.',
|
|
'ActiveTimers exposes reactive snapshots for debug panels and test pages.'
|
|
],
|
|
dynamics: [
|
|
'Every timer has a stable key and optional scope. Scheduling with replace cancels the previous entry for that key before creating the next one, which prevents duplicate refresh/reconnect loops.',
|
|
'The engine uses the injected clock for timestamps and backoff calculations, so tests can advance time deterministically instead of waiting for real time.',
|
|
'ActiveTimers wraps the engine and exposes entries() as reactive snapshots. That is for observability and test pages; business logic should keep using schedule(), interval(), cancel() and cancelScope().'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'using setTimeout directly in modules',
|
|
purpose: 'Timers become impossible to cancel, inspect or fake in tests.',
|
|
notes: 'Use $timer for reconnect, refresh, heartbeat and delayed jobs.'
|
|
},
|
|
{
|
|
name: 'anonymous timer keys',
|
|
purpose: 'Duplicate timers accumulate and cause repeated work.',
|
|
notes: 'Use stable keys and replace when the latest task should win.'
|
|
},
|
|
{
|
|
name: 'not canceling by scope on dispose',
|
|
purpose: 'Page/module timers can run after their owner is gone.',
|
|
notes: 'Use scopes and cancelScope() or dispose the owner root.'
|
|
},
|
|
{
|
|
name: 'using Date.now() beside fake timers',
|
|
purpose: 'Tests advance scheduler time but metadata remains real-time.',
|
|
notes: 'Use the injected clock or TimerClock for related timestamps.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'Schedule work',
|
|
code: `const Timers = App.Timers;
|
|
|
|
Timers.schedule('profile:refresh', 5_000, async () => {
|
|
await refreshProfile();
|
|
});
|
|
|
|
Timers.interval('sync', 30_000, syncInBackground, {
|
|
replace: true,
|
|
awaitTask: false
|
|
});`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineTimers(options)',
|
|
purpose: 'Pure scheduler.',
|
|
notes: 'Use in services, tests and non-Svelte contexts.'
|
|
},
|
|
{
|
|
name: 'createActiveTimers(options)',
|
|
purpose: 'Reactive scheduler.',
|
|
notes: 'Exposes entries() snapshots through Svelte state.'
|
|
},
|
|
{
|
|
name: 'App.Timers',
|
|
purpose: 'Always-present App root.',
|
|
notes: 'Injected into Connections and available to consumers.'
|
|
}
|
|
],
|
|
api: artifactApis.timer,
|
|
sections: [
|
|
{
|
|
title: 'Creation and ownership',
|
|
body: [
|
|
'App.Timers is the shared scheduler for browser-side artifacts. Connections, auto-refresh and debug panels should use this root instead of creating their own timer islands.',
|
|
'Create EngineTimers directly for deterministic unit tests, workers or server utilities that need an injected clock and do not need Svelte state.'
|
|
],
|
|
code: {
|
|
title: 'Scoped scheduling',
|
|
code: `Timers.schedule('profile:refresh', 5_000, refreshProfile, {
|
|
scope: 'profile',
|
|
replace: true
|
|
});
|
|
|
|
Timers.interval('conn:heartbeat', 30_000, heartbeat, {
|
|
scope: 'conn',
|
|
awaitTask: false
|
|
});
|
|
|
|
Timers.cancelScope('profile');`
|
|
}
|
|
},
|
|
{
|
|
title: 'Timer Entries',
|
|
table: [
|
|
{
|
|
name: 'key',
|
|
purpose: 'Stable identity for timer operations.',
|
|
notes: 'Used for replace/cancel/snapshot.'
|
|
},
|
|
{ name: 'scope', purpose: 'Optional group.', notes: 'Cancel a whole feature at once.' },
|
|
{
|
|
name: 'run count',
|
|
purpose: 'How many times the task ran.',
|
|
notes: 'Useful for intervals and debug UI.'
|
|
},
|
|
{ name: 'nextRunAt', purpose: 'Scheduled timestamp.', notes: 'Uses injected clock.' }
|
|
]
|
|
},
|
|
{
|
|
title: 'Diagnostics',
|
|
body: [
|
|
'Timers logs task failures, listener failures and schedules in the past through the shared diagnostics layer when a logger is provided.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/timer/test',
|
|
purpose: 'Scheduler behavior.',
|
|
notes: 'One-shots, intervals, cancellation, backoff and fake clocks.'
|
|
},
|
|
{
|
|
name: 'src/arts/conn/test',
|
|
purpose: 'Consumer integration.',
|
|
notes: 'Reconnect, heartbeat and ACK timeouts.'
|
|
},
|
|
{
|
|
name: '/test/timr',
|
|
purpose: 'Interactive timer lab.',
|
|
notes: 'Snapshots, intervals and cancellation.'
|
|
}
|
|
]
|
|
},
|
|
|
|
conn: {
|
|
section: 'Infrastructure',
|
|
title: 'Connections',
|
|
alias: '$connection',
|
|
summary:
|
|
'Realtime connection registry with transports, reconnect, heartbeat, request/reply, channels, buffering and session-aware reauth.',
|
|
factories: ['createEngineConnections', 'createActiveConnections', 'createWebSocketTransport'],
|
|
dependsOn: ['$timer', '$logger (optional)', '$bus (optional)'],
|
|
layer: 'EngineConnections / ActiveConnections',
|
|
overview: [
|
|
'Connections is a registry of named realtime connections. A connection is not the engine: EngineConnections owns all connection state; each Connection owns transport, channels, heartbeat, reconnect and request/reply.',
|
|
'The transport contract is pluggable. Browser WebSocket is one transport; tests and demos can use mock transports without changing the connection runtime.',
|
|
'When composed through App via defineActiveConnections, the registry receives Timers and Logger from the core. Identity tracking is decoupled: the canonical wiring uses orca presets (applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke) to bridge App.session events to the registry. Per-connection ConnectionSessionSource remains available for standalone or manual setups.'
|
|
],
|
|
dynamics: [
|
|
'Create one registry, then create named connections inside it. The registry tracks all names and aggregate state; each connection owns its transport lifecycle, channel collection, send buffer, heartbeat timers and reconnect strategy.',
|
|
'Transports emit open/message/close/failure signals. The connection translates those into framework states, schedules heartbeat and reconnect through Timers, and routes logs through the shared Logger/diagnostic constants.',
|
|
'Channels are scoped streams over a connection. They can join, leave, send and request. After reconnect, auto-join channels rejoin so feature code does not rebuild subscriptions manually.',
|
|
'Identity reauth is wired through the orca presets in $active-app/presets — applyStandardOrca(App) registers the canonical reactions (cache clear, perm invalidate, connections reauth on identity change, connections close on revoke). Each connection still needs its own auth() callback to produce a credential frame. Per-connection session options remain available for standalone or manual setups outside the App composition.'
|
|
],
|
|
commonMistakes: [
|
|
{
|
|
name: 'treating Connection as the root',
|
|
purpose: 'You lose registry-level lifecycle, aggregate state and disposal.',
|
|
notes:
|
|
'Create EngineConnections/ActiveConnections first, then createConnection(name, options).'
|
|
},
|
|
{
|
|
name: 'using mock transports for real demos',
|
|
purpose: 'It hides network ordering, close and reconnect behavior.',
|
|
notes: 'Use createWebSocketTransport for demos intended to validate realtime behavior.'
|
|
},
|
|
{
|
|
name: 'logging through ad-hoc callbacks',
|
|
purpose: 'Reconnect/heartbeat/channel logs bypass the framework logger pipeline.',
|
|
notes: 'Use the injected Logger and module diagnostics/constants.'
|
|
},
|
|
{
|
|
name: 'forgetting app-event reauth behavior',
|
|
purpose: 'Connections can keep old identity after login/logout/refresh.',
|
|
notes: 'Wire the orca preset (applyStandardOrca or applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke) so identity changes flow through reauthenticateAll/closeAll. For standalone connections without orca, use the per-connection session option.'
|
|
}
|
|
],
|
|
quickStart: {
|
|
title: 'WebSocket connection',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
session: defineActiveSession({ ... }),
|
|
connections: defineActiveConnections({})
|
|
}
|
|
});
|
|
|
|
applyStandardOrca(App); // wires reauth-on-identity / close-on-revoke
|
|
|
|
const Updates = App.connections.createConnection('updates', {
|
|
transport: createWebSocketTransport({ url: '/ws' }),
|
|
auth: () => {
|
|
const credential = App.session?.current?.credential;
|
|
return credential ? { accessToken: credential.accessToken } : null;
|
|
},
|
|
heartbeat: { enabled: true },
|
|
reconnect: { enabled: true }
|
|
});
|
|
|
|
await Updates.connect();
|
|
await Updates.send('project.updated', { id: projectId });`
|
|
},
|
|
factoryRows: [
|
|
{
|
|
name: 'createEngineConnections(options)',
|
|
purpose: 'Connection registry engine.',
|
|
notes: 'Owns all named connections.'
|
|
},
|
|
{
|
|
name: 'createActiveConnections(options)',
|
|
purpose: 'Reactive registry wrapper (raw factory).',
|
|
notes: 'Direct factory for tests or non-App contexts.'
|
|
},
|
|
{
|
|
name: 'defineActiveConnections(options)',
|
|
purpose: 'Service factory for the App schema.',
|
|
notes: 'Registered as services.connections; the builder injects logger and timers from the core.'
|
|
},
|
|
{
|
|
name: 'createWebSocketTransport(options)',
|
|
purpose: 'Browser WebSocket transport.',
|
|
notes: 'Real network transport for production and demos.'
|
|
},
|
|
{
|
|
name: 'createMockTransport(options)',
|
|
purpose: 'Test transport.',
|
|
notes: 'No network; useful for deterministic tests.'
|
|
}
|
|
],
|
|
api: artifactApis.conn,
|
|
sections: [
|
|
{
|
|
title: 'Creation and registry ownership',
|
|
body: [
|
|
'Declare the connections registry via defineActiveConnections in the App service schema. The registry is the root; individual connections are children owned by that registry.',
|
|
'Use one registry for related realtime connections so aggregate state, disposal and identity reauth reactions stay coordinated.'
|
|
],
|
|
code: {
|
|
title: 'Registry first',
|
|
code: `const App = createActiveApp({
|
|
services: {
|
|
connections: defineActiveConnections({})
|
|
}
|
|
});
|
|
|
|
const Chat = App.connections.createConnection('chat', {
|
|
transport: createWebSocketTransport({ url: '/ws/chat' }),
|
|
reconnect: { enabled: true },
|
|
heartbeat: { enabled: true },
|
|
session: { enabled: true }
|
|
});
|
|
|
|
const room = Chat.channel('room:general', { autoJoin: true });
|
|
await Chat.connect();`
|
|
}
|
|
},
|
|
{
|
|
title: 'Connection Features',
|
|
table: [
|
|
{
|
|
name: 'heartbeat',
|
|
purpose: 'Ping/pong liveness.',
|
|
notes: 'Closes transport on timeout.'
|
|
},
|
|
{
|
|
name: 'reconnect',
|
|
purpose: 'Backoff-based reconnect.',
|
|
notes: 'Can reconnect on online/visible browser events.'
|
|
},
|
|
{
|
|
name: 'request()',
|
|
purpose: 'Request/reply over frames.',
|
|
notes: 'ACK registry handles timeouts and replies.'
|
|
},
|
|
{
|
|
name: 'channels',
|
|
purpose: 'Topic-like scoped streams.',
|
|
notes: 'Can auto-join and rejoin after reconnect.'
|
|
},
|
|
{
|
|
name: 'buffer',
|
|
purpose: 'Buffer/drop/fail sends while closed.',
|
|
notes: 'Configurable max messages and bytes.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Session-aware reauth',
|
|
body: [
|
|
'The canonical pattern wires reauth through orca: applyStandardOrca(App) (or the individual applyConnectionsReauthOnIdentityChange / applyConnectionsCloseOnRevoke) calls reauthenticateAll() / closeAll() on App.connections when session events fire. Each connection still needs its own auth() callback to produce the credential frame.',
|
|
'For standalone or per-connection setups outside the App composition, the registry / connection still accepts ConnectionSessionSource ({ onChange }) via the session option. The internal session-wiring listens to onChange and reacts directly. This is the manual / advanced path.'
|
|
]
|
|
},
|
|
{
|
|
title: 'Active State',
|
|
body: [
|
|
'ActiveConnections tracks names and states reactively: connectedNames, reconnectingNames, failedNames, allConnected and anyConnected are ready for UI panels.'
|
|
]
|
|
}
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'src/arts/conn/test',
|
|
purpose: 'Connection runtime.',
|
|
notes: 'States, channels, websocket transport and app-event reauth.'
|
|
},
|
|
{
|
|
name: 'src/arts/aapp/test/ecosystem.integration.test.ts',
|
|
purpose: 'App integration.',
|
|
notes: 'Connections reauth/disconnect from public app identity events when opted in.'
|
|
},
|
|
{
|
|
name: '/test/conn',
|
|
purpose: 'Interactive realtime demo.',
|
|
notes: 'WebSocket chat and connection/channel lifecycle.'
|
|
}
|
|
]
|
|
}
|
|
} satisfies Record<string, ArtifactDocModel>;
|