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.
dev 6555c3b23e
Consolidate framework diagnostics and refactors
5 months ago
..
integrations Build runtime infrastructure layer: aapp + 5 new artifacts 6 months ago
test Consolidate framework diagnostics and refactors 5 months ago
testing Add arts/timr, arts/sess, arts/http; sess actor extension; aapp factory 6 months ago
README.md Consolidate framework diagnostics and refactors 5 months ago
active-app.svelte.ts Consolidate framework diagnostics and refactors 5 months ago
consts.ts Consolidate framework diagnostics and refactors 5 months ago
errors.ts Integrate auth ecosystem layer 5 months ago
index.ts Integrate auth ecosystem layer 5 months ago
types.ts Consolidate framework diagnostics and refactors 5 months ago

README.md

aapp — ActiveApp

aapp is the application-level composition that wires the runtime artifacts under a single namespace, with a single locale source of truth and a single logger.

import { createActiveApp } from '$aapp';
import { translations } from './lang/schema';

const App = createActiveApp({
	lang: { schema: translations, defaultLocale: 'es', fallbackChain: ['en'] },
	logger: {
		level: LogLevel.INFO,
		globalContext: { appVersion: '1.0.0', env: 'prod' },
		transports: [consoleTransport()]
	},
	frontend: { theme: 'base', mode: 'auto', density: 'normal' }
});

App.setLocale('es-MX');
App.Lang.t('common.ok');
App.Formats.currency.format(12.5);
App.Frontend.setTheme('forest');
App.Logger.info('boot', 'app ready');

App.dispose();

What it composes

Member Always present Default when not configured
App.Logger yes engine default — level: WARN + consoleTransport(). Pass { level: LogLevel.NONE, transports: [] } for silence
App.Lang yes mono — t('a.b') returns 'a.b', t('a.b|Fallback') returns 'Fallback', and DEV warns once per unresolved path through Logger under lang.mono
App.Formats yes real, locale = DEFAULT_LOCALE ('en-US')
App.Frontend yes real with default theme/mode/density
App.Dom yes real with default breakpoints
App.Storage yes in-memory adapter (resets on reload). Configure storage: { adapter: localAdapter } for real persistence; storage diagnostics are wired through the shared Logger
App.Http yes engine default — globalThis.fetch, no baseUrl, idempotent-by-default retry, 10s per-attempt timeout. The shared Logger is wired automatically; configure http: { baseUrl, timeout, retry }
App.Timers yes ActiveTimers scheduler owned by App. Used by artifacts that need keyed runtime timers (sess auto-refresh, conn reconnect/heartbeat/ack) and disposed by App.dispose()
App.Cache yes ActiveCache backed by memory by default. Configure cache: { adapter, policies, scopeResolver } for persistence, custom policies or tenant/actor/permission-aware keys
App.Sess no created lazily through App.createActiveSession(...). Logger is injected automatically; storage, refresh/revoke handlers and HTTP hooks remain explicit so auth policy does not become hidden magic
App.Auth no created lazily through App.createActiveAuth(...). App injects Http and Timers; the server authority remains $svrs/auth
Connections no created lazily through App.createActiveConnections(...). App injects Logger, Timers and a structural session bridge; each connection opts into session behavior independently

