API reference

Every export of aegis.js, generated from aegis.d.ts (the same declarations your editor uses). 258 names in 13 groups plus 6 deprecated aliases; the canonical dozen is all most apps need.

Reactive core#

AegisWarning class#

An engine warning as an exception (window.AEGIS_DEV = 'strict')

export class AegisWarning extends Error { code: string; what: string; why: string; fix: string }

batch function#

export function batch<T>(fn: () => T): T;

computed function#

export function computed<T>(fn: (prev: T) => T, nameOrOpts?: string | ComputedOptions<T>): Computed<T>;

createContext function#

export function createContext<T>(defaultValue?: T): Context<T>;

createScope function#

name — for error messages (component:div#app, list:row)

export function createScope(name?: string): Scope;

deferred function#

A deferred shadow of a signal (useDeferredValue): the input is bound to src, the heavy list — to deferred(src)

export function deferred<T>(src: Signal<T> | ReadonlySignal<T>, opts?: { lane?: 'transition' | 'idle' }): ReadonlySignal<T>;

dev const#

Dev-mode control: dev.enable() (localStorage + reload in production), dev.disable(), dev.resetWarnings()

export const dev: {
    readonly on: boolean;
    enable(): void;
    disable(): void;
    resetWarnings(): void;
    /** performance.measure / console.timeStamp on every flush in the “Aegis” track of the Performance panel */
    profile(on?: boolean): void;
    /** The reactive world of an island by DOM node: Aegis.dev.of($0) */
    of(el: Element): ScopeInspection | null;
    /** JSON snapshot of the components (or of one scope) — for a chat with an assistant */
    inspect(root?: Document | Element | Scope): ScopeInspection[] | ScopeInspection;
    /** Dependency graph as Mermaid */
    graph(root?: Scope): string;
    /** dev overlay: warnings pop up in the page corner; false — console only (or localStorage aegis:overlay=0) */
    overlay: boolean;
    /** EXPLAIN ANALYZE of the latest list() reconciliations: plan keyed | rebuild, n, kept, lis, moves, ms */
    plans(): Array<{ plan: 'keyed' | 'rebuild'; n: number; kept: number; lis: number; moves: number; ms: number }>;
    /** Runtime contracts of the graph and the scope tree: 'sampled' (default) | 'strict' (every flush; tests) | false */
    contracts: 'sampled' | 'strict' | false;
    /** Explanation of a warning code from ERRORS.md — prints to the console and returns the text */
    explain(code: string): Promise<string>;
    /** Snapshot of the resource cache (same as cache.stats().entries) — console.table(Aegis.dev.cache()) */
    cache(): CacheEntryStats[];
    /** In-page inspector panel (loads aegis-devtools.js next to the module); also ?aegis-devtools in the URL */
    panel(): Promise<{ host: HTMLElement; shadow: ShadowRoot; close(): void; highlight(el: Element | null): void }>;
};

effect function#

export function effect(fn: () => void | (() => void), nameOrOpts?: string | EffectOptions): () => void;

flush function#

Synchronously run the deferred micro/frame lanes and the effect queue

export function flush(): void;

flushSync function#

Synchronously run deferred show()/list() renders and the effect queue

export function flushSync(): void;

from function#

An external source as a signal: (EventTarget, event, map) | (producer(set) => unsubscribe, initial) | { subscribe }

export function from<T, E extends EventTarget = EventTarget>(target: E, event: string, map?: (target: E) => T): ReadonlySignal<T>;
export function from<T>(producer: (set: (v: T) => void) => (() => void) | void, initial?: T, opts?: { /** the producer starts with the first subscriber and stops with the last */ lazy?: boolean }): ReadonlySignal<T>;
export function from<T>(subscribable: { subscribe(fn: (v: T) => void): (() => void) | { unsubscribe(): void }; value?: T; peek?(): T }): ReadonlySignal<T>;

getOwner function#

The current owner scope (null outside a scope). For code after await: runWithOwner(getOwner(), () => …)

export function getOwner(): Scope | null;

history function#

Undo/redo for a signal, reactive() or store()

export function history<T>(source: Signal<T> | object, opts?: { limit?: number; debounce?: number }): {
    undo(): void; redo(): void; canUndo: ReadonlySignal<boolean>; canRedo: ReadonlySignal<boolean>;
    pause(): void; resume(): void; commit(): void; clear(): void;
    past: ReadonlySignal<unknown[]>; future: ReadonlySignal<unknown[]>;
};

inject function#

Take from the context: scope chain → DOM ancestors (between islands) → global. Call synchronously in setup

export function inject<T>(key: Context<T>): T;
export function inject<T>(key: Context<T> | string, fallback: T): T;
export function inject(key: string): unknown;

isReactive function#

export function isReactive(v: unknown): boolean;

isSignal function#

export function isSignal(v: unknown): v is Signal<unknown>;

lens function#

Writable derived: a write lives until the next change of the source

Two-way lens: reading — computed(get), writing — set(v) into the source (no local state).

bind:value=${lens(() => cents.value / 100, v => cents.value = Math.round(v * 100))}   |   lens(state.address, 'city')
export function lens<T>(get: () => T, set: (v: T) => void, name?: string): Signal<T>;
export function lens<O extends object, K extends keyof O>(obj: O, key: K, name?: string): Signal<O[K]>;

linked function#

export function linked<T>(source: () => T, name?: string): Signal<T>;
export function linked<S, T>(opts: { source: () => S; compute: (source: S, prev: { source: S; value: T } | undefined) => T }, name?: string): Signal<T>;

nextTick function#

export function nextTick(fn?: () => void): Promise<void>;

onDispose function#

export function onDispose(fn: () => void): () => void;

onError function#

Effect errors not swallowed by scope.onError / errorBoundary: fn(error, error.aegis). Without handlers — self.reportError(e). The writer of the signal does not get the exception (except with AEGIS_DEV = 'strict'). Returns unsubscribe; inside a scope it is removed automatically.

export function onError(fn: (error: unknown, info: { effect: string; scope: string; changed: Array<{ name: string; value: string }>; site?: string } | null) => void): () => void;

onWarn function#

Subscribe to warnings (dev mode): warnings-as-assertions in tests. Returns unsubscribe

export function onWarn(fn: (w: WarningInfo) => void): () => void;

persisted function#

A signal in localStorage/sessionStorage with cross-tab sync

export function persisted<T>(key: string, initial: T, opts?: {
    storage?: Storage | null;
    serialize?: (v: T) => string;
    deserialize?: (s: string) => T;
    sync?: boolean;
    debounce?: number;
}): Signal<T> & { clear(): void };

provide function#

Put a value into the context of the current scope (outside a scope — globally)

export function provide<T>(key: Context<T> | string, value: T): void;

reactive function#

Deeply reactive object: fields → signals, getters → computeds, methods → batched actions; arrays and plain objects are reactive deeply, Date/Map/File/DOM stay as they are. { shallow: true } = store()

export function reactive<T extends object>(obj: T, opts?: { shallow?: boolean }): T & ReactiveExtras<T>;

reset function#

Reset module singletons between tests (components, registry, resource cache, live region)

export function reset(opts?: { components?: boolean; cache?: boolean; registry?: boolean; dom?: boolean }): void;

root function#

Root scope for tests: const [api, dispose] = root(dispose => …)

export function root<T>(fn: (dispose: () => void) => T): [T, () => void];

runWithOwner function#

export function runWithOwner<T>(scope: Scope | null, fn: () => T): T;

selector function#

O(2) updates instead of N for the “selected row”: const isSelected = selector(selectedId)

export function selector<K, S = K>(source: Signal<S> | ReadonlySignal<S> | (() => S), equals?: (source: S, key: K) => boolean): (key: K) => boolean;

signal function#

export function signal<T>(initial: T, nameOrOpts?: string | SignalOptions<T>): Signal<T>;

signals function#

Named signals from object keys — for E-messages, trace() and dev.graph(); a getter → computed.

const { count, query } = signals({ count: 0, query: '' });
export function signals<T extends Record<string, unknown>>(obj: T, opts?: { prefix?: string }): { [K in keyof T]: Signal<T[K]> };

startTransition const#

Non-urgent update: writes inside go to the transition class (250 ms deadline) in slices after the current task; a repeated write before the drain collapses, the old UI stays visible. startTransition.pending — a signal

export const startTransition: (<T>(fn: () => T) => T) & { readonly pending: ReadonlySignal<boolean> };

stats function#

Engine counters: flushes, effectRuns, maxRounds, slow (top-20 by ms under dev.profile), scopes, effects, components, caches

export function stats(): { /** resident islands, page-outs and hibernations */ islands: { resident: number; evictions: number; hibernations: number } | null; flushes: number; effectRuns: number; maxRounds: number; /** rounds where the effect order had to be restored by sorting (subscription churn) */ reordered: number; slow: Array<{ name: string; ms: number }>; scopes: number; effects: number; components: number; resourceCache: number; cssCache: number; queued: number; prefetch: { fired: number; used: number; wasted: number; hoverDelay: number } | null; speculation: { inflight: number; queued: number; fired: number; skipped: number; aborted: number } | null };

trace function#

Debugging: print the stack of every write to a signal. trace(sig, false) — turn off

export function trace<T extends Signal<any>>(sig: T, on?: boolean): T;

transaction function#

Optimistic transaction (OCC): read(sig) records versions without subscribing; on commit the read-set is validated, on conflict — retry (retries) or onConflict → 'abort'; writes are applied in one batch

export function transaction<T>(fn: (tx: { read<V>(sig: Signal<V> | ReadonlySignal<V>): V; write<V>(sig: Signal<V>, value: V): void; attempt: number }) => Promise<T> | T, opts?: { retries?: number; onConflict?: (changed: Array<{ name: string; value: string }>) => 'abort' | void }): Promise<T>;

until function#

Await a signal: resolves on the first value for which the predicate is true; rejects with TimeoutError / on scope dispose

export function until<T>(source: Reactive<T>, predicate?: (v: T) => boolean, opts?: { timeout?: number }): Promise<T> & { toBe(v: T): Promise<T>; changed(): Promise<T> };

untrack function#

Run fn without subscribing to the signals it reads

export function untrack<T>(fn: () => T): T;

useScheduler function#

Replace the scheduler's clock and queues (tests, simulations); returns restore

export function useScheduler(impl: Partial<{ now(): number; micro(f: () => void): void; frame(f: (t?: number) => void): void; idle(f: (d: { timeRemaining(): number; didTimeout: boolean }) => void): void; yield(): Promise<void>; inputPending(): boolean; onRun: ((obs: { _name: string }, lane: string) => void) | null }>): () => void;

watch function#

Watch a signal/computed for changes.

source — what to track (signal, computed, or getter function) callback — called with (newValue, oldValue) on change opts.immediate — call immediately with current value (oldValue = undefined) opts.debounce — debounce the callback in ms returns dispose function

export function watch<T>(
    source: Signal<T> | ReadonlySignal<T> | (() => T),
    callback: (newVal: T, oldVal: T | undefined, onCleanup: (fn: () => void) => void) => void,
    opts?: { immediate?: boolean; debounce?: number; once?: boolean }
): WatchHandle;

Templates & DOM#

$ function#

export function $(selector: string, root?: Element | Document): Element | null;

$$ function#

export function $$(selector: string, root?: Element | Document): Element[];

adopt function#

Bind an html`` template to DOM already rendered by the server without re-rendering (0 mutations):

adopt(el)`<span class="value">${count}</span><button @click=${inc}>+</button>`

The element structure must match; text values are the only child of their element.

export function adopt(root: Element, opts?: { trust?: boolean }): (strings: TemplateStringsArray, ...values: unknown[]) => Element;

attach function#

Behaviour on an element after mount: init + cleanup + reactivity in one function. fn runs as an effect: re-runs when the signals it read change, cleanup before a re-run and on dispose.

export function attach<E extends Element = Element>(fn: (el: E) => void | (() => void), opts?: AttachOptions): Attachment<E>;
export function attach<E extends Element>(el: E, fn: (el: E) => void | (() => void), opts?: AttachOptions): () => void;

attr function#

export function attr(el: Element, name: string, value: Reactive<Displayable>): () => void;

bind function#

Two-way bind an input's value to a signal. Handles checkbox/radio (checked), number, select. returns cleanup function

export function bind(el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, sig: Signal<any> | FunctionBinding): () => void;

boost function#

MPA navigation without a reload: fetch the page → morph root → View Transitions. Islands outside root survive the navigation. Opt-out: data-no-boost; router() links are left alone (routers: [r]).

export function boost(opts?: {
    root?: string | Element;
    mode?: SwapMode;
    transition?: boolean;
    prefetch?: 'hover' | 'visible' | 'tap' | boolean | { on?: 'hover' | 'visible' | 'tap'; delay?: number | 'auto'; velocity?: number; rootMargin?: string };
    /** predictor: learns from aegis:load (paths are normalised: numbers → :id), warms the HTML of the top-K likely pages in idle time */
    predict?: boolean | { predictor?: Predictor; topK?: number; minP?: number };
    scroll?: 'restore' | 'preserve';
    head?: 'title' | 'title+styles' | false;
    routers?: Router[];
    focus?: 'auto' | string | Element | false;
    announce?: boolean | ((to: { path: string }, from: null) => string | null);
}): { pending: ReadonlySignal<boolean>; visit(url: string): Promise<boolean>; /** warm a page's HTML within the network budget */ prefetch(url: string, p?: number): Promise<void>; dispose(): void };

clone function#

A live copy of a fragment from html``: bindings are created anew in the current scope. A fragment with finished nodes (list()/show() anchors) cannot be reproduced — cloneNode + dev warning E013.

export function clone(frag: DocumentFragment): DocumentFragment;

cls function#

Toggle a single CSS class reactively. el — target element name — class name fn — boolean signal, function, or static value

export function cls(el: Element, name: string, value: Reactive<unknown>): () => void;

cls(el, { active: sig, done: () => … }) / cls(el, ['btn', size]) — diffs only its own classes

export function cls(el: Element, classes: ClassValue): () => void;

cssVars function#

CSS custom properties from signals: cssVars(el, { x, progress }) → --x, --progress

export function cssVars(el: HTMLElement | SVGElement, vars: Record<string, Reactive<string | number | null>>): () => void;

delegate function#

export function delegate(root: Element, event: string, selector: string, handler: (this: Element, e: Event, target: Element) => void): () => void;

destroy function#

export function destroy(el: Element): void;

destroyAll function#

export function destroyAll(root?: Document | Element): void;

html function#

export function html(strings: TemplateStringsArray, ...values: HtmlValue[]): DocumentFragment;

hydrate function#

Bring server HTML to life: [data-aegis] (including root itself). Idempotent. Seeds the cache from <script type="application/json" data-aegis-cache> first. Events: aegis:hydrate (cancelable), aegis:hydrated (detail: { name, api }), aegis:destroy. The first chunk mounts synchronously; handles.ready — a Promise for the completion of the eager part.

export function hydrate(root?: Document | Element, opts?: HydrateOptions): HydrateHandle[] & { ready: Promise<void> };
export namespace hydrate {
    /** Auto-hydrate(document) after register(). Default true */
    let auto: boolean;
}

jsonScript function#

JSON from <script type="application/json"> (Django json_script) — cached per element

export function jsonScript<T = unknown>(target: string | Element, root?: Document | Element): T | undefined;

list function#

export function list<T>(
    items: Signal<T[]> | ReadonlySignal<T[]> | (() => T[]) | T[],
    renderFn: (item: Signal<T>, index: RowIndex) => Node | Node[] | string | number,
    opts: ListOptions<T> & { item: 'signal' }
): Comment;
export function list<T>(
    items: Signal<T[]> | ReadonlySignal<T[]> | (() => T[]) | T[],
    renderFn: (item: T, index: RowIndex) => Node | Node[] | string | number,
    opts: ListOptions<T>
): Comment;
export function list<T>(
    items: Signal<T[]> | ReadonlySignal<T[]> | (() => T[]) | T[],
    /** index — a signal: current after sorting/removal. A row re-renders if the object under its key was replaced */
    /** renderFn may return one node, a fragment of several (<tr>+<tr>, <dt>+<dd>) or an array — no wrappers */
    renderFn: (item: T, index: RowIndex) => Node | Node[] | string | number,
    key?: string | ((item: T) => string | number),
    opts?: ListOptions
): Comment;

on function#

export function on<K extends keyof HTMLElementEventMap>(el: Element, event: K, handler: (e: HTMLElementEventMap[K]) => void, opts?: AddEventListenerOptions): () => void;
export function on(el: Element, event: string, handler: (e: Event) => void, opts?: AddEventListenerOptions): () => void;

portal function#

Render content into a different DOM location. Auto-removes on scope dispose.

target — DOM element to append into (e.g. document.body) contentFn — content factory or DocumentFragment/Element returns container div + dispose function

export function portal(
    target: Element,
    contentFn: (() => DocumentFragment | Element) | DocumentFragment | Element,
    /** popover: 'auto' | 'manual' | true — the container in the top layer (light-dismiss, :popover-open); anchor + placement — CSS anchor positioning */
    opts?: { popover?: 'auto' | 'manual' | boolean; anchor?: Element; placement?: string }
): { container: HTMLDivElement; dispose(): void; /** whether the popover portal is open (toggle) */ open: ReadonlySignal<boolean> };

prevent function#

Svelte 5-style event wrappers: @submit=${prevent(save)} (in html`` also @submit.prevent, .stop, .self, .once, .passive, .capture, .outside, .window, .document, .enter/.esc/…, .ctrl/.meta/.shift/.alt, .debounce.N, .throttle.N)

export function prevent<E extends Event>(fn: (e: E) => void): (e: E) => void;

ref function#

export function ref<T extends Element = Element>(): Ref<T>;

render function#

export function render(target: Element, content: DocumentFragment | Element): void;

sanitizeConfig const#

DOMPurify config for user-HTML zones: no data-aegis*, on*, script/template/iframe

export const sanitizeConfig: Readonly<{ FORBID_ATTR: string[]; FORBID_TAGS: string[] }>;

self function#

export function self<E extends Event>(fn: (e: E) => void): (e: E) => void;

show function#

show(open, () => html, { transition: 'fade' }) — options as the third argument when there is no else branch

export function show(
    condition: Signal<boolean> | (() => boolean) | boolean,
    trueBranch: (() => DocumentFragment | Element) | DocumentFragment | Element,
    opts: ShowOptions,
): Comment;
export function show(
    condition: Signal<boolean> | (() => boolean) | boolean,
    trueBranch: (() => DocumentFragment | Element) | DocumentFragment | Element,
    falseBranch?: (() => DocumentFragment | Element) | DocumentFragment | Element | null,
    opts?: ShowOptions
): Comment;

stop function#

export function stop<E extends Event>(fn: (e: E) => void): (e: E) => void;

style function#

Bind a single CSS property reactively. el — target element prop — CSS property name (camelCase) fn — string signal, function, or static value

--custom-property goes through setProperty; a number — only for unitless properties

export function style(el: HTMLElement | SVGElement, prop: string, value: Reactive<string | number | null>): () => void;

styleMap function#

Bind multiple CSS properties via a map { prop: signal/fn/string }. returns cleanup function that disposes all style effects

export function styleMap(el: HTMLElement | SVGElement, map: Record<string, Reactive<string | number | null>>): () => void;

swap function#

Insert server HTML carefully: dispose islands in the replaced subtree, insert, hydrate new ones, remove data-cloak, restore focus and caret. mode 'morph' — a targeted patch (id-aware), nodes are not recreated. If the response is a whole page, the target's selector (or select) is taken from it.

export function swap(target: Element, html: string | Response | Document | DocumentFragment | Element, opts?: {
    mode?: SwapMode;
    /** true — Sanitizer API (Element.setHTML) or a minimal clean-up of script/iframe/on*-attributes/javascript: with S012 in dev; an object — SanitizerConfig */
    sanitize?: boolean | object;
    select?: string;
    transition?: boolean | { name?: string; cls?: string };
    hydrate?: boolean;
}): Promise<{ inserted: Node[] }>;

text function#

export function text(el: Element, value: Reactive<Displayable>): () => void;

tpl function#

A server <template> with [data-slot] slots as the markup source for list()/show(); no HTML parsing (CSP)

export function tpl(target: string | HTMLTemplateElement, root?: Document | Element): (slots?: Record<string, SlotSpec>) => DocumentFragment;

trusted function#

The author vouches for the value: bypasses the html`` sink checks (a URL with a non-standard scheme, ready HTML for srcdoc/.innerHTML)

export function trusted<T = string>(v: T): { readonly __aegisTrusted: T };

uncloak function#

export function uncloak(el?: Element): void;

when function#

export function when<T>(res: { data: { value: T | null; peek(): T | null }; loading?: { value: boolean }; error?: { value: unknown; peek(): unknown }; refresh?: () => unknown }, branches: {
    loading?: () => Node | Node[] | string;
    error?: (error: any, retry: () => void) => Node | Node[] | string;
    /** data(value, signal) — the second argument for reactive list()/text() inside the branch */
    data?: (data: T, signal: ReadonlySignal<T | null>) => Node | Node[] | string;
    empty?: () => Node | Node[] | string;
}, opts?: { /** aria-busy on the container while loading/validating (default true) */ busy?: boolean; /** announcements: error assertive (default: the error text), data — only after a real wait, loading */ announce?: false | { loading?: A11yMsg<void>; error?: A11yMsg<unknown>; data?: A11yMsg<T> } }): Comment;

Components#

element function#

A custom element from the same component: attributes → ctx.props (reactive)

export function element<P extends Record<string, PropType | { type: PropType; default?: unknown; reflect?: boolean }> = {}>(tag: `${string}-${string}`, component: Component<ElementProps<P>>, opts?: { props?: P; shadow?: boolean; styles?: CSSStyleSheet | string; formAssociated?: boolean }): void;

errorBoundary function#

Error boundary: wraps component() in try/catch. fallback — receives (error, el) on failure

export function errorBoundary<R = void>(
    el: Element,
    setup: (ctx: ComponentContext) => R,
    fallback?: (error: Error, el: Element) => void
): ComponentResult<R> | undefined;

expose function#

Make Aegis global (window.Aegis) for inline scripts without import. Not done automatically: a module-level global breaks tree-shaking.

export function expose(target?: object): typeof Aegis;

island function#

export function island<T extends Record<string, PropType> = {}>(name: string, component: Component<IslandProps<T>>, opts?: { types?: T }): void;

mount function#

Shorthand: mount component by CSS selector or element. selector — CSS selector string or Element setup — component setup function returns component API or undefined if element not found

export function mount<R = void>(
    selector: string | Element,
    setup: (ctx: ComponentContext<HTMLElement>) => R
): ComponentResult<R> | undefined;

register function#

Register a component by name; { load } — the island code is loaded with import() on mount. data-* are passed as strings (JSON literals are parsed); types declares coercion: { count: Number, on: Boolean, tags: JSON }. A <script type="application/json"> block inside the island (or data-aegis-props="#id") → data.props and fields of data.

Low-level positional form. Prefer island(name, Component, { types }); keep register() for { load } (lazy island modules).

export function register<D = Record<string, unknown>>(
    name: string,
    setup: IslandSetup<D> | { load: () => Promise<IslandSetup<D> | { default: IslandSetup<D> }> },
    opts?: { types?: Record<string, PropType> }
): void;

scaffold function#

A register() scaffold for an island from its server markup (dev)

export function scaffold(el: HTMLElement): string;

Data & cache#

api const#

Sugar over request()

export const api: {
    get<T = unknown>(url: string, opts?: RequestOptions): Promise<T>;
    post<T = unknown>(url: string, body?: unknown, opts?: RequestOptions): Promise<T>;
    put<T = unknown>(url: string, body?: unknown, opts?: RequestOptions): Promise<T>;
    patch<T = unknown>(url: string, body?: unknown, opts?: RequestOptions): Promise<T>;
    delete<T = unknown>(url: string, opts?: RequestOptions): Promise<T>;
};

cache const#

Public access to the resource cache — keys are normalised as in resource()

export const cache: {
    get<T = unknown>(key: string | CacheKeyPart[] | Record<string, unknown>): T | undefined;
    has(key: string | CacheKeyPart[] | Record<string, unknown>): boolean;
    /** Logout: wipe the persist store (all principals, or only foreign ones with others: true) and the offline queue */
    purge(opts?: { persist?: boolean; queue?: boolean; others?: boolean }): Promise<void>;
    /** = seed(key, data, { age, staleTime }) */
    set(key: string | CacheKeyPart[] | Record<string, unknown>, data: unknown, opts?: { age?: number; staleTime?: number }): unknown;
    /** remove entries by pattern (no argument — all); returns the number removed */
    remove(pattern?: InvalidatePattern): number;
    keys(prefix?: string | CacheKeyPart[]): string[];
    entry(key: string | CacheKeyPart[]): { key: string; data: Signal<any>; error: Signal<unknown>; inflight: Signal<boolean>; refCount: number; age(): number | null } | null;
    /** subscribe to an entry's data (keeps it alive); returns unsubscribe */
    subscribe<T = unknown>(key: string | CacheKeyPart[], fn: (data: T | null) => void): () => void;
    /** cache decision events: fetch / fresh / joined / set — for assertions in tests */
    on(fn: (key: string, ev: CacheEvent) => void): () => void;
    /** explicit garbage collection by the clock (tests with fakeClock, low memory); returns the number removed */
    gc(now?: number): number;
    /** update an entity in every cache entry (configure({ identify })); returns the number of entries changed */
    patchEntity(entityKey: string, fn: (node: any) => any): number;
    /** three-way merge of objects */
    merge3<T = any>(base: T, local: T, server: T): { value: T; conflicts: string[] };
    /** the persist-store record (IndexedDB): { data, at, v, n } | null */
    persisted(key: string | CacheKeyPart[]): Promise<{ data: unknown; at: number; v: number; n: number; etag?: string | null } | null>;
    /** await hydration of the entry from disk: true — the data came from persist */
    hydrated(key: string | CacheKeyPart[]): Promise<boolean>;
    /** entries, bytes (with maxBytes), evictions and limits */
    size(): { entries: number; bytes: number; evictions: number; maxEntries: number; maxBytes: number };
    /** why the entry is fresh/stale, who requested it, what to recommend */
    explain(key: string | CacheKeyPart[]): CacheExplain;
    stats(): CacheStats;
};

configure function#

Configure the HTTP layer for your backend — one line per project: configure({ csrf: 'django' })

export function configure(opts: AegisConfig): AegisConfig;

defaults const#

The single mocking point for the whole engine: defaults.fetcher = mock — resource/cache/offline/guardedFetch go through it

export const defaults: { /** null → the engine's request(); mocking: defaults.fetcher = mock */ fetcher: Fetcher | null; motion: 'auto' | 'reduce' | 'none' };

guardedFetch function#

export function guardedFetch(scope?: Scope): GuardedFetch;

HttpError class#

HTTP response error: status, Response and the parsed body (e.data.errors from Laravel/Django)

export class HttpError extends Error {
    name: 'HttpError';
    status: number;
    response: Response;
    data: unknown;
    /** the circuit breaker is open — the request was not sent; retryAt — when to try again (ms epoch) */
    circuit?: boolean;
    retryAt?: number;
    /** the retry was rejected by the retry budget */
    budget?: boolean;
    /** the server asked to wait longer than maxWait (ms) */
    retryAfter?: number;
    constructor(status: number, response: Response, data: unknown);
}

infiniteResource function#

Cursor pagination: pages accumulate, loadMore is deduplicated

export function infiniteResource<P = unknown, T = unknown>(urlFor: (cursor: unknown) => string | null, opts?: {
    getNext?: (page: P) => unknown;
    select?: (page: P) => T[];
    fetcher?: Fetcher;
    immediate?: boolean;
    retry?: boolean | number;
}): ResourceResult<T[]> & { pages: ReadonlySignal<P[]>; hasMore: ReadonlySignal<boolean>; loadMore(): Promise<void>; reset(): Promise<void> };

invalidate function#

Reset freshness and refetch live entries; the Promise waits for the refetches. cancel: false — wait for the in-flight request and refetch after it

export function invalidate(pattern: InvalidatePattern, opts?: { cancel?: boolean; refetch?: 'active' | 'all' | 'none' }): Promise<void>;

leader function#

Leader among tabs (Web Locks): true in exactly one tab, the lock moves when it closes; .release() — hand it over; without Web Locks — a fallback

export function leader(name?: string, opts?: { fallback?: boolean }): ReadonlySignal<boolean> & { release(): void };

mutation function#

A mutation with pending, double-submit guard, optimistic + rollback, invalidate.

const addTodo = mutation((text, { signal }) => api.post('/api/todos', { text }, { signal }), { resources: [todos], optimistic: … });
export function mutation<A extends unknown[], R>(fn: (...args: [...A, { signal: AbortSignal; /** isolation: 'validate' — read a signal and remember its version (OCC) */ read<V>(sig: Signal<V> | ReadonlySignal<V>): V; /** ETag of the affected resource for If-Match */ etag: string | null }]) => Promise<R> | R, opts?: MutationOptions<A>): Mutation<A, R>;

poll function#

Polling with auto-stop on dispose. Sleeps in a background tab (background: true — do not sleep)

export function poll(fn: () => Promise<void> | void, ms: number, opts?: { background?: boolean }): () => void;

predictor function#

export function predictor(opts?: { decay?: number; alpha?: number; kappa?: number; storage?: Storage | { getItem(k: string): string | null; setItem(k: string, v: string): void; removeItem(k: string): void } | null; key?: string; max?: number }): Predictor;

prefetch function#

Warm the cache without subscribers (hover, approaching the viewport); the data is available to resource(url, { cache: true })

export function prefetch(url: string, opts?: { /** kind of warm-up for hit statistics (prefetchOn sets it itself) */ kind?: string; /** probability of use — warm up only when p·rtt exceeds the network cost */ p?: number; /** bypass the network budget (configure({ speculation })) */ force?: boolean; key?: string; staleTime?: number; cacheTime?: number; fetcher?: Fetcher; transform?: (d: unknown) => unknown }): Promise<void>;

prefetchOn function#

Warm-up on intent: hover (default) | tap | visible; with saveData/2g — tap only. Returns dispose

export function prefetchOn(el: Element, urlOrFn: string | ((target: Element) => string | null | undefined), opts?: { on?: 'hover' | 'tap' | 'visible'; /** 'auto' — horizon by scroll speed (400 / 1200 / 3000 px) */ rootMargin?: string; /** hover delay: ms | 'auto' */ delay?: number | 'auto'; /** cursor speed threshold (px/s): below it the user is “aiming”, warm up immediately */ velocity?: number; /** probability of use (or a function of the target) for the usefulness threshold */ p?: number | ((target: Element) => number); staleTime?: number; kind?: string }): () => void;

request function#

One HTTP request: baseURL, query, JSON body, CSRF for unsafe same-origin, timeout, HttpError with the parsed body.

export function request<T = unknown>(url: string, init?: RequestOptions & { raw?: false }): Promise<T>;
export function request(url: string, init: RequestOptions & { raw: true }): Promise<Response>;

resource function#

Reactive data loading — one primitive:

resource('/api/users')                                 GET
resource(() => `/api/users?page=${page.value}`)       refetch when signals change
resource({ params: () => uid.value, loader: async ({ params, signal }) => … })
resource(url, { cache: true, staleTime: 30000 })       SWR cache (shared by key)
resource(url, { offline: true })                       IndexedDB + mutation queue
export function resource<T = unknown>(source: string | (() => string | null | false), opts: ResourceOptions<T> & { initial: T } & { cache?: boolean | CacheOptions; offline?: false }): ResourceResult<T> & { data: Signal<T> };
export function resource<T = unknown>(source: string | (() => string | null | false), opts: ResourceOptions<T> & { offline: true | OfflineOptions }): OfflineResourceResult<T>;
export function resource<T = unknown>(source: string | (() => string | null | false), opts?: ResourceOptions<T> & { cache?: boolean | CacheOptions; offline?: false }): ResourceResult<T>;
export function resource<T = unknown, P = unknown>(source: LoaderSource<P, T>, opts?: ResourceOptions<T>): ResourceResult<T>;

seed function#

Put data into the cachedResource() cache by hand (a mutation response, a server payload). age — the age of the data in ms

Put data into the cache: age — age in ms, staleTime — how long it counts as fresh (default 0: SWR revalidation on mount)

export function seed(key: string | CacheKeyPart[], data: unknown, opts?: { age?: number; staleTime?: number }): unknown;

seedFrom function#

Seed the cache from server HTML:

<script type="application/json" data-aegis-cache="/api/users" data-aegis-age="120">[…]</script>

Idempotent; hydrate() calls it itself. Returns the number of seeded entries.

export function seedFrom(root?: Document | Element): number;

settled function#

Await the completion of all resource()/mutation()/guardedFetch requests — instead of sleep(50) in tests

export function settled(): Promise<void>;

speculate function#

Speculation Rules for server-first pages: one <script type="speculationrules"> with document rules (prefetch / prerender by eagerness); without support — <link rel="prefetch"> on intent. Returns dispose

export function speculate(opts?: { prefetch?: boolean; prerender?: boolean | 'conservative' | 'moderate' | 'eager'; eagerness?: 'conservative' | 'moderate' | 'eager'; select?: string; exclude?: string; urls?: string[] }): () => void;

sse function#

Server-Sent Events over EventSource: the "aegis-signals" event writes JSON into signals; closed on scope dispose

export function sse(url: string, opts?: {
    signals?: Record<string, Signal<any>>;
    events?: Record<string, (data: any, e: MessageEvent) => void>;
    onMessage?: (data: any, e: MessageEvent) => void;
    withCredentials?: boolean;
}): { status: ReadonlySignal<'connecting' | 'open' | 'closed'>; close(): void; source: EventSource };

streamResource function#

Stream a response into a growing signal; done — the completion signal

export function streamResource<T = unknown>(source: string | (() => string | null), opts?: StreamOptions<T>): ResourceResult<any> & { done: ReadonlySignal<boolean> };

useClock function#

Replace the cache clock (staleTime, cacheTime, explain): useClock(() => t); returns restore. See fakeClock() in aegis/test

export function useClock(now?: (() => number) | null): () => void;

withRetry function#

Retry with exponential backoff and full jitter; honours Retry-After; AbortError is not retried

export function withRetry<T>(fn: (attempt: number) => Promise<T>, opts?: RetryOptions): Promise<T>;

Forms#

draft function#

A form draft in sessionStorage: changed fields without password/file, restored on creation (dirty stays), cleared on success

export function draft(f: FormCore | WireFormResult, key: string, opts?: { storage?: Storage | { getItem(k: string): string | null; setItem(k: string, v: string): void; removeItem(k: string): void }; debounce?: number; ttl?: number; exclude?: (key: string, value: unknown) => boolean; restore?: (values: Record<string, unknown>, apply: () => void) => void }): { restored: boolean; clear(): void; stop(): void; key: string };

email const#

export const email: ValidationRule<string>;

emailRule const#

export const emailRule: ValidationRule<string>;

fieldArray function#

An array of fields on top of form()/wireForm(): items[i][sub] with stable row keys and name renumbering.

const items = fieldArray(f, 'items', { row: { qty: 1, sku: '' } });   // form rules: { 'items[].qty': [min(1)] }
list(items.rows, row => html`<input bind:field=${row.field('qty')}>`, r => r.key)
export function fieldArray<R extends Record<string, any>>(f: FormCore | WireFormResult, path: string, opts?: { row?: R; rules?: { [K in keyof R]?: RuleLike[] }; name?: (i: number, sub?: string) => string; initial?: Partial<R>[] }): FieldArray<R>;

form function#

form(defaults, { rules, schema }) — a form from default values: form({ name: '', age: 0 })

export function form<S extends Record<string, any>>(schema: { [K in keyof S]: FieldDef<S[K]> }): FormResult<{ [K in keyof S]: FieldDef<S[K]> }> & FormSubmitState & FormCore;
export function form<T extends Record<string, any>>(defaults: T & { [K in keyof T]: T[K] extends { value: any } ? never : T[K] }, opts?: FormOptions): FormResult<{ [K in keyof T]: { value: T[K] } }> & FormSubmitState & FormCore & {
    submit(handler: (values: T, ctx: { signal: AbortSignal; submitter: HTMLElement | null; event: SubmitEvent | null }) => unknown | Promise<unknown>, opts?: { submitter?: HTMLElement; event?: SubmitEvent }): Promise<unknown>;
    submit(url: string, opts?: { headers?: HeadersInit; transform?: (v: T) => unknown; fetchOpts?: RequestOptions; submitter?: HTMLElement; onRedirect?: 'assign' | 'router' | 'none' | ((r: Response) => void) }): Promise<{ ok: boolean; status?: number; data?: any; error?: unknown; aborted?: boolean; redirected?: boolean }>;
};

matches function#

export function matches(otherKey: string, msg?: string): ValidationRule<any>;

max function#

export function max(n: number, msg?: string): ValidationRule<number | string | null>;

maxFiles function#

export function maxFiles(n: number, msg?: string): ValidationRule<any>;

maxLen function#

export function maxLen(n: number): ValidationRule<string>;

maxSize function#

export function maxSize(size: number | string, msg?: string): ValidationRule<any>;

mime function#

export function mime(types: string | string[], msg?: string): ValidationRule<any>;

min function#

export function min(n: number, msg?: string): ValidationRule<number | string | null>;

minLen function#

export function minLen(n: number): ValidationRule<string>;

pattern function#

export function pattern(re: RegExp, msg?: string): ValidationRule<string>;

required const#

export const required: ValidationRule<any>;

setValidationMessages function#

Create a translation function from a flat dictionary. Returns a callable t(key, params?) with .load(), .merge(), .dict.

dict — flat key→translation map (single language)

Messages of the built-in validation rules: a dictionary by code or (code, params) => string; null — the default by <html lang>

Rule messages: a dictionary (a string or plural forms { one, few, many, other }), a function (code, params) => string | null, or t from i18n() — keys <prefix><code>, the locale follows t.locale

export function setValidationMessages(dict: Record<string, string | Record<string, string>> | ((code: string, params?: Record<string, unknown>) => string | null) | TranslationFunction<any> | null, prefix?: string): void;

wireForm function#

Bring a server <form> to life: signals, validation (native constraints through the Constraint Validation API with browser messages, rules, Standard Schema, async rules), a11y, wizard, server submit.

const f = wireForm(el, { schema: zodSchema, rules: { login: [unique] }, submit: true });   // submit: true — FormData to the form's action
on(el, 'submit', f.submit(values => api.post('/save', values)));
export function wireForm(formEl: HTMLFormElement, opts?: {
    schema?: Record<string, RuleLike[]> | StandardSchemaV1<any, any>;
    rules?: Record<string, RuleLike[]>;
    /** when to show errors; the truth is always in issues */
    mode?: 'blur-then-live' | 'live' | 'submit';
    native?: boolean;
    asyncDebounce?: number;
    /** 'browser' — input.validationMessage in the browser's language; 'page' — ValidityState codes → a dictionary in the page's language */
    messages?: 'browser' | 'page';
    submit?: boolean | ((values: Record<string, unknown>, ctx: { signal: AbortSignal; submitter: HTMLElement | null; event: SubmitEvent | undefined }) => unknown);
    onRedirect?: 'assign' | 'router' | 'none' | ((r: Response) => void);
    announceSuccess?: boolean;
    /** Escape during submission aborts it */
    escapeAborts?: boolean;
    /** watch fields appear/disappear (MutationObserver) */
    observe?: boolean;
    /** value coercion: { birthday: Date, qty: Number, tags: Array, agree: Boolean } */
    types?: Record<string, typeof Date | typeof Number | typeof Array | typeof Boolean | typeof String>;
    a11y?: FormA11y;
    focusOnError?: 'summary' | 'field' | false;
    /** render the error summary right away (true — a container before the form) */
    summary?: boolean | string | Element;
    /** guard() right away */
    guard?: boolean | GuardOptions;
    /** draft in sessionStorage: a key or true (the form's action + id); also data-aegis-draft on <form> */
    draft?: string | boolean;
    /** an HTML response from the server (Rails 422 render, Django form_invalid): 'morph' (default) — the form is morphed in place and errors are read from the markup; 'replace'; false — leave it */
    html?: 'morph' | 'replace' | false;
    /** <button name="intent" value="add"> or data-intent — a local action without a request; without JS the same button goes to the server */
    intents?: Record<string, (f: WireFormResult, e: SubmitEvent) => void>;
}): WireFormResult;

wizard function#

A wizard on top of form()/wireForm(): steps are groups of keys ('address.*') or [data-step] in the form; focus on the new step, announce “Step n of N”, [data-step-nav] children get aria-current="step"; history: true — ?step=i through the Navigation API; persist: 'key' — the step in sessionStorage

export function wizard(f: FormCore | WireFormResult, opts?: { steps?: string[][]; persist?: string; history?: boolean; focus?: boolean }): Wizard;

Motion#

animate function#

export function animate(target: Element, mutate: () => void, opts?: { name?: string; cls?: string }): Promise<void>;

flip function#

export function flip(nodes: ArrayLike<Element>, opts?: { stiffness?: number; damping?: number }): () => void;

reducedMotion const#

prefers-reduced-motion as a signal; a write overrides the system setting

export const reducedMotion: Signal<boolean>;

spring function#

export function spring(el: Element, props: Record<string, [from: string | number, to: string | number]>, opts?: SpringOptions): Animation;

springSignal function#

A spring as a value: write target, read current; velocity is kept when the target changes

export function springSignal<T extends number | number[] | Record<string, number>>(initial: T, opts?: { stiffness?: number; damping?: number; mass?: number; precision?: number }): { target: Signal<T>; current: ReadonlySignal<T>; set(v: T, opts?: { hard?: boolean }): void };

transition function#

CSS enter/leave transition driven by a reactive condition. Applies CSS classes on enter/leave and auto-hides via el.hidden.

el — target element condition — signal, function, or boolean controlling visibility opts — CSS class names and optional duration returns effect disposer

Animated show/hide through the CSS contract ${name}-enter-* / ${name}-leave-* (interrupt-safe); legacy { enter, enterActive, leave, leaveActive, duration } is supported

export function transition(
    el: Element,
    condition: Signal<boolean> | (() => boolean) | boolean,
    opts?: {
        enter?: string;
        enterActive?: string;
        leave?: string;
        leaveActive?: string;
        duration?: number;
    }
): () => void;

tween function#

A tween as a value

export function tween<T extends number | number[] | Record<string, number>>(initial: T, opts?: { duration?: number; easing?: (t: number) => number }): { target: Signal<T>; current: ReadonlySignal<T>; set(v: T, opts?: { hard?: boolean }): void };

Routing & platform#

anchor function#

export function anchor(floating: Element, reference: Element, opts?: {
    placement?: 'top' | 'bottom' | 'left' | 'right';
    offset?: number;
    autoUpdate?: boolean;
}): () => void;

command function#

export function command(root: Element, commands?: Record<string, (trigger: Element, target: Element | null) => void>): {
    dispose(): void;
    add(name: string, handler: (trigger: Element, target: Element | null) => void): void;
};

router function#

A router on top of the Navigation API (fallback: popstate + <a> interception). Nested routes with layout, async handler/loader (native indicator, scroll after data, race cancellation), guard/redirect as data, search parameters as signals, lazy routes through import(). Does not intercept: hash links, forms, download, data-aegis-reload, unmatched paths (they go to the server).

export function router<R extends Record<string, unknown>>(routes: { [K in keyof R]: K extends string ? RouteHandler<any, K> | (RouteDef & { handler?: RouteHandler<any, K>; component?: Component<RouteComponentProps<K, any>> }) : never }, opts?: RouterOptions): Router;

transitioning const#

The router's View Transition is in progress

export const transitioning: ReadonlySignal<boolean>;

Accessibility#

announce function#

Screen-reader announcement: two permanent regions (polite → role=status, assertive → role=alert), a lossless queue, dedup of identical text within 500 ms, auto-clear after 7 s. Returns clear(). announce.init() creates the regions in advance.

export function announce(message: string, politeness?: 'polite' | 'assertive'): () => void;
export function announce(message: string, opts: { politeness?: 'polite' | 'assertive'; clearAfter?: number | false; dedupe?: number; native?: boolean }): () => void;

reactive form: a signal/function → the region (in the current scope); the initial value is not announced

export function announce<T>(source: ReadonlySignal<T> | (() => T), opts?: { politeness?: 'polite' | 'assertive'; debounce?: number; format?: (v: T) => string | null | false; immediate?: boolean }): () => void;
export namespace announce { function init(): void; function clear(politeness?: 'polite' | 'assertive'): void; }

busy function#

Busy without disabled: aria-busy + aria-disabled + data-busy, clicks/Enter are muted, focus stays on the element

export function busy(el: Element, pending: ReadonlySignal<boolean> | (() => boolean)): () => void;

live function#

Reactive announcement of a signal/function with debounce: live(() => ${n.value} results)

export function live<T>(source: ReadonlySignal<T> | (() => T), opts?: { politeness?: 'polite' | 'assertive'; debounce?: number; format?: (v: T) => string | null | false; immediate?: boolean; clearAfter?: number | false }): () => void;

modal function#

Native modal: open → showModal(); Esc/close → open = false

export function modal(dialog: HTMLDialogElement, open: Signal<boolean>): () => void;

roving function#

export function roving(container: Element, opts?: RovingOptions): { dispose(): void; moveFocus(delta: number): void; refresh(): void; active: ReadonlySignal<number>; setActive(i: number): void };

tabbables function#

Tabbable elements in document order, including open shadow roots and <slot>; inert/hidden/disabled/tabindex<0/closed <details> are excluded

export function tabbables(root: Element | ShadowRoot): HTMLElement[];

trap function#

export function trap(container: Element, opts?: TrapOptions): (() => void) & { dispose(): void; refresh(): void };

CSS & theming#

adoptStyles function#

export function adoptStyles(root: Document | ShadowRoot, ...sheets: CSSStyleSheet[]): void;

css function#

Styles in a cascade layer: css.layer('components').card { … }

export function css(strings: TemplateStringsArray | string, ...values: unknown[]): CSSStyleSheet;
export namespace css { function layer(name: string): (strings: TemplateStringsArray | string, ...values: unknown[]) => CSSStyleSheet; }

injectStyles function#

export function injectStyles(): void;

media function#

matchMedia as a signal (one per query)

export function media(query: string): ReadonlySignal<boolean>;

scopedStyle function#

Scoped styles without mutating id: the data-aegis-css attribute, one sheet per text (refcount), removed on scope dispose

export function scopedStyle(el: Element, cssText: string): CSSStyleSheet;

theme function#

Theme: mode 'light' | 'dark' | 'system' in localStorage + an attribute on <html> + color-scheme

export function theme(opts?: { attr?: string; storage?: string }): { mode: Signal<'light' | 'dark' | 'system'>; dark: ReadonlySignal<boolean> };

Layout, timers, observers#

debounced function#

export function debounced<T extends (...args: any[]) => any>(fn: T, ms: number): T & { cancel(): void; /** call immediately, cancelling the timer */ flush: T };

interval function#

export function interval(fn: () => void, ms: number): () => void;

inView function#

Element visibility as signals (IntersectionObserver)

export function inView(el: Element, opts?: IntersectionObserverInit): { visible: ReadonlySignal<boolean>; ratio: ReadonlySignal<number> };

lazy function#

export function lazy(el: Element, loadFn: (el: Element) => Promise<void> | void, opts?: {
    skeleton?: (() => Element) | Element;
    rootMargin?: string;
    threshold?: number;
}): { loaded: Signal<boolean>; loading: Signal<boolean>; dispose: () => void };

mutate function#

export function mutate(el: Element, callback: MutationCallback, opts?: MutationObserverInit): () => void;

observe function#

export function observe(el: Element, callback: IntersectionObserverCallback, opts?: IntersectionObserverInit): () => void;

resize function#

export function resize(el: Element, callback: (entries: ResizeObserverEntry[]) => void): () => void;

size function#

Element size as signals (ResizeObserver)

export function size(el: Element, opts?: { box?: 'border-box' | 'content-box' }): { width: ReadonlySignal<number>; height: ReadonlySignal<number> };

throttled function#

export function throttled<T extends (...args: any[]) => any>(fn: T, ms: number): T;

timeout function#

export function timeout(fn: () => void, ms: number): () => void;

viewport function#

The window as signals (singleton, one passive listener, writes in rAF)

export function viewport(): { width: ReadonlySignal<number>; height: ReadonlySignal<number>; scrollX: ReadonlySignal<number>; scrollY: ReadonlySignal<number> };

virtualScroll function#

export function virtualScroll<T>(parent: Element, items: T[] | Signal<T[]> | ReadonlySignal<T[]> | (() => T[]), opts: {
    /** 'cv' (default) — all rows in the DOM under content-visibility; 'window' — DOM recycling, only the visible rows + overscan in the DOM */
    mode?: 'cv' | 'window';
    overscan?: number;
    /** container height for mode 'window' */
    height?: number | string;
    itemHeight?: number;
    chunkSize?: number;
    /** row key ("id" by default); rows are keyed, with their own scope */
    key?: string | ((item: T, index: number) => string | number);
    renderItem: (item: T, index: number) => Element | DocumentFragment;
}): { container: HTMLElement; refresh(): void; dispose(): void; /** mode 'window' */ range?: ReadonlySignal<{ start: number; end: number; total: number }>; scrollToIndex?(i: number, opts?: { align?: 'start' | 'center' | 'end' }): void };

i18n#

i18n function#

Translations: plural through Intl.PluralRules ({ one, few, many, other } by params.n/count), nested keys, reactive locale t.locale with lazy loading (t.loading), Intl formatters t.num/t.date/t.rel/t.list. i18n(flatDict) — a flat dictionary of one locale.

export function i18n<D extends Record<string, string>>(dict: D, opts?: I18nOptions): TranslationFunction<keyof D & string>;
export function i18n(dicts?: Record<string, unknown>, opts?: I18nOptions): TranslationFunction;

Other#

VERSION const#

export const VERSION: string;

Types#

A11yMsg type#

Conditional rendering with automatic scope management. Creates a Comment anchor internally, inserts/removes branches reactively. Each branch gets its own Scope — effects/listeners are disposed on switch.

condition — signal, function, or boolean controlling which branch shows trueBranch — content factory or element for truthy condition falseBranch — content factory or element for falsy condition (optional) returns Comment anchor node (insert this into your DOM)

The three states of a resource in html``: ${when(users, { loading, error, data })}

a11y message: a string | a function of the value | false (silent)

export type A11yMsg<X = unknown> = string | ((x: X) => string | null | false) | false;

AegisConfig interface#

export interface AegisConfig {
    /** Trusted origins (besides your own and baseURL): only they receive the flat headers and the CSRF token */
    origins?: string[];
    /** Allowed non-standard schemes for URL attributes (myapp:, intent:) */
    urlSchemes?: string[];
    /** Islands: origin policy for data-aegis-src ('same-origin' | function | RegExp | prefixes) and an allow-list of names inside [data-aegis-untrusted] */
    islands?: { src?: 'same-origin' | ((url: string) => boolean) | RegExp | string[]; allow?: string[] };
    /** Trusted Types: the name of the passthrough policy for html`` literals (default 'aegis') and the policy for server HTML (swap/boost/wireForm) */
    trustedTypes?: { name?: string; server?: ((html: string, who: string) => any) | string };
    /** event delegation: one listener on document for the listed types (bubbling only; capture/passive/once and @ev.direct — directly) */
    delegateEvents?: string[] | null;
    /** SWR cache capacity: maxEntries (default 500, SIEVE eviction among idle entries), maxBytes (default 0 — no limit) */
    cache?: { maxEntries?: number;
    /** Persist principal: entries and offline mutations of another scope are neither hydrated nor replayed (logout: cache.purge()) */
    scope?: () => string | null | undefined;
    /** Names of query parameters whose values are replaced with * in cache keys, BroadcastChannel and history */
    redact?: string[]; maxBytes?: number };
    /** response header with key patterns for invalidate() — 'Aegis-Invalidate: /api/users*, /api/stats' (same-origin); false — off */
    invalidateHeader?: string | false;
    /** circuit breaker per origin: after threshold consecutive retryable errors requests fail immediately (e.circuit, e.retryAt) for cooldown, then one probe */
    breaker?: boolean | { threshold?: number; cooldown?: number; key?: (url: string) => string };
    /** entity identification for cache.patchEntity() and cache: { entity }: (obj) => 'user:42' | null */
    identify?: ((obj: any) => string | null) | null;
    /** speculative requests (prefetch, route preload, predict, island warm-up): 'auto' — by navigator.connection (saveData / 2g / prefers-reduced-data → off, 3g → one in flight), false — never, { maxInflight, saveData: 'ignore' } */
    speculation?: 'auto' | false | { maxInflight?: number; saveData?: 'respect' | 'ignore' };
    /** warm-up usefulness threshold (p·min(rtt, horizon) − transfer − fixedCost, ms) and hover delay: 80 | 'auto' (adaptive by dwell histograms) */
    prefetch?: { minUtility?: number; fixedCost?: number; horizon?: number; hoverDelay?: number | 'auto'; rtt?: number; bytes?: number } | null;
    /** retry limit per client in a sliding window: retries ≤ ratio × requests + min; rejection — e.budget === true */
    retryBudget?: boolean | { ratio?: number; min?: number; window?: number };
    /** revalidation scheduler: token-bucket refill per reason (ms), concurrency, stagger between starts, reconnect jitter */
    revalidate?: { focus?: number; reconnect?: number; concurrency?: number; stagger?: number; reconnectJitter?: number };
    /** a preset or your own scheme; null — off; without a call — auto-detected from <meta name="aegis-csrf"> / <meta name="csrf-token"> */
    csrf?: CsrfPreset | CsrfConfig | null;
    /** default headers (default: X-Requested-With: XMLHttpRequest) */
    headers?: Record<string, string>;
    baseURL?: string;
    /** ms; 0 — no timeout */
    timeout?: number;
    /** fetch replacement: proxies, logging, mocks */
    fetch?: (url: string, init: RequestInit) => Promise<Response>;
    onError?: (error: HttpError, info: { url: string; status: number }) => void;
    /** after a form's PRG redirect: 'assign' (default) — location.assign(response.url); 'none'; or your own handler */
    onRedirect?: 'assign' | 'none' | ((response: Response) => void);
}

AsyncValidationRule type#

Rule: (value, key, fields, { signal }) → error string | null | Promise (async — with debounce, cancellation and validating[key])

export type AsyncValidationRule<V = any> = (value: V, key: string, fields: Record<string, Signal<any>>, ctx: { signal: AbortSignal | null }) => string | null | undefined | Promise<string | null | undefined>;

Attachment interface#

attach() marker for html``: <canvas ${attach(el => …)}>

export interface Attachment<E extends Element = Element> {
    readonly fn: (el: E) => void | (() => void);
    readonly opts: AttachOptions;
}

AttachOptions interface#

export interface AttachOptions {
    /** init + cleanup only, without reactive re-runs (maps, editors) */
    once?: boolean;
}

CacheEntryStats interface#

export interface CacheEntryStats { key: string; state: CacheState; age: number | null; staleTime: number; subscribers: number; inflight: boolean; error: string | null; size: number; gcIn: number | null; fetches: number; unchanged: number; suggestedStaleTime: number | null; prefetched: string | null; tags: string[] | null }

CacheEvent interface#

export interface CacheEvent { t: number; reason: 'mount' | 'url' | 'refresh' | 'invalidate' | 'focus' | 'reconnect' | 'prefetch' | 'seed' | string; result: 'fetch' | 'fresh' | 'joined' | 'set'; ms?: number; status?: 'ok' | 'error'; changed?: boolean }

CacheExplain interface#

export interface CacheExplain {
    key: string; state: CacheState; why: string;
    age?: number | null; staleTime?: number; cacheTime?: number | null; subscribers?: number;
    /** ms until garbage collection (null — there are subscribers, Infinity — cacheTime: Infinity) */
    gcIn?: number | null; fetches?: number; unchanged?: number;
    /** observed change / access interval, ms */
    changeInterval?: number | null; readInterval?: number | null;
    /** staleTime from observations (≈5% stale reads) */
    suggestedStaleTime?: number | null;
    tags?: string[] | null; prefetched?: string | null; /** ETag of the last response (If-None-Match → 304) */ etag?: string | null; staleTimeMode?: 'http' | 'auto' | string | null; history: CacheEvent[];
}

CacheKeyPart type#

A cache key part: a string, a number, a params object or a function (the reactive part)

export type CacheKeyPart = string | number | boolean | null | Record<string, unknown> | (() => string | number | Record<string, unknown>);

CacheOptions interface#

export interface CacheOptions {
    /** a key instead of the URL: a string or a hierarchical array ['users', () => id.value] (invalidate(['users']) matches all) */
    key?: string | CacheKeyPart[] | (() => string | CacheKeyPart[]);
    /** tags for invalidate({ tags }) */
    tags?: string | string[];
    /** after every list response seed the child entries: (data) => [[key, item], …] — a card opens without a request */
    seeds?: (data: any) => Array<[string | CacheKeyPart[], unknown]>;
    /** the response is an entity (configure({ identify })): update it in every list */
    entity?: boolean;
    /** ms | 'http' (Cache-Control max-age / Age / Expires of the response; ['http', fallbackMs]) | 'auto' | { auto: true, k?, min?, max? } — T* = sqrt(2k/(λ̂μ̂)) − 1/λ̂ from the observed rates */
    staleTime?: number | 'http' | ['http', number] | 'auto' | { auto: true; k?: number; min?: number; max?: number };
    /** ms until an idle entry is collected; Infinity — keep until evicted by limits; 'http' — max-age + stale-while-revalidate from the response */
    cacheTime?: number | 'http';
    /** keep old data when the URL changes (default true for a reactive source) */
    keepPrevious?: boolean;
    /** default ['focus', 'reconnect']; [] — off. Events that arrived in a hidden tab are applied when you return to it */
    revalidateOn?: Array<'focus' | 'reconnect'>;
    /** polling: ms or a function of the data (0 — stop); one timer for all resources, 1 s grid, backoff on errors, sleeps in a hidden tab */
    interval?: number | ((data: any) => number);
    /** keep polling in a hidden tab (the browser throttles anyway) */
    background?: boolean;
    /** never evict the entry by cache limits */
    pin?: boolean;
    /** the entry survives a reload (IndexedDB): true | { version — a format change resets, maxBytes — storage budget (4 MB), maxAge — lifetime (7 days) } */
    persist?: boolean | { version?: number; maxBytes?: number; maxAge?: number };
    /** share data and invalidation with other tabs (BroadcastChannel, default true) */
    sync?: boolean;
}

CacheState type#

export type CacheState = 'fresh' | 'stale' | 'inflight' | 'error' | 'empty' | 'absent';

CacheStats interface#

export interface CacheStats { entries: CacheEntryStats[]; prefetch: { fired: number; used: number; wasted: number; byKind: Record<string, { p: number; n: number }>; hoverDelay: number }; speculation: { inflight: number; queued: number; fired: number; skipped: number; aborted: number }; ghost: number; bytes: number; evictions: number; limits: { maxEntries: number; maxBytes: number }; now: number }

ClassValue type#

class: string | array | { name: reactive truthy } (clsx semantics)

export type ClassValue = string | null | undefined | false | ClassValue[] | Record<string, Reactive<unknown>>;

Component type#

D — the shape of the element's data-* attributes (JSON values are parsed); the cast is unchecked, like defineProps<T>()

Component = a function (ctx) => Node | api | void; ctx.props — a reactive() object of props

export type Component<P = Record<string, any>, R = void | object | Node> = (ctx: ComponentContext & { props: P & ReactiveExtras<P> }) => R | Promise<R>;

ComponentContext interface#

Rich context passed to component setup functions. All DOM helpers are pre-bound to the component's scope.

export interface ComponentContext<E extends Element = HTMLElement> {
    el: E;
    /**
     * Server-rendered children of the component (a snapshot before setup). Without a selector — all remaining ones,
     * with a selector ('[slot=footer]') — only the matching ones. Nodes are moved, not copied.
     */
    slot(selector?: string): DocumentFragment;
    signal: typeof signal;
    computed: typeof computed;
    effect: typeof effect;
    batch: typeof batch;
    on: typeof on;
    delegate: typeof delegate;
    bind: typeof bind;
    text: typeof text;
    attr: typeof attr;
    cls: typeof cls;
    style: typeof style;
    clsMap: typeof clsMap;
    styleMap: typeof styleMap;
    html: typeof html;
    show: typeof show;
    list: typeof list;
    interval: typeof interval;
    timeout: typeof timeout;
    observe: typeof observe;
    resize: typeof resize;
    mutate: typeof mutate;
    provide: typeof provide;
    inject: typeof inject;
    when: typeof when;
    selector: typeof selector;
    /** Pre-bound guardedFetch — already tied to this component's scope */
    guardedFetch: GuardedFetch;
    /** State that survives island hibernation and page-out (onSaveInstanceState): the signal is restored from the snapshot on remount */
    state<T>(key: string, init: T): Signal<T>;
    /** the same, short name */
    fetch: GuardedFetch;
    /** the component's scope (for code after await: scope.run(() => …)) */
    scope: Scope;
    /** errorBoundary without a browser extension: errors of the component's effects */
    onError(fn: (error: any) => void): () => void;
    debounced: typeof debounced;
    throttled: typeof throttled;
    poll: typeof poll;
    /** Returns unregister — remove the cleanup early */
    onDispose(fn: () => void): () => void;
}

ComponentResult type#

The result of component(): if setup returned a template (Node) it is inserted into el and { el, destroy } is returned

export type ComponentResult<R> = R extends Node ? { el: Element; destroy(): void } : R;

Computed interface#

computed(): a read-only signal with manual dispose (rarely needed — it dies with its scope)

export interface Computed<T> extends ReadonlySignal<T> {
    dispose(): void;
}

ComputedOptions interface#

export interface ComputedOptions<T> extends SignalOptions<T> {
    /** computed((prev) => …, { initial }) — the previous value as the first argument */
    initial?: T;
}

Context interface#

export interface Context<T> { readonly id: symbol; readonly default: T }

CsrfConfig interface#

export interface CsrfConfig {
    header: string;
    cookie?: string;
    meta?: string;
    token?: () => string;
    /** decodeURIComponent of the cookie value (Laravel) */
    decode?: boolean;
}

CsrfPreset type#

export type CsrfPreset = 'django' | 'rails' | 'laravel' | 'go';

Displayable type#

export type Displayable = string | number | boolean | null | undefined;

EffectOptions interface#

export interface EffectOptions {
    /** false — children of the effect (effect/on/interval/createScope/subscribe created in its body) live until the owner is disposed, not until the next run */
    own?: boolean;
    name?: string;
    /** print the reason for every re-run (dev) */
    trace?: boolean;
    /** 'sync' (default) — synchronous; 'micro' — one run per microtask; 'frame' — one run per frame */
    flush?: 'sync' | 'micro' | 'frame' | 'transition' | 'idle';   // transition/idle — deadline classes (250 ms / 2 s), drained in slices after the current task
}

ElementDefinition interface#

export interface ElementDefinition<P extends Record<string, PropDefinition> = Record<string, PropDefinition>> {
    props?: P;
    shadow?: boolean;
    styles?: CSSStyleSheet | string;
    setup?: (el: HTMLElement, props: PropSignals<P>, ctx: {
        internals?: ElementInternals;
        shadow?: ShadowRoot;
        emit(name: string, detail?: unknown): boolean;
    }) => void | (() => DocumentFragment | Element) | Node;
    formAssociated?: boolean;
    extends?: string;
}

ElementProps type#

ctx.props of a custom element: { count: Number } → number | null, { count: { type: Number, default: 0 } } → number

export type ElementProps<P> = { [K in keyof P]: PropValue<P[K] extends PropType ? { type: P[K] } : P[K]> };

Fetcher type#

export type Fetcher = (url: string, opts: { signal?: AbortSignal; method?: string; body?: unknown }) => Promise<unknown>;

FieldArray interface#

export interface FieldArray<R extends Record<string, any> = Record<string, any>> {
    rows: ReadonlySignal<FieldArrayRow[]>;
    length: ReadonlySignal<number>;
    push(init?: Partial<R>): FieldArrayRow;
    insert(i: number, init?: Partial<R>): FieldArrayRow;
    remove(i: number): void;
    move(from: number, to: number): void;
    swap(a: number, b: number): void;
    replace(rows: Partial<R>[]): void;
    clear(): void;
    nameOf(i: number, sub?: string): string;
}

FieldArrayRow interface#

A fieldArray() row: key is stable across renumbering (the key for list()), index — the current position

export interface FieldArrayRow { key: number; index: number; value(sub: string): Signal<any>; field(sub: string): FieldRef }

FieldDef interface#

Form field: { value, rules } — rules are typed by the value: minLen(3) on a numeric field is a type error

export interface FieldDef<V = any> { value: V; rules?: Array<ValidationRule<V> | AsyncValidationRule<V>> }

FieldRef interface#

Field reference: bind:field=${f.field('email')} — bind + touched + aria-invalid + aria-describedby + the error container in one line

export interface FieldRef<T = any> {
    key: string;
    value: Signal<T>;
    /** the shown error */
    error: Signal<string | null>;
    /** the truth: the result of the rules for the current value, regardless of display */
    issue: ReadonlySignal<string | null>;
    touched: Signal<boolean>;
    validating: ReadonlySignal<boolean>;
    id: string;
    errorId: string;
}

FormA11y interface#

Form a11y policy: field — how to announce a field error ('blur' — one polite announcement on blur, 'live' — the span becomes role=status, 'off'), summary — one announcement on a failed submit

export interface FormA11y { field?: 'blur' | 'live' | 'off'; summary?: boolean }

FormCore interface#

export interface FormCore {
    /** nested object of values: items[0][qty] → { items: [{ qty }] } */
    values: ReadonlySignal<any>;
    /** truth per field: the result of sync rules and the Standard Schema for the current values, recomputed reactively; $any — at least one */
    issues: Record<string, ReadonlySignal<string | null>> & { $any: ReadonlySignal<boolean>; $form: ReadonlySignal<string | null> };
    touched: Record<string, Signal<boolean>>;
    /** valid and nothing is validating/submitting — for a disabled button */
    canSubmit: ReadonlySignal<boolean>;
    /** there was at least one submit */
    submitted: ReadonlySignal<boolean>;
    /** submit lifecycle; submitting — a computed of it */
    status: Signal<FormStatus>;
    /** abort the current submit (the handler receives ctx.signal) */
    abort(): void;
    /** focus the first shown error */
    focusFirstError(): boolean;
    /** bind an input to a field (per-input layer: two-way, touched, display mode, aria, :user-invalid); f.wire('email') — a directive for html`` */
    wire(el: HTMLElement, key?: string): () => void;
    wire(key: string): Attachment<any>;
    /** field reference for bind:field=${f.field('email')} */
    field(key: string): FieldRef;
    /** bring a whole <form> to life — every [name], like wireForm */
    attach(formEl: HTMLFormElement): HTMLFormElement;
    /** field keys (reactive: the form grows) */
    keys: ReadonlySignal<string[]>;
    /** Standard Schema output (coerce / trim / default) — goes into submit instead of the raw values; null while there are issues */
    parsed: ReadonlySignal<any>;
    /** the form grows: add a field (rules — explicit or by the 'items[].qty' pattern), remove, rename (signals move along) */
    addField(key: string, initial?: unknown, rules?: RuleLike[], opts?: { initial?: unknown }): Signal<any>;
    removeField(key: string): void;
    renameField(from: string, to: string): void;
    /** GOV.UK-style error summary: role=alert, a heading with the error count, links to the fields; on a failed submit focus goes to it */
    summary(target?: string | Element, opts?: { heading?: 'h2' | 'h3' | 'p' }): Element;
    /** shown errors: [{ key, message, el }] — for your own summary */
    errorList: ReadonlySignal<Array<{ key: string; message: string; el: HTMLElement | null }>>;
    /** protection against losing edits: beforeunload + Navigation API interception with confirmation; returns dispose */
    guard(opts?: GuardOptions): () => void;
    validating: Record<string, ReadonlySignal<boolean>> & { $any: ReadonlySignal<boolean> };
    dirtyFields: ReadonlySignal<Record<string, true>>;
    /** changed fields only (for PATCH) */
    changes: ReadonlySignal<any>;
    /** sync rules + schema; async rules run in the background */
    validate(): boolean;
    validateField(key: string): boolean;
    /** sync + async + schema */
    validateAsync(): Promise<boolean>;
    /** new “initial” values (an object or a signal, e.g. resource().data) */
    setInitial(values: Record<string, any> | ReadonlySignal<any> | Signal<any>): void;
    /** the current values become the initial ones (after a successful PATCH) */
    commit(): void;
    /** beforeunload while dirty; returns dispose */
    guardUnload(): () => void;
    setErrors(errors: Record<string, any> | Array<{ path?: string | string[]; pointer?: string; message: string }>): void;
    reset(): void;
}

FormOptions interface#

export interface FormOptions {
    a11y?: FormA11y;
    /** on a failed submit focus the summary (if any) or the first field */
    focusOnError?: 'summary' | 'field' | false;
    types?: Record<string, typeof Date | typeof Number | typeof Array | typeof Boolean | typeof String>;
    rules?: Record<string, RuleLike[]>;
    schema?: StandardSchemaV1<any, any>;
    asyncDebounce?: number;
    /** when to SHOW rule errors (the truth is always in issues): blur, then live for fields with an error (default) | live | only after submit */
    mode?: 'blur-then-live' | 'live' | 'submit';
    /** false — do not touch native validation (noValidate / setCustomValidity) */
    native?: boolean;
}

FormResult interface#

export interface FormResult<T extends Record<string, { value: any; rules?: any[] }>> {
    fields: { [K in keyof T]: Signal<T[K]['value']> };
    errors: { [K in keyof T]: Signal<string | null> };
    dirty: ReadonlySignal<boolean>;
    valid: ReadonlySignal<boolean>;
    validate(): boolean;
    validateField(key: keyof T): boolean;
    submit(url: string, opts?: { headers?: Record<string, string>; transform?: (body: any) => any; fetchOpts?: RequestInit }): Promise<{ ok: boolean; data?: any; status?: number; errors?: any; error?: Error }>;
    reset(): void;
    setErrors(errors: Partial<{ [K in keyof T]: string | string[] }>): void;
}

FormStatus type#

export type FormStatus = 'idle' | 'validating' | 'submitting' | 'success' | 'error';

FormSubmitState interface#

export interface FormSubmitState {
    submitting: ReadonlySignal<boolean>;
    submitCount: ReadonlySignal<number>;
    submitError: ReadonlySignal<unknown>;
    result: ReadonlySignal<unknown>;
}

FunctionBinding type#

[get, set] — function binding for bind:value / bind(): the same as lens(get, set)

export type FunctionBinding<T = any> = [() => T, (v: T) => void];

GuardedFetch interface#

Factory: creates a fetch function with auto-abort of previous requests. Each call to the returned function aborts the prior inflight request. Scope dispose aborts the current request.

scope — optional Scope for auto-abort on dispose (falls back to current scope) returns an async fetch function: (url, opts?) => Promise<json | text | undefined>

export interface GuardedFetch {
    /** a stale (cancelled) call never resolves; { stale: 'undefined' } — the old behaviour */
    (url: string, opts?: RequestOptions & { stale?: 'undefined' }): Promise<any>;
    pending: ReadonlySignal<boolean>;
    error: ReadonlySignal<unknown>;
}

GuardOptions interface#

export interface GuardOptions {
    /** your own confirmation (a dialog) — Promise<boolean>; window.confirm by default */
    confirm?: (toUrl: string) => boolean | Promise<boolean>;
    /** treat an anchor (#) navigation as leaving */
    hash?: boolean;
}

HtmlValue type#

What can go into html``: text, a node, a signal, a function (reactive), ref/attach, a class/style object, an array. Not Promise/Date (${String(date)}, when()/resource())

export type HtmlValue = Displayable | Node | ReadonlySignal<any> | ((...args: any[]) => unknown) | Ref<any> | Attachment<any> | ClassValue | Record<string, Reactive<unknown>> | FunctionBinding | FieldRef | HtmlValue[];

HydrateHandle interface#

export interface HydrateHandle {
    el: HTMLElement;
    name: string;
    /** undefined while the island waits for its loading strategy */
    api: unknown;
    ready: Promise<unknown>;
}

HydrateOptions interface#

export interface HydrateOptions {
    /** MutationObserver: inserted islands come alive, removed ones are destroyed (htmx/Turbo/jQuery) */
    watch?: boolean;
    /** remount the ones already alive */
    force?: boolean;
    /** override data-aegis-load for all (tests: 'eager'). Strategies with arguments: 'visible(300px)', 'idle(1500)', 'interaction(click,keydown)' */
    load?: 'eager' | 'visible' | 'idle' | 'interaction' | string;
    /** no warnings about unregistered components */
    quiet?: boolean;
    /** ms of synchronous work before scheduler.yield(); Infinity — everything synchronous. Default 8 */
    budget?: number;
    /** Working-set paging of visible islands: margin — how far from the screen an island is paged out with a snapshot (or data-aegis-resident), max — the resident set size (CLOCK) */
    resident?: { margin?: string; max?: number };
    /** deadline for idle islands, ms. Default 2000 */
    idleTimeout?: number;
}

I18nOptions interface#

export interface I18nOptions {
    /** initial locale (default: <html lang> or 'en') */
    locale?: string;
    /** fallback locale for missing keys */
    fallback?: string;
    pluralOpts?: Intl.PluralRulesOptions;
    /** write t.locale into <html lang> */
    syncLang?: boolean;
}

InvalidatePattern type#

Key pattern: an exact key, 'prefix*', ['users'] (hierarchical prefix), a predicate or { prefix, exact, tags, refetch }

export type InvalidatePattern = string | CacheKeyPart[] | ((key: string, entry?: unknown) => boolean) | { prefix?: string; exact?: string | CacheKeyPart[]; tags?: string | string[]; refetch?: 'active' | 'all' | 'none' };

IslandProps type#

An island from a component function: data-* (with types) → ctx.props

ctx.props of an island: declared types are typed, other data-* — unknown

export type IslandProps<T> = { [K in keyof T]: PropValue<{ type: T[K] }> } & Record<string, unknown>;

IslandSetup type#

export type IslandSetup<D = Record<string, unknown>> = (el: HTMLElement, data: D, ctx: ComponentContext<HTMLElement>) => void | object | Node | Promise<void | object | Node>;

ListOptions interface#

export interface ListOptions<T = any> {
    key?: string | ((item: T, index: number) => string | number);
    /** markup for the empty list */
    fallback?: (() => Node | DocumentFragment | string) | Node;
    /** enter/leave CSS contract for rows (true → 'aegis'); a leaving row gets data-leaving */
    transition?: boolean | string | 'view';
    /** 'signal' — renderFn receives Signal<T>; replacing the object under a key patches the signal instead of re-rendering */
    item?: 'signal';
    /** 'view' — moves and inserts are animated with the View Transitions API (one startViewTransition per flush, FLIP for free); viewClass — the view-transition-class of rows (default 'aegis-row') */
    viewClass?: string;
    /** off-screen rows are frozen (contentvisibilityautostatechange): the row scope is destroyed, the DOM stays as a snapshot, on return — re-render with current data */
    hibernate?: boolean;
    /** a signal “rows are still being created in slices” (streaming assembly under startTransition / typing) */
    pending?: Signal<boolean>;
    /** assembly order from the visible part: the scrolling element and the row height */
    viewport?: Element; itemHeight?: number;
}

LoaderSource interface#

export interface LoaderSource<P, T> {
    params?: P | (() => P);
    loader: (ctx: { params: P; signal: AbortSignal }) => Promise<T> | T;
}

Mutation interface#

export interface Mutation<A extends unknown[], R> {
    /** run; the error is not thrown — it is in .error (run() throws) */
    (...args: A): Promise<R | undefined>;
    run(...args: A): Promise<R | undefined>;
    pending: ReadonlySignal<boolean>;
    error: ReadonlySignal<unknown>;
    data: ReadonlySignal<R | null>;
    abort(): void;
}

MutationOptions interface#

export interface MutationOptions<A extends unknown[]> {
    /** 'validate' — signals read through ctx.read before the response are validated by version on commit (OCC): changed → the patch is dropped, the keys are refetched, E052 in dev */
    isolation?: 'validate';
    /** resources whose data is snapshotted before optimistic and rolled back on error */
    resources?: Array<{ data: { peek(): any }; mutate(v: any): void }>;
    optimistic?: (...args: A) => void;
    invalidates?: InvalidatePattern | InvalidatePattern[];
    /** wait for the invalidates refetches before clearing pending (default true) */
    awaitInvalidates?: boolean;
    /** server response → a new base of the affected resources without a refetch: (result, base, ...args) => data */
    commit?: (result: any, base: any, ...args: A) => any;
    /** update cache entries by patterns without a refetch: { '/api/users*': (data, result, ...args) => data } */
    updates?: Record<string, (data: any, result: any, ...args: A) => any>;
    /** update entities in every entry (configure({ identify })): (result, ...args) => [[entityKey, (node) => node], …] */
    patch?: (result: any, ...args: A) => Array<[string, (node: any) => any]>;
    /** 412/409 from the server: base — before the edit, local — with optimistic, server — current; return 'server' | 'client' | an object to retry the mutation with */
    onConflict?: (c: { base: any; local: any; server: any; merge(): { value: any; conflicts: string[] }; error: HttpError }) => 'server' | 'client' | object | Promise<'server' | 'client' | object>;
    /** screen-reader announcements: true — defaults (Saved / the error text / Change reverted / conflict), or your own */
    announce?: true | { pending?: A11yMsg; success?: A11yMsg<any>; error?: A11yMsg<unknown>; undone?: A11yMsg<unknown>; conflict?: A11yMsg<unknown> };
    /** 'ignore' (default, double-submit guard) | 'queue' | 'latest' | 'parallel' */
    concurrent?: 'ignore' | 'queue' | 'latest' | 'parallel';
    onSuccess?: (result: any, ...args: A) => void;
    onError?: (error: unknown, ...args: A) => void;
}

OfflineOptions interface#

export interface OfflineOptions {
    /** attempts per mutation before dead-letter (default 10); 4xx — immediately */
    maxAttempts?: number;
    dbName?: string;
    storeName?: string;
    staleTime?: number;
    syncTag?: string;
}

OfflineResourceResult interface#

export interface OfflineResourceResult<T> extends ResourceResult<T> {
    /** dead-letter: mutations the server rejected or that could not be delivered */
    failed: ReadonlySignal<Array<{ mutation: { mutId: string; method: string; url: string; body: unknown }; error: unknown }>>;
    online: ReadonlySignal<boolean>;
    syncing: ReadonlySignal<boolean>;
    /** network mutation; offline or a network error → into the IndexedDB queue, sent when online / Background Sync */
    send(method: string, url: string, body?: unknown, opts?: { optimistic?: (current: T | null) => T }): Promise<unknown>;
    /** compatibility: mutate('POST', url, body, optimistic) === send(...) */
    mutate(method: string, url: string, body?: unknown, optimistic?: (current: T | null) => T): Promise<unknown>;
    mutate(fnOrValue: T | ((prev: T | null) => T)): void;
}

PluralForms type#

export type PluralForms = Partial<Record<'zero' | 'one' | 'two' | 'few' | 'many' | 'other', string>>;

Predictor interface#

Navigation predictor: a first-order Markov chain over route patterns with forgetting (decay), a server prior (kappa pseudo-counts) and persistence in storage

export interface Predictor {
    learn(from: string, to: string): void;
    /** rank the candidates: p = (count + kappa·prior + alpha) / (n + kappa + alpha·|candidates|) */
    next(from: string, candidates: string[]): Array<{ key: string; p: number }>;
    p(from: string, to: string): number;
    /** server prior for from: { to: p } — or <script type="application/json" data-aegis-predict="from"> via seedFrom()/hydrate() */
    prior(from: string, map: Record<string, number>): void;
    reset(): void;
}

PropDefinition interface#

export interface PropDefinition<T = unknown> {
    type?: typeof Number | typeof String | typeof Boolean | typeof Object | typeof Array;
    default?: T;
    reflect?: boolean;
    attribute?: string;
}

PropSignals type#

export type PropSignals<P> = { [K in keyof P]: Signal<PropValue<P[K]>> };

PropType type#

export type PropType = NumberConstructor | BooleanConstructor | StringConstructor | JSON | ObjectConstructor | ArrayConstructor | ((raw: string) => unknown);

PropValue type#

The value type of a prop from { type, default }: { type: Number } → number | null, { type: Number, default: 0 } → number

export type PropValue<P> = P extends { type: BooleanConstructor } ? boolean
    : P extends { type: NumberConstructor } ? (P extends { default: number } ? number : number | null)
    : P extends { type: StringConstructor } ? (P extends { default: string } ? string : string | null)
    : P extends { type: ArrayConstructor } ? (P extends { default: infer D } ? D : unknown[] | null)
    : P extends { type: ObjectConstructor } ? (P extends { default: infer D } ? D : Record<string, unknown> | null)
    : P extends { type: (raw: string) => infer R } ? R
    : P extends { default: infer D } ? D : unknown;

Reactive type#

A value, a signal or a getter — anything the reactive helpers accept

export type Reactive<T> = T | Signal<T> | ReadonlySignal<T> | (() => T);

ReactiveExtras interface#

export interface ReactiveExtras<T> {
    readonly $signals: Record<string, Signal<unknown>>;
    readonly $raw: T;
    $snapshot(): T;
    $patch(patch: Partial<T>): void;
    /** effect over a deep snapshot */
    $subscribe(fn: (snapshot: T) => void): () => void;
    $reset(): void;
}

ReadonlySignal interface#

export interface ReadonlySignal<T> {
    readonly value: T;
    /** read without subscribing */
    peek(): T;
    /** manual subscription: fn(value) on every real change; returns unsubscribe */
    subscribe(fn: (value: T) => void): () => void;
    /** brand: only Aegis signals (not any { value }) */
    readonly [SIGNAL]: true;
}

Ref interface#

DOM element ref — plain object with .el property. NOT a Signal. Set automatically by the html`` engine.

example

const inputRef = ref();
html`<input ${inputRef}>`;
effect(() => inputRef.el?.focus());
export interface Ref<T extends Element = Element> {
    el: T | null;
    /** @internal — called by the html`` engine */
    _set(el: T): void;
}

RequestOptions interface#

export interface RequestOptions extends Omit<RequestInit, 'body' | 'method' | 'headers'> {
    /** If-Match for optimistic locking (the ETag from cache.explain(key).etag or the mutation's ctx.etag) */
    ifMatch?: string;
    method?: string;
    /** object → JSON + Content-Type; FormData/Blob/string — as is */
    body?: unknown;
    query?: Record<string, string | number | boolean> | URLSearchParams;
    headers?: HeadersInit;
    /** ms; default configure().timeout */
    timeout?: number;
    /** return the Response without parsing the body */
    raw?: boolean;
}

ResourceOptions interface#

export interface ResourceOptions<T> {
    initial?: T;
    transform?: (data: unknown) => T;
    fetcher?: Fetcher;
    immediate?: boolean;
    /** structural sharing of the response: unchanged parts keep their identity; arrays of objects are matched by the 'id' field (default), by your own field or a function; false — off */
    share?: boolean | string | ((item: any) => unknown);
    /** one GET per URL in flight for several resource(url) (default true with the default fetcher) */
    dedupe?: boolean;
    /** retries with backoff: true → 3, a number, or a predicate (err, attempt) => boolean; default 0 */
    retry?: boolean | number | ((error: unknown, attempt: number) => boolean);
    /** refetch on events — opt-in only */
    refetch?: { focus?: boolean; reconnect?: boolean; interval?: number };
}

ResourceResult interface#

One contract for resource() / resource({ cache }) / resource({ offline }) / streamResource / infiniteResource

export interface ResourceResult<T> {
    data: ReadonlySignal<T | null> | Signal<T | null>;
    /** a request is in flight and there is no data yet (skeleton once) */
    loading: ReadonlySignal<boolean>;
    /** a request is in flight over existing data (dimming, not flicker) */
    validating: ReadonlySignal<boolean>;
    /** keepPrevious: data of the previous key is shown while the new one loads */
    stale: ReadonlySignal<boolean>;
    status: ReadonlySignal<ResourceStatus>;
    error: ReadonlySignal<HttpError | Error | null>;
    /** current URL / serialised params */
    key: ReadonlySignal<string | null>;
    refresh(): Promise<void>;
    /** Optimistic update: a local write (for offline — also into IndexedDB) */
    mutate(fnOrValue: T | ((prev: T | null) => T)): void;
    abort(): void;
    /** the last request of this resource (for await in tests) */
    readonly promise: Promise<void> | null;
    /** await the data: resolve(data) or reject(error) */
    ready(): Promise<T | null>;
    dispose(): void;
}

ResourceStatus type#

export type ResourceStatus = 'idle' | 'pending' | 'success' | 'error';

RetryOptions interface#

export interface RetryOptions {
    retries?: number;
    base?: number;
    max?: number;
    signal?: AbortSignal;
    shouldRetry?: (error: unknown, attempt: number) => boolean;
}

RouteComponentProps type#

props of a route component: params + { data: the loader result, query }

export type RouteComponentProps<P extends string = string, D = unknown> = RouteParams<P> & { data: D; query: Record<string, string> };

RouteContext interface#

export interface RouteContext<D = unknown> {
    /** loader result */
    data: D;
    params: Record<string, string>;
    query: Record<string, string>;
    /** the element for child routes (layout) */
    outlet: Element | null;
    signal: AbortSignal | undefined;
    route: RouteInfo;
    from: RouteInfo;
}

RouteDef interface#

export interface RouteDef<D = unknown> {
    handler?: RouteHandler<D>;
    /** page component (the island()/mount() contract); mounted into router({ outlet }) or into the parent layout's outlet */
    component?: Component<RouteComponentProps<string, D>>;
    /** data before the old page is disposed; cancelled through signal on a new navigation */
    loader?: (params: Record<string, string>, ctx: { signal: AbortSignal | undefined; query: Record<string, string>; params: Record<string, string>; /** true — a warm-up before the navigation (hover / predict / r.preload): priority can be lowered */ speculative?: boolean }) => D | Promise<D>;
    /** true — go on; false — cancel (sync); a string — redirect; a Promise — wait */
    guard?: (to: RouteInfo, from: RouteInfo) => boolean | string | void | Promise<boolean | string | void>;
    redirect?: string | ((to: RouteInfo, from: RouteInfo) => string);
    /** lazy route: the module's default export = handler */
    load?: () => Promise<RouteHandler<D> | { default: RouteHandler<D> }>;
    /** data warm-up on hover/visible (router({ preload })) */
    preload?: (params: Record<string, string>) => void;
    /** the layout survives a change of the child route; receives a live outlet */
    layout?: (ctx: RouteContext<D>) => void | Promise<void>;
    children?: Record<string, RouteHandler | RouteDef>;
}

RouteHandler type#

export type RouteHandler<D = unknown, P extends string = string> = (params: RouteParams<P>, ctx: RouteContext<D>) => void | Promise<void>;

RouteInfo interface#

export interface RouteInfo { path: string | null; params: Record<string, string>; query: Record<string, string>; search?: string }

RouteParams type#

'/users/:id/posts/:postId' → { id: string; postId: string }

export type RouteParams<P extends string> = P extends `${string}:${infer Name}/${infer Rest}`
    ? { [K in Name | keyof RouteParams<`/${Rest}`>]: string }
    : P extends `${string}:${infer Name}` ? { [K in Name]: string } : Record<string, string>;

Router interface#

export interface Router {
    route: ReadonlySignal<string>;
    params: ReadonlySignal<Record<string, string>>;
    query: ReadonlySignal<Record<string, string>>;
    pending: ReadonlySignal<boolean>;
    error: ReadonlySignal<unknown>;
    state: ReadonlySignal<unknown>;
    transitioning: ReadonlySignal<boolean>;
    /** a search parameter as a two-way signal (URL = state); the route scope is not recreated */
    search<T = string>(name: string, opts?: SearchOptions<T>): Signal<T>;
    search<T = string>(name: string, opts: SearchOptions<T> & { multi: true }): Signal<T[]>;
    navigate(path: string, opts?: { replace?: boolean; state?: unknown }): Promise<unknown>;
    back(): void;
    forward(): void;
    /** per-entry state without navigation */
    setState(state: unknown): void;
    /** the first route has rendered */
    ready: Promise<unknown>;
    /** is there a route for the path (boost() yields to the router) */
    matches(path: string): boolean;
    /** warm a route (code + loader) before navigation within the network budget; p — the probability for the usefulness threshold */
    preload(path: string, p?: number): Promise<void>;
    cleanup(): void;
    dispose(): void;
}

RouterOptions interface#

export interface RouterOptions {
    base?: string;
    root?: Element | Document;
    /** View Transitions between pages: true → types ['page', 'back'|'forward'] in data-vt-type on <html>; a function — your own types; false */
    transition?: boolean | ((info: { back: boolean }) => string[] | false);
    /** class of the active link (aria-current="page" is always set) */
    activeClass?: string;
    /** warm-up of the route's code and loader on intent: 'hover' (cursor slowdown or delay) | 'visible' | 'tap' | { on, delay: 80 | 'auto', velocity, rootMargin } */
    preload?: 'hover' | 'visible' | 'tap' | boolean | { on?: 'hover' | 'visible' | 'tap'; delay?: number | 'auto'; velocity?: number; rootMargin?: string };

RovingOptions interface#

export interface RovingOptions {
    selector?: string;
    /** 'grid' — Left/Right by cells, Up/Down by rows, Home/End within the row, Ctrl+Home/End, PageUp/Down */
    orientation?: 'horizontal' | 'vertical' | 'both' | 'grid';
    wrap?: boolean;
    /** grid: number of columns or 'auto' (from the geometry of the first row) */
    cols?: number | 'auto';
    /** PageUp/PageDown step (rows for grid) */
    page?: number;
    /** a letter → the nearest item by text / aria-label, 500 ms buffer */
    typeahead?: boolean;
    /** horizontal inversion; 'auto' — by computed direction */
    dir?: 'ltr' | 'rtl' | 'auto';
    /** tab stop: 'selected' — [aria-selected/checked/current], 'first', an index */
    initial?: 'selected' | 'first' | number;
    /** aria-activedescendant mode (combobox): focus stays on this input, keys are listened to on it */
    virtual?: HTMLElement | null;
    /** MutationObserver: refresh when children change; a removed active item → focus the element with the same index */
    observe?: boolean;
    /** tree: ArrowRight expands (aria-expanded), ArrowLeft collapses or goes to the parent */
    tree?: boolean;
    onActivate?: (el: Element, index: number) => void;
}

RowIndex interface#

Lazy row index of list(): the signal is created on the first read of .value

export interface RowIndex {
    readonly value: number;
    peek(): number;
    subscribe(fn: (i: number) => void): () => void;
}

RuleLike type#

A rule may be a Standard Schema (zod/valibot/arktype): the first issue.message; an async schema — an async rule

export type RuleLike<V = any> = ValidationRule<V> | AsyncValidationRule<V> | StandardSchemaV1<V, any>;

Scope interface#

export interface Scope {
    readonly name: string | null;
    /** the component element (inject() through DOM ancestors) */
    el: Element | null;
    run<T>(fn: () => T): T;
    /** using scope = createScope() */
    [Symbol.dispose]?(): void;
    /** Returns unregister — remove the cleanup early */
    onDispose(fn: () => void): () => void;
    /** Error handler for effects of this scope and nested ones; errors carry e.aegis = { effect, scope, changed } */
    onError(fn: (error: any) => void): () => void;
    dispose(): void;
}

ScopeInspection interface#

export interface ScopeInspection { scope: string | null; el: Element | null; signals: Array<{ name: string; value: string; /** the signal itself (non-enumerable field) */ readonly ref?: ReadonlySignal<unknown> }>; effects: Array<{ name: string; deps: string[]; scope: string | null; /** source position of the effect() (dev) */ site?: string | null }>; children: number }

SearchOptions interface#

export interface SearchOptions<T> {
    parse?: (raw: string) => T;
    serialize?: (v: T) => string;
    default?: T;
    /** ?tag=a&tag=b → T[] */
    multi?: boolean;
    history?: 'replace' | 'push';
}

ShowOptions interface#

export interface ShowOptions {
    /** branches are created once and hidden with display:none (like v-show): DOM and state are kept */
    keep?: boolean;
    /** CSS contract `${name}-enter-from|active|to` / `${name}-leave-*` (true → 'aegis'); leave plays out before removal */
    transition?: boolean | string;
}

Signal interface#

export interface Signal<T> extends ReadonlySignal<T> {
    value: T;
    /** sig.update(v => v + 1) */
    update(fn: (prev: T) => T): void;
}

SignalOptions interface#

export interface SignalOptions<T> {
    /** the first subscriber appeared (effect / computed / subscribe) — start the producer, open the connection */
    watched?(): void;
    /** the last subscriber left — stop */
    unwatched?(): void;
    name?: string;
    equals?: false | ((a: T, b: T) => boolean);
    /** How often it is written: 'high' — config/locale/theme, 'medium' — session, 'low' (default) — everything else. Derived non-live computeds are re-checked only after writes of their own level (Salsa durability) */
    durability?: 'low' | 'medium' | 'high';
}

SlotSpec type#

export type SlotSpec = Reactive<Displayable> | {
    text?: Reactive<Displayable>;
    attr?: Record<string, Reactive<Displayable>>;
    cls?: ClassValue;
    style?: Record<string, Reactive<string | number | null>>;
    prop?: Record<string, Reactive<unknown>>;
    on?: Record<string, (e: Event) => void>;
};

SpringOptions interface#

export interface SpringOptions {
    stiffness?: number;
    damping?: number;
    mass?: number;
    velocity?: number;
}

StandardResult type#

export type StandardResult<O> = { value: O; issues?: undefined } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<PropertyKey | { key: PropertyKey }> }> };

