Data
resource()#
one primitive for async data: resource(url), resource(() => url), resource({ params, loader }), resource(url, { cache: true, staleTime }) (SWR, shared by key, keepPrevious), resource(url, { offline: true }) (IndexedDB + mutation queue). One result shape: data, loading (first load), validating (refetch over data), stale, status, error, refresh(), mutate(), abort(), ready(). Structural sharing keeps row identity on refetch, retry with backoff, refetch: { focus, reconnect, interval } opt-in
mutation()#
pending, double-submit guard, optimistic update + rollback, invalidates
streamResource() / sse()#
NDJSON/text streaming into a growing signal; Server-Sent Events writing straight into signals
configure({ csrf: 'django' })#
CSRF presets (django/rails/laravel/go), default headers, baseURL, timeout; request()/api.get|post|… send JSON, add the token, throw HttpError with the parsed body; defaults.fetcher is the single mocking point
prefetch() / prefetchOn(el, url, { on: 'hover' })#
warm the cache on hover/tap/visible; infiniteResource() for cursor pagination
settled()#
await every in-flight request in tests instead of sleep(50)
Synchronous first render#
show()/list() inside html`` render immediately; flushSync() for manual inserts. Tests read action → assert with no await nextTick
Dev mode without setup#
on automatically on localhost / file:// / ?dev / localStorage['aegis:dev']; every warning prints once with what/why/fix; onWarn() turns warnings into assertions, window.__AEGIS_DEV__ = 'strict' throws AegisWarning; lost-reactivity detectors (E019) catch text(el, count.value), show(count.value > 3, …) and effects that read no signals; reset() isolates tests, root() gives a disposable scope
One CSS contract for motion#
.${name}-enter-from|active|to / .${name}-leave-* works in transition(), show(…, { transition }), list(…, { transition }) and animate(); leave animations finish before nodes are removed, interrupts are safe (no offsetHeight reflow dance), injectStyles() ships defaults in @layer aegis
list() options#
{ key, fallback, transition, item: 'signal' }; item: 'signal' patches a row signal instead of re-rendering when the object behind a key is replaced
Layout as signals#
size(el), inView(el), viewport() (observers, never getBoundingClientRect inside an effect)
Springs#
spring() continues from the current position and velocity when interrupted and compiles to two keyframes + CSS linear(); springSignal() / tween() are values you write target to and read current from
Styles#
css.layer('components')``, scopedStyle() without touching el.id (one sheet per text, ref-counted, removed with the scope)
Interop#
from() turns an EventTarget, a producer or any { subscribe } (Preact, RxJS, Svelte store) into a signal; history() gives undo/redo for a signal, reactive() or store()
A11y#
trap(el, { escape, outside, inert }), modal(dialog, open) on native <dialog>; an unregistered island prints a ready-to-paste register() scaffold
i18n#
plural categories via Intl.PluralRules, reactive t.locale with lazy dictionaries, t.num/date/rel/list formatters; validation messages are localisable (setValidationMessages)
Files in forms#
File values, maxSize('2MB') / mime(['image/*']) / maxFiles(n), f.formData(), f.preview(key)
One component contract#
const Counter = ({ props, html }) => html…``; island('counter', Counter, { types }) for a server island and element('x-counter', Counter, { props }) for a custom element, same function, ctx.props is a reactive object in both
Errors that explain themselves#
every effect error carries e.aegis = { effect, scope, changed } and the message says which effect, which component and which signals changed; scope.onError() / ctx.onError() catch them, so errorBoundary() finally catches errors after mount; trace(sig) prints who wrote a signal, effect(fn, { trace: true }) prints why it re-ran; a typo in the ctx destructuring gets a did-you-mean
reactive() 2.0#
getters become computeds, methods become batched actions, $patch / $subscribe / $snapshot / $reset; only plain objects and arrays are wrapped (Date/Map/File stay as they are); store() = reactive(obj, { shallow: true })
Faster bindings#
${sig} in text and attributes subscribes directly to the node (no effect per binding); list() moves the minimum number of rows (LIS): a swap is 2 DOM moves, not 997
For assistants#
llms.txt (rules + canonical island), AGENTS.md, ERRORS.md (every warning code)
media(), reducedMotion, theme()#
media queries and dark mode as signals with color-scheme; defaults.motion = false turns animations off for tests and honours prefers-reduced-motion in spring/flip/transition
Forms#
submitting, submitCount, submitError, result, errors.$form; server errors mapped from non_field_errors/detail/RFC 9457 arrays; double-submit guard
cachedResource()#
alias of resource(url, { cache: true }); invalidate() revalidates live resources immediately, revalidateOn: ['focus', 'reconnect'] by default
seed() / seedFrom()#
zero-fetch hydration: the server inlines data, the island renders without a request
<script type="application/json" data-aegis-cache="/api/users?page=1" data-aegis-age="120">[…]</script>
<div data-aegis="users" data-url="/api/users?page=1"></div>hydrate() seeds the cache first, so cachedResource(data.url, { staleTime: 30_000 }) inside the island has its data immediately. seed(key, data) also puts a mutation response straight into the cache.
watch()#
explicit dependency watching with debounce
offlineResource()#
alias of resource(url, { offline: true }): IndexedDB cache, one mutation queue per app flushed on reconnect with retry/backoff and Background Sync, send() for network writes