Server-authoritative engines that have a browser reflector live under $svrs/*: use $svrs/auth for createEngineAuth() and auth HTTP handlers, $svrs/perm for createEnginePermissions() and authorization handlers, and $svrs/cach for createEngineCache() in services, repositories or server load code. aapp composes only the active/client side.

Sium is not a member of App. Validation is page-scoped — pages with forms construct their own engine via the one-line App.createSiumEngine() method that wires App.Lang and App.Logger automatically:

const sium = App.createSiumEngine();
const result = await sium.validate(LoginSchema, input);

Equivalent to createEngineSium({ lang: App.Lang, logger: App.Logger, locale: App.Lang.getLocale() }). Each call returns a fresh engine. The method takes no arguments — Lang, Logger and the active locale all flow from App, so there is nothing left to override at this layer.

The locale passed is a snapshot of App.Lang.getLocale() at the moment of construction. It is the fallback used when the caller invokes sium.resolveIssue(issue) without an explicit locale; the snapshot does not react to subsequent App.setLocale(...) calls. Pages that need locale-reactive issue messages either pass App.Lang.getLocale() per call (sium.resolveIssue(issue, App.Lang.getLocale())) or rebuild the engine inside an $effect that depends on the locale. If a page needs a custom Sium engine (different logger category, different locale default, etc.) it constructs createEngineSium(...) directly from $sium.

The method lives on App rather than as a standalone helper because App is already in scope on every page via context — App.createSiumEngine() is the natural call site.

Connections is also lazy, but for the opposite reason: realtime is application-scoped infrastructure, while the connection map is app-specific and benefits from call-site generics:

const Connections = App.createActiveConnections<AppConnections>();

Each registry is disposed by App.dispose(). Individual connections decide whether they react to session refresh/expire events via their own session option.

What it solves

  • Single locale source. App.setLocale('es-MX') propagates to Lang, Formats and Frontend through a shared LocaleSource. No bridge code per call site.
  • Single logger. Built once and piped into Lang.setLogger so every artifact emits structured entries through the same transports (console, Sentry, Datadog, ...).
  • Uniform call sites. App.Lang.t(label) and App.Formats.* always work, whether or not the caller configured i18n or fmts. No null checks.
  • Single lifecycle. App.dispose() tears down the optional session, timers, persistence bridge, frontend, dom, formats, storage, lang and logger in a deterministic order.

Composition order

  1. Logger — createEngineLogger(options.logger). The engine applies its own defaults when options.logger is undefined.
  2. Lang — real createActiveLang(...) when options.lang.schema is provided; mono otherwise. Both wire Lang.setLogger to the shared Logger.
  3. Storage — built next so Frontend can read persisted preferences before construction. Storage diagnostics are wired to the shared Logger.
  4. Formats — built with a localeSource derived from Lang.
  5. Dom — built before Frontend.
  6. Frontend — receives Dom and the same localeSource. When frontend.persist is configured, persisted values seed the initial options and onPreferenceChange is wired to write back to Storage.
  7. Http — built with the shared Logger injected automatically so request/retry/error events land under category 'http'. In SvelteKit load, scope to the request via App.Http.with({ fetch: event.fetch }).
  8. Timers — built with the shared Logger. This is the App-owned scheduler used by long-lived runtime tasks; no module-global singleton.
  9. Cache — built with the shared Logger. It is always present with a memory adapter unless cache.adapter is configured. Scopes remain explicit through each query and can use cache.scopeResolver.
  10. Sess — created lazily via App.createActiveSession(...), not from createActiveApp(...) options. App injects Logger, but the consumer keeps auth policy explicit (storage, onRefresh, onRevoke, HTTP hooks).

dispose() runs in reverse order.

Common shapes

Full multilingual app

const App = createActiveApp({
	lang: { schema, defaultLocale: 'es', fallbackChain: ['en'] },
	logger: { level: LogLevel.INFO, transports: [consoleTransport()] },
	frontend: { theme: 'base', mode: 'auto' }
});

Monolingual app with fixed currency

const App = createActiveApp({
	formats: { currency: { currency: 'EUR' } },
	frontend: { theme: 'base' }
});

App.Formats.currency.format(99.5); // "99,50 €" with default locale

App.Lang is mono — components calling App.Lang.t('actions.save|Save') render 'Save' without ever loading a translation table.

Headless / API surface

const App = createActiveApp({
	logger: { level: LogLevel.WARN, transports: [httpTransport({ url })] }
});

Frontend and Dom are still constructed but their browser-only effects (viewport tracking, attribute writes) no-op in SSR.

Locale flow

Lang is the single source of truth. Formats and Frontend subscribe to it via a common LocaleSource ($locale). The locale value is BCP 47:

App.setLocale('es'); // bare base
App.setLocale('es-MX'); // exact regional variant
App.setLocale('pt-BR'); // works end-to-end

See $lang/README.md for the BCP 47 resolution rules in Lang.t() / Lang.ts().

When lang is not configured, App.setLocale still updates the mono lang's internal locale and notifies Formats/Frontend — locale switching keeps working.

SSR locale resolution + hydration

createActiveApp(...) does not read navigator.language. That is a deliberate decision: reading the navigator on the client while the server rendered with a different locale produces a hydration mismatch and a one-frame text flash. Locale is the app's responsibility — resolve it on the server, pass it as data to the client, and use it as defaultLocale when constructing App.

The canonical SvelteKit pattern:

// src/web/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

const SUPPORTED = ['es', 'en', 'es-MX', 'es-AR', 'en-GB', 'pt-BR'] as const;
const DEFAULT = 'es';

function pickLocale(accept: string | null, supported: readonly string[]): string {
	if (!accept) return DEFAULT;
	const ranked = accept
		.split(',')
		.map((entry) => {
			const [tag, q] = entry.trim().split(';q=');
			return { tag: tag.toLowerCase(), q: q ? Number(q) : 1 };
		})
		.sort((a, b) => b.q - a.q);
	for (const { tag } of ranked) {
		// Exact BCP 47 match first, then base.
		if (supported.includes(tag)) return tag;
		const base = tag.split('-')[0];
		if (supported.includes(base)) return base;
	}
	return DEFAULT;
}

export const load: LayoutServerLoad = ({ request, cookies }) => {
	const cookie = cookies.get('locale');
	if (cookie && SUPPORTED.includes(cookie)) return { locale: cookie };
	const locale = pickLocale(request.headers.get('accept-language'), SUPPORTED);
	return { locale };
};
<!-- src/web/routes/+layout.svelte -->
<script lang="ts">
	import { setContext, onDestroy } from 'svelte';
	import { createActiveApp } from '$aapp';
	import { translations } from '$lib/lang/schema';

	let { data, children } = $props();

	const App = createActiveApp({
		lang: { schema: translations, defaultLocale: data.locale, fallbackChain: ['en'] },
		logger: {
			/* ... */
		}
	});

	setContext('app', App);
	onDestroy(() => App.dispose());