StandardSchemaV1 interface#

Standard Schema (zod v4 / valibot / arktype / …) — no dependency

export interface StandardSchemaV1<I = unknown, O = I> {
    readonly '~standard': {
        readonly version: 1;
        readonly vendor: string;
        readonly validate: (value: unknown) => StandardResult<O> | Promise<StandardResult<O>>;
    };
}

StreamOptions interface#

export interface StreamOptions<T> {
    method?: string;
    body?: unknown;
    headers?: HeadersInit;
    /** 'ndjson' (default: data — an array of JSON lines), 'text' (data — a string) or a line parser */
    parse?: 'ndjson' | 'text' | ((line: string) => T);
    initial?: unknown;
    reduce?: (acc: any, item: T) => any;
    immediate?: boolean;
}

SwapMode type#

export type SwapMode = 'inner' | 'outer' | 'append' | 'prepend' | 'before' | 'after' | 'morph';

TranslationFunction interface#

Translation function with reactive dictionary.

export interface TranslationFunction<K extends string = string> {
    /** Translate a key, with optional param substitution ({name}) and plural by params.n / params.count */
    (key: K, params?: Record<string, string | number>): string;
    /** Replace entire dictionary (e.g. on locale switch) */
    load(newDict: Record<string, unknown>): void;
    /** Merge additional translations into the current dictionary */
    merge(extra: Record<string, unknown>): void;
    /** The reactive dictionary signal */
    dict: Signal<Record<string, unknown>>;
    /** reactive locale; a write switches the dictionary (lazy loading → t.loading) */
    locale: Signal<string>;
    loading: ReadonlySignal<boolean>;
    num(v: number | bigint, o?: Intl.NumberFormatOptions): string;
    date(v: Date | number, o?: Intl.DateTimeFormatOptions): string;
    rel(v: number, unit: Intl.RelativeTimeFormatUnit, o?: Intl.RelativeTimeFormatOptions): string;
    list(v: Iterable<string>, o?: Intl.ListFormatOptions): string;
    plural(n: number, forms: PluralForms): string;
}