</script>

{@render children()}

Server and client agree on the locale on first render — no mismatch, no flash.

To let the user change locale at runtime, persist the choice to a cookie so the next request re-renders with the same value:

async function changeLocale(locale: SupportedLocale): Promise<void> {
	App.setLocale(locale);
	document.cookie = `locale=${locale}; path=/; max-age=31536000; SameSite=Lax`;
}

If you genuinely want to honor navigator.language on first visit, do it once in the server load when no cookie exists and no Accept-Language is set — never on the client.

Storage

App.Storage is always present. Without configuration it uses an in-memory adapter — values exist for the lifetime of the App and never persist. For real persistence, pass an adapter:

import { createActiveApp, localAdapter } from '$aapp';

const App = createActiveApp({
	storage: { adapter: localAdapter, namespace: 'my-app' }
});

const cart = App.Storage.entry('cart', { items: [] as string[] });
cart.update((p) => ({ ...p, items: [...p.items, 'sku-42'] }));

Per-entry overrides let you mix backends — cookies for SSR-readable values, localStorage for the rest:

import { createActiveApp, localAdapter, cookieAdapter } from '$aapp';

const App = createActiveApp({
	storage: { adapter: localAdapter, namespace: 'my-app' }
});

const locale = App.Storage.entry('locale', 'es', {
	adapter: cookieAdapter({ path: '/', maxAge: 31_536_000 }),
	namespace: false,
	raw: true
});

Storage diagnostics are wired automatically through StorageDiagnostics and the shared App.Logger; failures include { adapter, key, fullKey, op, error } in the diagnostic context. See $stor/README.md for the full API (adapters, envelope, versioning, validation).

Reactive keys

When the storage key tracks a runed variable (current user, active workspace, route param), use App.Storage.dynamicEntry():

<script lang="ts">
	let userId = $state(1);

	const profile = App.Storage.dynamicEntry(
		() => `user-${userId}:profile`,
		() => ({ name: '', cart: [] as string[] })
	);
	// userId = 2 → profile rebinds to 'user-2:profile' (previous entry
	// disposed, onChange listeners migrate automatically).
</script>

Must run inside a Svelte component or $effect.root scope.

Cross-tab sync without polling

Wrap any adapter with withBroadcast (re-exported from $aapp) for instant cross-tab synchronization through BroadcastChannel:

import { createActiveApp, localAdapter, cookieAdapter, withBroadcast } from '$aapp';

const App = createActiveApp({
	storage: {
		adapter: withBroadcast(localAdapter, { channel: 'my-app' }),
		namespace: 'my-app'
	}
});

// Cookies + broadcast = changes propagate across tabs the moment they
// happen (browsers do not emit a native event for cookie mutations).
const session = withBroadcast(cookieAdapter({ path: '/', maxAge: 3600 }), {
	channel: 'my-app:session'
});

Persisting Frontend preferences

Theme, mode, density, dir, reducedMotion, reducedSound can be persisted with a single flag:

const App = createActiveApp({
	storage: { adapter: localAdapter, namespace: 'my-app' },
	frontend: { theme: 'base', persist: true }
});

App.Frontend.setTheme('forest'); // → written to localStorage
// next reload → Frontend reads 'forest' from storage during construction

Selective + per-key overrides:

import { createActiveApp, localAdapter, cookieAdapter } from '$aapp';

const App = createActiveApp({
	storage: { adapter: localAdapter, namespace: 'my-app' },
	frontend: {
		theme: 'base',
		persist: {
			keys: ['theme', 'density', 'mode'],
			overrides: {
				// theme to a cookie so the server can render the right palette
				theme: { adapter: cookieAdapter({ path: '/' }), namespace: false, raw: true }
			}
		}
	}
});

When persist is set but no persistent adapter is configured (the default in-memory adapter is in use), values still flow through Storage — they just do not survive reload. No warning is emitted; the absence of persistence is visible in the storage adapter the caller chose.

State primitive

aapp does not include a stores system. Svelte 5 + runes already provide the primitive: a .svelte.ts module with $state is your store, scoped to import graph rather than to a global registry.

// src/lib/stores/cart.svelte.ts
let items = $state<CartItem[]>([]);

export const cart = {
	get items() {
		return items;
	},
	add(item: CartItem) {
		items.push(item);
	},
	clear() {
		items = [];
	}
};

Pages and components import cart directly. The "infrastructure" artifacts (Logger, Lang, Formats, Frontend, Dom) live in App because they are cross-cutting and need uniform configuration. Domain state (current user, cart, session, feature flags) is application-specific — putting it under App.Stores would couple the framework to a bucket of unrelated nouns.

For a Pinia/Zustand-style central registry, build it in user space — it does not belong in aapp.

Testing

Use createTestApp(options) instead of createActiveApp(options) in unit tests. Same shape, plus:

  • silent logger by default (no console pollution)
  • captureLogs: true attaches a sink and exposes entries as App.entries

createTestApp lives at the $aapp/testing subpath so it does not ship with production bundles that import the main $aapp barrel:

import { createTestApp } from '$aapp/testing';

const App = createTestApp({ captureLogs: true, lang: { schema } });

App.Logger.warn('auth', 'token expiring');
expect(App.entries).toHaveLength(1);
expect(App.entries[0].category).toBe('auth');

App.dispose();

If the caller passes their own logger.transports, the capture transport is appended — both sinks receive every entry.

API

interface ActiveAppOptions<S extends LangNode> {
	logger?: LoggerOptions;
	lang?: { schema: S; defaultLocale?: SupportedLocale; fallbackChain?: SupportedLocale[] };
	formats?: Omit<ActiveFormatsOptions, 'locale' | 'localeSource'>;
	frontend?: Omit<ActiveFrontendOptions, 'locale' | 'localeSource' | 'dom'> & {
		persist?: FrontendPersist;
	};
	dom?: ActiveDomProps;
	storage?: ActiveAppStorageOptions;
	http?: Omit<EngineHttpOptions, 'logger'>;
	timers?: Omit<EngineTimersOptions, 'logger'>;
	cache?: Omit<ActiveCacheOptions, 'logger'>;
	connections?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session'>;
}

interface ActiveApp<S extends LangNode = LangNode> {
	readonly Logger: EngineLogger;
	readonly Lang: ActiveLang<S>;
	readonly Formats: ActiveFormats;
	readonly Frontend: ActiveFrontend;
	readonly Dom: ActiveDom;
	readonly Storage: ActiveStorage;
	readonly Http: EngineHttp;
	readonly Timers: ActiveTimers;
	readonly Cache: ActiveCache;
	readonly Sess: ActiveSession<unknown, unknown, unknown> | undefined;

	getLocale(): SupportedLocale;
	setLocale(locale: SupportedLocale): void;
	onLocaleChange(fn: (locale: SupportedLocale) => void): () => void;

	createSiumEngine(): EngineSium;
	createActiveSession<TUser, TCredential = undefined, TData = undefined>(
		options?: Omit<EngineSessionOptions<TUser, TCredential, TData>, 'logger'>
	): ActiveSession<TUser, TCredential, TData>;
	createActiveConnections<TConnections extends ConnectionMap = ConnectionMap>(
		options?: Omit<ActiveConnectionsOptions, 'logger' | 'timers' | 'session'>
	): ActiveConnections<TConnections>;

	dispose(): void;
}

Why Sium is out

Sium is a validation library that reaches into per-page data — login forms, profile editors, signup wizards. Putting it in App would force every page (including those without forms) to load the entire schema/types/issues machinery just to use Lang or Formats. Keeping Sium page-scoped means:

  • Pages without validation pay nothing for it.
  • Each form can use a sium engine tuned to its own needs (custom logger category, validation context, etc.).
  • App stays focused on the runtime contract every page needs.

The standard pattern is one line at the top of the page module:

const sium = createEngineSium({ lang: App.Lang, logger: App.Logger });

Powered by TurnKey Linux.