TrapOptions interface#

Focus trap: Tab cycle, autoFocus, focus return; escape / outside (release or your own handler); inert for the background (except allow)

export interface TrapOptions {
    /** true / 'first' — [data-autofocus] → the first tabbable → the container; 'container' — a static container (long text, APG); a selector — your own element */
    autoFocus?: boolean | 'first' | 'container' | string;
    /** focus that left (programmatically, from a widget) is brought back inside; default true */
    recapture?: boolean;
    /** Escape: true — release(), a function — your own handler */
    /** true — Esc/Android back/AT close request through CloseWatcher (the platform stack; <dialog>/popover do not close together with the trap); 'key' — keydown only; a function — your own handler */
    escape?: boolean | 'key' | ((e: Event) => void);
    /** a click outside the container (and outside allow) */
    outside?: boolean | ((e: PointerEvent) => void);
    /** inert for the background */
    inert?: boolean;
    allow?: string;
    /** true — to the element active before trap(); Element | () => Element — your own; if the trigger was removed (a list() row) — the nearest living neighbour */
    returnFocus?: boolean | Element | (() => Element | null);
}

ValidationRule type#

export type ValidationRule<V = unknown> = (value: V, key: string, fields: Record<string, Signal<unknown>>, ctx?: { signal: AbortSignal | null; /** true — inside the computed truth (issues): the rule must be pure */ live?: boolean }) => string | null;

WarningInfo interface#

export interface WarningInfo {
    /** scope path where the warning occurred: component:div#app ‹ list:row (null outside a scope) */
    where?: string | null;
    /** the element the warning is about (printed to the console as %o) */
    el?: Element | null;
    /** source position (dev): '/js/app.js:42:15' — the html`` template, effect() or resource() where the warning occurred */
    site?: string | null;
    /** full file URL for site */
    url?: string | null;
    /** the source line with a caret under the offending ${} (dev; the file is fetched once) */
    snippet?: Promise<string | null>;
    /** the same after resolution */
    snippetText?: string | null; code: string; what: string; why: string; fix: string }

WatchHandle interface#

export interface WatchHandle { (): void; stop(): void; pause(): void; resume(): void }

WireFormResult interface#

export interface WireFormResult {
    /** the <form> itself */
    el: HTMLFormElement;
    keys: ReadonlySignal<string[]>;
    parsed: ReadonlySignal<any>;
    /** attach an input that appeared after wireForm(); detach a field; rescan the form (after swap/morph/list) */
    wire(input: HTMLElement): void;
    unwire(key: string): void;
    rewire(): void;
    /** read errors from the (server) form markup into errors; true — there is at least one */
    adoptErrors(root?: ParentNode): boolean;
    addField(key: string, initial?: unknown, rules?: RuleLike[]): Signal<any>;
    removeField(key: string): void;
    renameField(from: string, to: string): void;
    field(key: string): FieldRef;
    fields: Record<string, Signal<unknown>>;
    errors: Record<string, Signal<string | null>>;
    summary(target?: string | Element, opts?: { heading?: 'h2' | 'h3' | 'p' }): Element;
    errorList: ReadonlySignal<Array<{ key: string; message: string; el: HTMLElement | null }>>;
    guard(opts?: GuardOptions): () => void;
    /** truth per field (sync rules + schema), regardless of display */
    issues: Record<string, ReadonlySignal<string | null>> & { $any: ReadonlySignal<boolean>; $form: ReadonlySignal<string | null> };
    touched: Record<string, Signal<boolean>>;
    dirty: ReadonlySignal<boolean>;
    valid: ReadonlySignal<boolean>;
    canSubmit: ReadonlySignal<boolean>;
    submitted: ReadonlySignal<boolean>;
    status: Signal<FormStatus>;
    submitting: ReadonlySignal<boolean>;
    submitCount: ReadonlySignal<number>;
    result: ReadonlySignal<unknown>;
    abort(): void;
    focusFirstError(): boolean;
    validating: Record<string, ReadonlySignal<boolean>> & { $any: ReadonlySignal<boolean> };
    validateField(key: string): boolean;
    validateAsync(): Promise<boolean>;
    validate(): boolean;
    reset(): void;
    setErrors(errors: Record<string, any> | Array<{ path?: string | string[]; pointer?: string; message: string }>): void;
    /** handler(values, { signal, submitter, event }); without a handler — a server submit (FormData, 422 → field errors, 303 → navigation) */
    submit(handler?: ((values: Record<string, unknown>, ctx: { signal: AbortSignal; submitter: HTMLElement | null; event: SubmitEvent | undefined }) => unknown | Promise<unknown>) | { as?: 'json'; headers?: HeadersInit; onSuccess?: (data: any, r: Response) => void; onRedirect?: 'assign' | 'router' | 'none' | ((r: Response) => void); announceSuccess?: boolean; html?: 'morph' | 'replace' | false; intents?: Record<string, (f: WireFormResult, e: SubmitEvent) => void> }): (e?: Event) => Promise<any>;
    step: Signal<number> | null;
    stepCount: number | null;
    next: (() => boolean) | null;
    prev: (() => void) | null;
}

Wizard interface#

export interface Wizard {
    step: Signal<number>;
    steps: WizardStep[];
    count: number;
    /** validates the fields of the current step (sync + async + schema for them only) and moves on */
    next(): Promise<boolean>;
    prev(): Promise<boolean>;
    go(i: number, opts?: { validate?: boolean }): Promise<boolean>;
    validateStep(i: number): Promise<boolean>;
    first: ReadonlySignal<boolean>;
    last: ReadonlySignal<boolean>;
    progress: ReadonlySignal<number>;
    dispose(): void;
}

WizardStep interface#

export interface WizardStep { index: number; keys(): string[]; valid: ReadonlySignal<boolean>; dirty: ReadonlySignal<boolean>; done: ReadonlySignal<boolean> }

Deprecated aliases#

These still work but are marked @deprecated in aegis.d.ts; each line names the replacement.

  • cachedResource — = resource(source, { cache: true, ...opts }) @deprecated Use resource(url, { cache: true, staleTime }).
  • clsMap — Toggle multiple CSS classes via a map { className: signal/fn/bool }. @returns cleanup function that disposes all class effects @deprecated Use cls(el, { active: sig }) — same object form, diffs only its own classes.
  • component — Use mount(el, Component) — the same setup contract, accepts an element or a selector. component() stays as an alias.
  • defineElement — tagName must contain a hyphen (otherwise a DOMException at runtime — and a type error here) @deprecated Use element(tag, Component, { props }) — the same component function as islands and mount(). defineElement() stays as the low-level form.
  • offlineResource — = resource(source, { offline: true, ...opts }) @deprecated Use resource(url, { offline: true }).
  • store — Reactive store from a plain definition object. - Data properties → signals (get/set transparently) - Getters → computed signals (lazy) - Methods → bound to the proxy (auto-batched) Special keys on the returned proxy: - $signals — raw signals map - $reset() — reset all data properties to initial values @example const todos = store({ items: [], filter: 'all', get filtered() { return this.filter === 'all' ? this.items : this.items.filter(i => i.done); }, add(text) { this.items = [...this.items, { id: Date.now(), text, done: false }]; }, }); @deprecated Use reactive(obj) — getters become computeds, methods become batched actions, plus $patch/$subscribe/$snapshot/$reset. store() = reactive(obj, { shallow: true }).