Documentation menu

@gyral/core

Gyral core: Model-View-Intent web components on the web platform, with their own view layer.

npm install @gyral/core

Writing code? Start from working code, not from this page. Recipes has a task list with filters and a signup form, with their tests; Forms opens with a complete validated form; Getting started builds a first component. Adapt the closest example and check it, then look a name up here when you are about to call it.

Generated from the package's type declarations. Entry points: @gyral/core, @gyral/core/storage, @gyral/core/indexeddb, @gyral/core/vite, @gyral/core/eslint.

@gyral/core

Functions

Name Summary
call A command that calls method with args on the first element matching selector inside this component (its shadow root, or its own children in light-DOM mode) once the current update has rendered:
changed Narrows a StoreChanged message to one store's typed state, or undefined.
child Intent parser for a child component's outputs, typed by the child's output union.
command Builds a command whose mappers are typed by the driver's input, output and error.
componentsReady Resolves once every rendered tag the component loader knows (in the document and in the shadow roots of components it defined) is defined, and the page has settled.
css Component CSS: css`p { margin-block: ${GAP}px; }`.
define Compiles a Model-View-Intent spec into a custom element and registers it under tag.
defineDriver Defines a driver.
defineForm
defineHook Defines an element hook: a small behaviour attached to the element it sits on.
defineStore Defines a store.
each A keyed list.
field Intent parser for one control (pair with data-intent-on="input" for live checks).
fieldErrors Groups issues by field name.
focus A command that focuses the first element matching selector inside this component (its shadow root, or its own children in light-DOM mode) once the current update has rendered:
form Intent parser for a <form data-intent> submission, validated by a schema.
html The template tag: html`<p>${s.text}</p>`.
intentsOf A component's intent names as a module-level constant, so pure list rows can name intents without passing them through pick (rows must be pure).
invalid Mirrors model errors to native validity: setCustomValidity and aria-invalid.
keyed Renders value, and replaces it with a fresh copy whenever key changes: a form cleared after sending, a CSS animation restarted, a child component reset.
outputs emit, typed by a component's output union (the 4th define type parameter), so an output of the wrong shape fails to compile.
provideDrivers Makes element a driver provider for every Gyral component below it (including inside shadow roots).
raw Trusted markup from your own code (Markdown output, JSON-LD), parsed in a child hole and re-parsed only when the string changes.
retry The same driver (same name, so substitution by name still works), with run retried when it rejects: up to policy.times more attempts, after delayMs (doubling from it with backoff: 'exponential'; a random part of it with jitter), and only for failures retryIf accepts.
send A command that sends msg to the component's instance of store.
settled Resolves once the page is quiet: every connected Gyral host has rendered its latest state, view-transition updates have run, focus commands and outputs have been delivered, and messages have stopped arriving (a stream's next value, already on its way in a microtask, is waited for; long-lived commands themselves are not).
subscription A streaming driver over a source Gyral doesn't own.
svg The SVG fragment tag: svg`<path d=${s.d} />`.

call

function call(selector: string, method: string, ...args: readonly unknown[]): Command<never>;

A command that calls method with args on the first element matching selector inside this component (its shadow root, or its own children in light-DOM mode) once the current update has rendered:

Start: (s) => [{ ...s, playing: true }, [call('video', 'play')]]
Join: (s, m) => [s, [call('room-stage', 'join', m.room)]]

args must be plain data (structured-cloneable), so the command stays data a test can compare. A missing element or method, or a method that throws or rejects, is reported through the error channel (ADR 0024); the error's cause says which.

changed

function changed<S>(store: StoreRef<S>, msg: StoreChanged): {
    readonly state: S;
    readonly prev: S;
} | undefined;

Narrows a StoreChanged message to one store's typed state, or undefined.

child

function child<O extends Tagged, E extends Element, M>(source: ChildSource<O, E>, toMsg: (output: O, el: E) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;

Intent parser for a child component's outputs, typed by the child's output union. el is the child element, for reading its props (an item id, for example).

Pass () => Child when the class isn't defined yet, e.g. a component that contains itself; annotate the constant's type so TypeScript accepts the self-reference:

const Folder: GyralElementClass<State, Msg, Props, Out> = define()('x-folder', {
  intent: { Child: child(() => Folder, (out, el) => …) }, …
});

command

function command<I, O, E, const MS = never, const MF = MS>(driver: Driver<I, O, E>, input: I, handlers?: CommandHandlers<O, E, MS, MF>): Command<MS | MF>;

Builds a command whose mappers are typed by the driver's input, output and error. Without handlers (or without onSuccess) it answers with no message: command(log, line) is a Command<never>, which fits any Command<Msg>.

componentsReady

function componentsReady(): Promise<void>;

Resolves once every rendered tag the component loader knows (in the document and in the shadow roots of components it defined) is defined, and the page has settled. Await it in browser tests and page scripts after the loader has run (page scripts placed after it; in tests, import virtual:gyral/components first).

css

function css(strings: TemplateStringsArray, ...values: readonly CssValue[]): StyleSource;

Component CSS: css`p { margin-block: ${GAP}px; }`. Strings and numbers are inserted as written, so values from your own constants need no wrapper.

define

function define(): <S, M extends Tagged = never, P extends object = object, N extends string = never>(tag: string, spec: ComponentSpec<S, M, P, N>) => GyralElementClass<S, M, P, never, N>;
function define<S, M extends Tagged = never, P extends object = object, O extends Tagged = never>(): Definer<S, M, P, O>;

Compiles a Model-View-Intent spec into a custom element and registers it under tag. Two calls (ADR 0023): the first takes the types, the second the tag and spec, from whose intent keys it infers the component's intent names.

define<State, Msg>()('x-counter', { init, intent: { Increment: … }, update, view })

With no type arguments, define()('x-badge', spec) infers the state, messages, props and intent names from the spec. Guide: https://gyral.dev/docs/components/

defineDriver

function defineDriver<I, O, E = unknown>(spec: Driver<I, O, E> | DriverToken<E> | string): Driver<I, O, E>;

Defines a driver. Given an implementation (run), it returns it as is, so literals infer I, O and E. Given only a name, it returns a typed token: commands built from it are typed, and it runs only when a provider (provideDrivers) or el.drivers supplies a driver of that name. A command whose token nothing provides fails with G0043, naming the driver.

defineForm

function defineForm<Schema extends StandardSchemaV1>(schema: Schema, options?: FormOptions<Schema>): FormDefinition<Schema>;

defineHook

function defineHook<A extends readonly unknown[]>(spec: HookSpec<A>): (...args: A) => HookResult<A>;

Defines an element hook: a small behaviour attached to the element it sits on.

defineStore

function defineStore<S, M extends Tagged>(name: string, spec: StoreSpec<S, M>): Store<S, M>;

Defines a store. name keys it in registries, seeds and overrides, so keep it unique.

each

function each<T>(items: readonly T[], key: (item: T) => string | number, row: (item: T) => ChildValue): ListResult;
function each<T, P>(items: readonly T[], key: (item: T) => string | number, row: (item: T, picked: P) => ChildValue, pick: (item: T) => P): ListResult;

A keyed list. key must give each item a unique string or number; row must depend only on its arguments: a row re-renders only when its item object or its pick result changes.

field

function field<Schema extends StandardSchemaV1, M>(schema: Schema, toMsg: (value: Out<Schema>) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;

Intent parser for one control (pair with data-intent-on="input" for live checks). Validates checked for checkboxes and radios, otherwise value.

fieldErrors

function fieldErrors(issues: readonly FieldIssue[]): Readonly<Record<string, readonly string[]>>;

Groups issues by field name. Call it in the IntentRejected reducer and keep the result in state: invalid() re-applies an error only when it gets a new array.

focus

function focus(selector: string, options?: FocusOptions): Command<never>;

A command that focuses the first element matching selector inside this component (its shadow root, or its own children in light-DOM mode) once the current update has rendered:

NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]

Non-focusable targets (a heading) need tabindex="-1" in the view.

form

function form<Schema extends StandardSchemaV1, M>(definition: FormDefinition<Schema> | Schema, toMsg: (data: Out<Schema>, formData: FormData) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;

Intent parser for a <form data-intent> submission, validated by a schema. toMsg also gets the raw FormData, for sending the submission to the server (submitForm in @gyral/http) after it passed client-side validation.

html

const html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult

The template tag: html`<p>${s.text}</p>`.

intentsOf

function intentsOf<C extends {
    readonly intentNames?: string;
}>(): IntentNames<NonNullable<C['intentNames']>>;

A component's intent names as a module-level constant, so pure list rows can name intents without passing them through pick (rows must be pure). The same object the view gets as i, typed by the component's class (ADR 0023). A row that names intents this way needs an explicit return type, or the row, the view and the class infer each other in a circle:

const i = intentsOf<typeof TodoList>();
const Row = (t: Todo): TemplateResult => html`<input value=${t.id} data-intent=${i.Toggle} />`;

invalid

const invalid: (errors?: Errors) => HookResult<[errors?: Errors]>

Mirrors model errors to native validity: setCustomValidity and aria-invalid.

<input name="email" ${invalid(s.errors.email)} />

The custom error clears when the control fires input or change (the user edited it), when the bound errors become undefined or empty (the model cleared them), or when a submit finds the error stale (the value changed without an event, e.g. set from code): then the error is dropped and the form is submitted again once. An error is applied again only when a new value arrives (keep fieldErrors() results in state): client runs only when the errors change.

keyed

function keyed(key: string | number, value: ChildValue): ListResult;

Renders value, and replaces it with a fresh copy whenever key changes: a form cleared after sending, a CSS animation restarted, a child component reset.

outputs

function outputs<O extends Tagged>(): (output: O) => Command<never>;

emit, typed by a component's output union (the 4th define type parameter), so an output of the wrong shape fails to compile. A module-level constant, like intentsOf():

type Out = { readonly _tag: 'Picked'; readonly id: number };
const emit = outputs<Out>();
// in update: Pick: (s, m) => [s, [emit({ _tag: 'Picked', id: m.id })]]

Costs nothing at run time: it returns emit itself.

provideDrivers

function provideDrivers(element: Element, drivers: DriverOverrides): () => void;

Makes element a driver provider for every Gyral component below it (including inside shadow roots). Returns a function that removes the overrides. Use it on a test container or an app root; a <gyral-drivers> element with a .drivers property works the same way.

raw

function raw(html: string): RawResult;

Trusted markup from your own code (Markdown output, JSON-LD), parsed in a child hole and re-parsed only when the string changes. Never pass user input.

retry

function retry<I, O, E>(driver: Driver<I, O, E>, policy: RetryPolicy<E>): Driver<I, O, E>;

The same driver (same name, so substitution by name still works), with run retried when it rejects: up to policy.times more attempts, after delayMs (doubling from it with backoff: 'exponential'; a random part of it with jitter), and only for failures retryIf accepts. An abort (the command switched away, the component disconnected) ends it at once and is never retried. Wrap where the driver is chosen: provideDrivers at app setup, el.drivers, or a test fake.

const api = retry(makeHttpDriver({ baseUrl: '/api', timeoutMs: 8000 }), {
  times: 3, delayMs: 300, backoff: 'exponential', jitter: true, retryIf: retryableHttpError,
});
const feed = retry(subscription('feed', connect), { times: Infinity, delayMs: 1000 });

send

function send<S, M extends Tagged>(store: Store<S, M>, msg: NoInfer<M>): Command<never>;

A command that sends msg to the component's instance of store. Writes are commands.

settled

function settled(): Promise<void>;

Resolves once the page is quiet: every connected Gyral host has rendered its latest state, view-transition updates have run, focus commands and outputs have been delivered, and messages have stopped arriving (a stream's next value, already on its way in a microtask, is waited for; long-lived commands themselves are not). Rejects with the loop guard's error when rendering never settles in development (components feeding each other).

el.send({ _tag: 'Increment' });
await settled();

subscription

function subscription<O, I = undefined, E = unknown>(name: string, subscribe: (emit: (value: O) => void, ctx: SubscriptionContext<I>) => Unsubscribe, options?: SubscriptionOptions<E>): Driver<I, O, E>;

A streaming driver over a source Gyral doesn't own. subscribe starts listening, calls emit with each value (it may emit the current value at once) and returns how to stop (a function, or an object with unsubscribe()). The subscription lasts while the command runs; aborting it (lane switch, disconnect) releases the source, and emits after that are ignored. Substitute it by name in tests.

const cart = subscription<CartState>('cart', (emit) => {
  emit(reduxStore.getState());
  return reduxStore.subscribe(() => emit(reduxStore.getState()));
});
// subscriptions: () => [command(cart, undefined, { onSuccess: (c) => ({ _tag: 'Cart', c }) })]

svg

const svg: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult

The SVG fragment tag: svg`<path d=${s.d} />`. Its top level is SVG content, so it renders only inside an <svg> (or another SVG element) of an html template. A whole <svg> graphic is written with html.

Classes

Name Summary
GyralError A failure Gyral caught, with its context; cause is what was thrown.

GyralError

class GyralError extends Error {
  readonly name = "GyralError";
  readonly component: string | undefined;
  readonly phase: ErrorPhase;
  readonly msg: string | undefined;
  constructor(phase: ErrorPhase, text: string, cause: unknown, component?: string, msg?: string);
}

A failure Gyral caught, with its context; cause is what was thrown.

Constants

Name Summary
nothing Renders nothing in a child hole; removes the attribute in an attribute hole.
OUTPUT_EVENT Event a child component dispatches on its host to send an output up (ADR 0010); public.
prop The prop builders.

nothing

const nothing: typeof nothing

Renders nothing in a child hole; removes the attribute in an attribute hole.

OUTPUT_EVENT

const OUTPUT_EVENT: "gyral-output"

Event a child component dispatches on its host to send an output up (ADR 0010); public.

prop

const prop: { readonly string: Scalar<string>; readonly number: Scalar<number>; readonly boolean: <O extends boolean = boolean>(opts?: (Common & { readonly schema?: Schema<O> | undefined; } & { readonly default?: NoInfer<O> | undefined; }) | undefined) => Prop<O>; readonly json: WithSchema; readonly value: WithSchema; }

The prop builders.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
AnyDriver Any driver, with its input type erased.
AnyStore Any store, with its types erased (for declarations and registries).
AnyStoreInstance Any store instance, with its types erased.
CallInput The input of a call() command.
ChildSource A child class, or a function returning it (for recursive or later-defined components).
ChildValue Everything a child hole accepts.
Command A side effect described as data, plus pure mappers from its outcome to messages.
CommandHandlers How a command's outcome becomes a message.
CommandInfo
ComponentSpec A component spec.
Concurrency How commands in the same lane interact.
CssValue A css interpolation: inserted as written (another css value inserts its text).
Ctx Read-only context handed to every reducer and to the view (ADR 0007, 0013).
Definer The second call of define(): takes the tag and the spec, infers the intent names.
Driver Performs one kind of side effect.
DriverContext
DriverOverrides Drivers substituted by name (test fakes, configured instances): el.drivers, provideDrivers.
DriverToken A driver with no implementation of its own (defineDriver<I, O, E>('session')): commands built from it are typed by I, O and E, and it runs only when provideDrivers or el.drivers supplies a driver of that name (ADR 0006 addendum "Driver tokens").
Errored Framework message (ADR 0024): an update, an intent parser or a command of this component failed.
ErrorPhase Where a failure happened.
FieldIssue One validation problem.
FieldName The definition's field names, when its input is an object; otherwise any string.
FocusOptions
FormAssociation Pure model projections applied by Gyral to the host's ElementInternals (ADR 0008).
FormControlValidity Native constraint flags and a validation bubble anchor in the component's render root.
FormControlValue A native submitted value, or null to omit this component from the submission.
FormDefinition A form's schema, shared by the client component and (with @gyral/ssr) the server route.
FormDisabled Framework message: the host or its fieldset changed the effective disabled state.
FormFields Submitted text values of a form (Files are dropped: they can't be re-filled).
FormOptions
FormReset Framework message: the owning native form was reset.
FormResult
FormStateRestore Framework message: native session restoration or autofill supplied control state.
FormValue
FormValues A form's submitted text values, keyed by the definition's fields, for re-rendering the form with what was typed (sticky fields).
GyralElement The custom element define() produces: a plain HTMLElement.
GyralElementClass
Head A page's head: what renderPage() writes on the server and setHead() applies on the client.
HeadLink A metadata <link>: alternate (with hreflang), icon (with sizes, type), manifest (with crossorigin), prev/next, … Any other attribute is written as given.
HeadMeta <meta name content>, or <meta property content> (Open Graph uses property).
HookAttributes Attributes a hook's server half adds to the start tag (true: present, no value).
HookResult What a hook returns in a template: <input ${invalid(errors)}>.
HookSpec
Hydrated Framework message: the component is live in the browser (ADR 0012 addendum).
HydrateStrategy When a server-rendered component hydrates.
IntentInput What the intent layer sees when an element's intent fires.
IntentNames A component's intent names, the keys of its intent object, as the view's i and as intentsOf<typeof C>(), so data-intent=${i.Increment} is checked (ADR 0023).
IntentParser Parses a platform event into one message variant, IntentRejected, or undefined to decline it: a synchronous undefined passes the event to the next intent outward for the same event.
IntentRejected Framework message: an intent's input failed schema validation (ADR 0008).
Intents Intent parsers keyed by intent name (ADR 0001, ADR 0023).
JsonValue A JSON value: what a JSON-LD block holds.
ListResult A keyed list for a child hole: what each(…) returns.
Next What a reducer (or init) returns: new state, optionally with commands to run.
OutputEvent The gyral-output event a child dispatches on its host for each output (ADR 0010): detail is the output.
OutputsOf The output union of a define() class: OutputsOf<typeof Item>.
OutputSource Anything child() can read an output type from: a define() class.
ParserCtx What an intent parser gets besides the input: the reducers' context plus the component's current state, read-only.
ParseResult What an intent parser returns: a message, IntentRejected (invalid input), or undefined to decline the event.
ParserFor The parser for intent name K (ADR 0023): a name that is a message tag parses into that variant; any other name is an intent of its own whose parser may produce any message.
Prop One declared prop: what a builder returns.
PropDeclarations The prop builders for props type P: one prop.* builder per key.
PropGuard A plain type guard, accepted by prop.value and prop.json in place of a schema.
PropKind
PropsChanged Framework message: declared props changed after the first render (ADR 0007).
PropsOf The props type of a table of builders: PropsOf<typeof props>.
RawResult Trusted markup for a child hole: what raw(html) returns.
Reducer One entry of update: takes the state, one message (Msg, a variant of M or a framework message) and the read-only context, and returns the next state, optionally with commands that produce more M messages.
RetryPolicy How retry(driver, policy) retries a rejected run (retry.ts).
SeedCheck The result of checking a seed against a store's schema.
ShadowOption spec.shadow: true (the default) or an object for a shadow root, false for light DOM.
StandardSchemaV1 The Standard Schema interface.
Stateless State of a component that keeps none (a pure view of its props).
Store
StoreChanged Framework message: a store this component reads changed (ADR 0013).
StoreInstance A live store: one per page on the client, one per request on the server, one per test.
StoreOverrides Store instances substituted by store name (tests, islands).
StoreReader Reads a declared store's current state, typed by the store (ADR 0013).
StoreRef What ctx.read() accepts: anything that names a store and carries its state type.
StoreResolver Finds a store's instance in a scope (a registry).
StoreSpec
StoreUpdate One pure reducer per message tag, like a component's update but without context.
Styles What spec.styles accepts: css values, plain CSS strings (e.g.
StyleSource What css returns: CSS text, mapped to one shared CSSStyleSheet in the browser.
SubscriptionContext
SubscriptionOptions Driver options a subscription may set.
Tagged Every message is a tagged object; the tag is also the intent name used in markup.
TemplateResult What html returns: a template plus this render's values.
Unsubscribe How to stop listening: a function, or an object with unsubscribe() (RxJS, XState).
Update One pure reducer per message tag (exhaustive by construction), plus optional reducers for framework messages.
Variant The member of message union M whose tag is K: Variant<Msg, 'Add'>.

AnyDriver

type AnyDriver = Driver<never, unknown>;

Any driver, with its input type erased. Every Driver<I, O, E> (from defineDriver, subscription, a test fake) is assignable to it as is: no cast.

AnyStore

interface AnyStore {
    readonly name: string;
    instance(initial?: never): AnyStoreInstance;
    checkSeed(seed: unknown): SeedCheck<unknown>;
}

Any store, with its types erased (for declarations and registries).

AnyStoreInstance

interface AnyStoreInstance {
    readonly store: AnyStore;
    readonly state: unknown;
    send(msg: never): void;
    subscribe(listener: (state: unknown, prev: unknown) => void): () => void;
    dispose(): void;
    /**
     * Called by the registry that holds this instance, so the store's `send(other, msg)`
     * commands reach `other`'s instance in the same scope.
     */
    bindScope?(resolve: StoreResolver): void;
}

Any store instance, with its types erased.

CallInput

interface CallInput {
    readonly selector: string;
    readonly method: string;
    readonly args: readonly unknown[];
}

The input of a call() command.

ChildSource

type ChildSource<O extends Tagged, E extends Element> = OutputSource<O, E> | (() => OutputSource<O, E>);

A child class, or a function returning it (for recursive or later-defined components).

ChildValue

type ChildValue = string | number | boolean | null | undefined | typeof nothing | TemplateResult | ListResult | RawResult | readonly ChildValue[];

Everything a child hole accepts.

Command

interface Command<M> {
    readonly driver: AnyDriver;
    readonly input: unknown;
    /** Concurrency lane. Default: the driver's name. */
    readonly key?: string;
    readonly concurrency?: Concurrency;
    readonly onSuccess: (output: unknown) => M | undefined;
    /** If omitted, a failure is reported to the error channel (ADR 0024, G0041). */
    readonly onFailure?: (error: unknown) => M | undefined;
}

A side effect described as data, plus pure mappers from its outcome to messages. Commands that answer with no message (focus(), call(), navigate()) are Command<never>, which fits wherever a Command<Msg> is expected: no type argument needed.

CommandHandlers

interface CommandHandlers<O, E, MS, MF = MS> {
    readonly onSuccess?: (output: O) => MS | undefined;
    readonly onFailure?: (error: E) => MF | undefined;
    readonly key?: string;
    readonly concurrency?: Concurrency;
}

How a command's outcome becomes a message. Every field is optional: without onSuccess the command answers with nothing (Command<never>); without onFailure a failure is reported to the error channel (ADR 0024).

CommandInfo

interface CommandInfo {
    /** The command, e.g. `--clear`. */
    readonly command: string;
    /** The invoker (the button) that sent it. */
    readonly source: Element | undefined;
}

ComponentSpec

type ComponentSpec<S, M extends Tagged, P, N extends string = string> = SpecBody<S, M, P, N> & IntentField<S, M, P, N> & UpdateField<S, M, P> & InitField<S, M, P>;

A component spec. N is the component's intent names, the keys of intent (ADR 0023).

Concurrency

type Concurrency = 'merge' | 'switch' | 'exhaust' | 'queue';

How commands in the same lane interact. See ADR 0006 for the table.

CssValue

type CssValue = string | number | StyleSource;

A css interpolation: inserted as written (another css value inserts its text).

Ctx

interface Ctx<P> {
    /** Props from above. */
    readonly props: P;
    /** Stores from the side: `read(cart).lines`. The store must be in `spec.stores`. */
    readonly read: StoreReader;
}

Read-only context handed to every reducer and to the view (ADR 0007, 0013).

Definer

type Definer<S, M extends Tagged, P extends object, O extends Tagged> = <N extends string = never>(tag: string, spec: ComponentSpec<S, M, P, N>) => GyralElementClass<S, M, P, O, N>;

The second call of define(): takes the tag and the spec, infers the intent names.

Driver

interface Driver<I, O, E = unknown> {
    readonly name: string;
    readonly run: (input: I, ctx: DriverContext<O>) => O | Promise<O>;
    /** Default policy for this driver's commands. Default `'merge'`. */
    readonly concurrency?: Concurrency;
    /** Turns a thrown or rejected value into this driver's typed error. */
    readonly toError?: (cause: unknown) => E;
}

Performs one kind of side effect. A plain object: easy to fake in tests.

DriverContext

interface DriverContext<O = unknown> {
    /** Aborts when the command is switched away or its component disconnects. */
    readonly signal: AbortSignal;
    /**
     * Delivers an extra result while the command runs (streaming drivers: routers, timers,
     * sockets). Each value goes through the command's `onSuccess`. Ignored once the command
     * has settled or been aborted. A streaming `run` usually never resolves; abort ends it.
     */
    readonly emit: (output: O) => void;
}

DriverOverrides

type DriverOverrides = Readonly<Record<string, AnyDriver>>;

Drivers substituted by name (test fakes, configured instances): el.drivers, provideDrivers. Annotate a driver map with this type (or Record<string, AnyDriver>), not Record<string, Driver<unknown, unknown>>, which rejects drivers with a typed input.

DriverToken

interface DriverToken<E = unknown> {
    readonly name: string;
    readonly concurrency?: Concurrency;
    readonly toError?: (cause: unknown) => E;
}

A driver with no implementation of its own (defineDriver<I, O, E>('session')): commands built from it are typed by I, O and E, and it runs only when provideDrivers or el.drivers supplies a driver of that name (ADR 0006 addendum "Driver tokens").

Errored

interface Errored {
    readonly _tag: 'Errored';
    readonly phase: 'update' | 'parse' | 'command';
    readonly error: GyralError;
}

Framework message (ADR 0024): an update, an intent parser or a command of this component failed. The failure was reported (reportError, or claimed by an ancestor's boundary) and changed nothing; reduce it to show an error state or to reset. A failure while reducing Errored is reported and not sent again.

ErrorPhase

type ErrorPhase = 'init' | 'update' | 'view' | 'parse' | 'command' | 'hook' | 'store' | 'subscribe';

Where a failure happened.

FieldIssue

interface FieldIssue {
    readonly path: string;
    readonly message: string;
}

One validation problem. path is dot-joined and matches the field's name.

FieldName

type FieldName<Schema extends StandardSchemaV1> = In<Schema> extends Readonly<Record<string, unknown>> ? keyof In<Schema> & string : string;

The definition's field names, when its input is an object; otherwise any string.

FocusOptions

interface FocusOptions {
    /** Don't scroll the element into view (default `false`: the browser scrolls if needed). */
    readonly preventScroll?: boolean;
    /** Also select the text of an input or textarea. */
    readonly select?: boolean;
    /**
     * If nothing matches after this render, keep the request and focus the target as soon as a
     * later render of this component produces it; a newer `focus()` from the component replaces
     * it, and after one second it gives up with the usual warning. `settled()` doesn't wait for it.
     */
    readonly wait?: boolean;
}

FormAssociation

interface FormAssociation<S, P> {
    readonly value: (state: S, ctx: Ctx<P>) => FormControlValue;
    readonly state?: (state: S, ctx: Ctx<P>) => FormControlValue;
    readonly validity?: (state: S, ctx: Ctx<P>) => FormControlValidity | undefined;
}

Pure model projections applied by Gyral to the host's ElementInternals (ADR 0008).

FormControlValidity

interface FormControlValidity {
    readonly flags: ValidityStateFlags;
    readonly message: string;
    readonly anchor?: string;
}

Native constraint flags and a validation bubble anchor in the component's render root.

FormControlValue

type FormControlValue = string | File | FormData | null;

A native submitted value, or null to omit this component from the submission.

FormDefinition

interface FormDefinition<Schema extends StandardSchemaV1> {
    readonly _tag: 'FormDefinition';
    readonly schema: Schema;
    /** The fields listed as secret (names containing `password`, `passcode`, `csrf` or `xsrf` are secret too). */
    readonly secret: readonly string[];
    /**
     * Validates submitted form data exactly as `form()` does; the server's `formAction` uses it,
     * so both paths produce the same `IntentRejected` (`intent` defaults to `''`).
     */
    readonly validate: (data: FormData, intent?: string) => FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>>;
    /** The submitted text values without secrets: what a re-rendered form may show again. */
    readonly values: (data: FormData) => FormValues<Schema>;
}

A form's schema, shared by the client component and (with @gyral/ssr) the server route.

FormDisabled

interface FormDisabled {
    readonly _tag: 'FormDisabled';
    readonly disabled: boolean;
}

Framework message: the host or its fieldset changed the effective disabled state.

FormFields

type FormFields = Readonly<Record<string, string | readonly string[]>>;

Submitted text values of a form (Files are dropped: they can't be re-filled).

FormOptions

interface FormOptions<Schema extends StandardSchemaV1> {
    /**
     * Fields whose submitted values are never sent back: not in `IntentRejected.values`, not in
     * `values()`, not in the devtools timeline. Any posted field name may be listed, including
     * fields outside the schema. Names containing `password`, `passcode`, `csrf` or `xsrf` are
     * secret without being listed (the browser never restores a password field either, and a
     * CSRF token must come fresh from the server, not from the echo).
     */
    readonly secret?: readonly (FieldName<Schema> | (string & {}))[];
}

FormReset

interface FormReset {
    readonly _tag: 'FormReset';
}

Framework message: the owning native form was reset.

FormResult

type FormResult<T> = {
    readonly ok: true;
    readonly data: T;
} | {
    readonly ok: false;
    readonly rejected: IntentRejected;
};

FormStateRestore

interface FormStateRestore {
    readonly _tag: 'FormStateRestore';
    readonly state: FormControlValue;
    readonly mode: 'restore' | 'autocomplete';
}

Framework message: native session restoration or autofill supplied control state.

FormValue

type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];

FormValues

type FormValues<Schema extends StandardSchemaV1> = In<Schema> extends Readonly<Record<string, unknown>> ? {
    readonly [K in keyof In<Schema> & string]?: string | readonly string[];
} : FormFields;

A form's submitted text values, keyed by the definition's fields, for re-rendering the form with what was typed (sticky fields). The same shape as IntentRejected.values: repeated names (and name[]) are arrays; Files and secret fields are left out.

GyralElement

interface GyralElement<S, M extends Tagged> extends HTMLElement {
    /** Current model state. */
    readonly state: S;
    /** Feeds a message through `update`, as if an intent had produced it. */
    send(msg: M): void;
    /** Per-instance driver substitutions by name (test fakes). Checked before the spec's. */
    drivers: DriverOverrides;
    /**
     * Per-instance store instances by store name (tests, islands). Checked before the nearest
     * `<gyral-stores>` provider and the document default (ADR 0013).
     */
    stores: StoreOverrides;
    /**
     * Messages applied through `update` right after `init`, before the first render. The server
     * uses it to render a rejected form with the same reducer as the JS path (ADR 0008). Ignored
     * when the element resumes from a hydration seed (the seed already contains their effect).
     */
    initialMessages: readonly Tagged[];
}

The custom element define() produces: a plain HTMLElement. Tests wait for rendering with settled().

GyralElementClass

interface GyralElementClass<S, M extends Tagged, P, O extends Tagged = never, N extends string = string> {
    /** Instances expose their declared props as settable properties. */
    new (): GyralElement<S, M> & {
        -readonly [K in keyof P]: P[K];
    };
    readonly spec: ComponentSpec<S, M, P, N>;
    readonly tagName: string;
    /** Native custom-element registration flag, true when the spec opts into forms. */
    readonly formAssociated?: boolean;
    /** Type-only: the outputs this component emits (read by `child()`). */
    readonly outputs?: O;
    /** Type-only: the component's intent names, the keys of `intent` (read by `intentsOf()`). */
    readonly intentNames?: N;
}

Head

interface Head {
    readonly title: string;
    readonly description?: string;
    /** Absolute URL, usually `new URL(match.path, origin).href` (ADR 0009 "Canonical paths"). */
    readonly canonical?: string;
    /** `<meta name="robots">`, e.g. `'noindex, follow'`. */
    readonly robots?: string;
    /** Later entries win over earlier ones with the same name or property. */
    readonly meta?: readonly HeadMeta[];
    /** Later entries win over earlier ones with the same attributes other than `href`. */
    readonly links?: readonly HeadLink[];
    /** Structured data, each written as one `<script type="application/ld+json">`. */
    readonly jsonLd?: readonly JsonValue[];
    /** `<html lang>`. */
    readonly lang?: string;
    /** `<html dir>`. */
    readonly dir?: 'ltr' | 'rtl' | 'auto';
}

A page's head: what renderPage() writes on the server and setHead() applies on the client.

interface HeadLink {
    readonly rel: string;
    readonly href: string;
    readonly [attribute: string]: string;
}

A metadata <link>: alternate (with hreflang), icon (with sizes, type), manifest (with crossorigin), prev/next, … Any other attribute is written as given.

HeadMeta

type HeadMeta = {
    readonly name: string;
    readonly content: string;
    readonly media?: string;
} | {
    readonly property: string;
    readonly content: string;
    readonly media?: string;
};

<meta name content>, or <meta property content> (Open Graph uses property). media makes one entry per media query (theme-color for light and dark).

HookAttributes

type HookAttributes = Readonly<Record<string, string | true>>;

Attributes a hook's server half adds to the start tag (true: present, no value).

HookResult

interface HookResult<A extends readonly unknown[] = readonly unknown[]> {
    readonly [HOOK]: HookSpec<A>;
    readonly [COMMIT_HOOK]: (part: HookPart, result: HookResult) => void;
    readonly args: A;
}

What a hook returns in a template: <input ${invalid(errors)}>.

HookSpec

interface HookSpec<A extends readonly unknown[]> {
    /** Attributes for the server-rendered start tag. */
    server?(args: A): HookAttributes;
    /** Runs after the commit when `args` changed; `prev` is undefined the first time. */
    client(el: Element, args: A, prev: A | undefined): void;
    /**
     * When `client` runs again: `'args'` (the default) when an argument changed (`Object.is`),
     * `'always'` on every render that commits the hook's position (a skipped `each` row doesn't).
     */
    readonly update?: 'args' | 'always';
    /**
     * Teardown, with the last arguments `client` got: runs
     * when Gyral removes the element, when the position stops holding this hook, or when the host
     * disconnects (not on moves). After a host reconnects, `client` runs again with `prev`
     * undefined.
     */
    dispose?(el: Element, args: A): void;
}

Hydrated

interface Hydrated {
    readonly _tag: 'Hydrated';
    /** `true` when the instance resumed from server-rendered markup (a hydration seed). */
    readonly serverRendered: boolean;
}

Framework message: the component is live in the browser (ADR 0012 addendum). Sent once, after the first client render has completed, to every client-side instance. Use it for progressive enhancement: render the no-JS form on the server and in the first client render (so hydration matches), then switch to the enhanced UI in the Hydrated reducer.

HydrateStrategy

type HydrateStrategy = 'load' | 'idle' | 'visible' | 'interaction';

When a server-rendered component hydrates. load (the default) means right away.

IntentInput

interface IntentInput {
    /** The intent's name (a message tag): the `data-intent-<event>` or `data-intent` value. */
    readonly name: string;
    readonly event: Event;
    /** The element carrying the intent attribute (`event.currentTarget` is Gyral's root). */
    readonly target: Element;
    /** `value` of the input, select, textarea or button that carries the intent. */
    readonly value: string | undefined;
    /** `checked` for checkbox and radio inputs. */
    readonly checked: boolean | undefined;
    /** Submitted data (including the submitter button) when the intent is on a `<form>`. */
    readonly formData: FormData | undefined;
    /**
     * `detail` of any `CustomEvent`: a Gyral child's output, or the event of another custom
     * element (a map's `marker-select`). Unknown until the parser checks it (a guard, a schema).
     */
    readonly detail: unknown;
    /** `KeyboardEvent.key` for `keydown`/`keyup` intents. */
    readonly key: string | undefined;
    /** For `toggle` intents (popover, `<details>`): the state it changed to. */
    readonly newState: 'open' | 'closed' | undefined;
    /** For `command` intents: the invoker's command (e.g. `--clear`) and source. */
    readonly command: CommandInfo | undefined;
}

What the intent layer sees when an element's intent fires.

IntentNames

type IntentNames<N extends string> = {
    readonly [K in N]: K;
};

A component's intent names, the keys of its intent object, as the view's i and as intentsOf<typeof C>(), so data-intent=${i.Increment} is checked (ADR 0023).

IntentParser

type IntentParser<M, P = unknown, S = unknown> = (input: IntentInput, ctx: ParserCtx<P, S>) => ParseResult<M> | Promise<ParseResult<M>>;

Parses a platform event into one message variant, IntentRejected, or undefined to decline it: a synchronous undefined passes the event to the next intent outward for the same event. May be async because schema validation may be; an async parser keeps the event. ctx is the read-only context reducers get (props as they are when the event fires, read(store)) plus state, the component's state at that moment; parsers that don't need it take one parameter.

IntentRejected

interface IntentRejected {
    readonly _tag: 'IntentRejected';
    /** The `data-intent` name whose input was rejected. */
    readonly intent: string;
    readonly issues: readonly FieldIssue[];
    /**
     * For `form()` rejections: what the user submitted, so the view can re-fill the form. On
     * the no-JS path (ADR 0008 server half) the server re-renders the page from this.
     */
    readonly values?: FormFields | undefined;
}

Framework message: an intent's input failed schema validation (ADR 0008).

Intents

type Intents<M extends Tagged, P = unknown, N extends string = string, S = unknown> = {
    readonly [K in N]: ParserFor<M, P, K, S>;
};

Intent parsers keyed by intent name (ADR 0001, ADR 0023). The keys are the component's intent names: define<State, Msg>()(tag, { intent: { Archive: …, Increment: … } }) gives the view i.Archive and i.Increment. A key that is a message tag must return that variant; any other key may return any message (several controls that change one thing can also share one intent and tell themselves apart by name). Messages from drivers need no parser.

JsonValue

type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
    readonly [key: string]: JsonValue;
};

A JSON value: what a JSON-LD block holds.

ListResult

interface ListResult {
    readonly [EACH]: (part: ChildPart, list: ListResult) => void;
    readonly items: readonly unknown[];
    readonly key: (item: unknown) => unknown;
    readonly row: (item: unknown, picked: unknown) => unknown;
    readonly pick: ((item: unknown) => unknown) | undefined;
}

A keyed list for a child hole: what each(…) returns.

Next

type Next<S, M> = S | readonly [S, ReadonlyArray<Command<M | IntentRejected>>];

What a reducer (or init) returns: new state, optionally with commands to run. Commands may also answer with the framework message IntentRejected (e.g. submitForm in @gyral/http when the server rejects a form), which goes to the optional IntentRejected reducer.

OutputEvent

type OutputEvent<O extends Tagged = Tagged> = CustomEvent<O>;

The gyral-output event a child dispatches on its host for each output (ADR 0010): detail is the output. Bubbles, not composed. A non-Gyral parent listens for OUTPUT_EVENT. Only the direct parent's intent on the child host takes it, and once it does the event stops there.

OutputsOf

type OutputsOf<C> = C extends {
    readonly outputs?: infer O;
} ? Exclude<O, undefined> : never;

The output union of a define() class: OutputsOf<typeof Item>.

OutputSource

interface OutputSource<O extends Tagged, E extends Element> {
    new (): E;
    /** Type-only marker for the component's output union. Never set at runtime. */
    readonly outputs?: O;
}

Anything child() can read an output type from: a define() class.

ParserCtx

interface ParserCtx<P, S = unknown> extends Ctx<P> {
    /** The state when the event fires. */
    readonly state: Readonly<S>;
}

What an intent parser gets besides the input: the reducers' context plus the component's current state, read-only. Parsers decide synchronously (preventDefault()), and where the user is often decides it: Tab leaves a grid only from its last cell.

ParseResult

type ParseResult<M> = M | IntentRejected | undefined;

What an intent parser returns: a message, IntentRejected (invalid input), or undefined to decline the event.

ParserFor

type ParserFor<M extends Tagged, P, K, S = unknown> = string extends K ? // The wide form (any intent name), which every specific spec is assignable to.
IntentParser<M, P, S> | true | TrueOnlyForFieldlessMessage : K extends M['_tag'] ? IntentParser<Variant<M, K>, P, S> | TagOnly<Variant<M, K>> : IntentParser<M, P, S>;

The parser for intent name K (ADR 0023): a name that is a message tag parses into that variant; any other name is an intent of its own whose parser may produce any message.

Prop

interface Prop<T> {
    readonly kind: PropKind;
    /** The attribute name, `false` for property only, `undefined` for the kebab-case default. */
    readonly attribute: string | false | undefined;
    /**
     * Refines (string/number/boolean) or defines (json/value) the accepted values: a Standard
     * Schema, or for json/value a type guard (props.ts `checkValue` tells them apart).
     */
    readonly schema: StandardSchemaV1 | PropGuard<unknown> | undefined;
    /** A missing value is a bug: warned once per instance at first render. */
    readonly required: boolean;
    /** Used whenever the element's value is missing (`boolean` props default to `false`). */
    readonly default?: unknown;
    /**
     * When a new value counts as unchanged (no re-render, no `PropsChanged`): `Object.is`, the
     * same JSON for `prop.json`, or the `equals` option of `prop.json`/`prop.value`.
     */
    readonly equals: (a: unknown, b: unknown) => boolean;
    /** Type-only: the value components see. Never set. */
    readonly [OUTPUT]?: () => T;
}

One declared prop: what a builder returns.

PropDeclarations

type PropDeclarations<P> = {
    readonly [K in keyof P]-?: Prop<P[K]>;
};

The prop builders for props type P: one prop.* builder per key. A builder's output type must fit the prop's type, so a prop whose type excludes undefined needs required: true or a default (the honesty rule, ADR 0007 addendum). Without explicit type arguments, define() infers P from the builders.

PropGuard

type PropGuard<T> = (value: unknown) => value is T;

A plain type guard, accepted by prop.value and prop.json in place of a schema.

PropKind

type PropKind = 'string' | 'number' | 'boolean' | 'json' | 'value';

PropsChanged

interface PropsChanged<P> {
    readonly _tag: 'PropsChanged';
    readonly props: P;
    readonly prev: P;
}

Framework message: declared props changed after the first render (ADR 0007).

PropsOf

type PropsOf<T extends Readonly<Record<string, Prop<unknown>>>> = {
    readonly [K in keyof T]: T[K] extends Prop<infer V> ? V : never;
};

The props type of a table of builders: PropsOf<typeof props>.

RawResult

interface RawResult {
    readonly [MARKUP]: (part: ChildPart, html: string) => void;
    readonly html: string;
}

Trusted markup for a child hole: what raw(html) returns.

Reducer

type Reducer<S, M, Msg, P = object> = (state: S, msg: Msg, ctx: Ctx<P>) => Next<S, M>;

One entry of update: takes the state, one message (Msg, a variant of M or a framework message) and the read-only context, and returns the next state, optionally with commands that produce more M messages. To type a reducer written outside the spec:

const add: Reducer<State, Msg, Variant<Msg, 'Add'>, object> = (s, m) => …

RetryPolicy

interface RetryPolicy<E = unknown> {
    /** Retries after the first failure. */
    readonly times: number;
    /** Delay before each retry (the base delay for exponential backoff). Default 0. */
    readonly delayMs?: number;
    readonly backoff?: 'fixed' | 'exponential';
    /** Full jitter: wait a random time between 0 and the computed delay, so clients spread out. */
    readonly jitter?: boolean;
    /**
     * Which failures to retry, given the driver's typed error (its `toError`, else the thrown
     * value). Default: every failure. For `@gyral/http`, `retryableHttpError`.
     */
    readonly retryIf?: (error: E) => boolean;
}

How retry(driver, policy) retries a rejected run (retry.ts). E: the driver's error.

SeedCheck

type SeedCheck<S> = {
    readonly ok: true;
    readonly state: S;
} | {
    readonly ok: false;
    readonly issues: readonly string[];
};

The result of checking a seed against a store's schema.

ShadowOption

type ShadowOption = boolean | {
    readonly delegatesFocus?: boolean;
};

spec.shadow: true (the default) or an object for a shadow root, false for light DOM. delegatesFocus: true: focusing the host, or clicking a non-focusable part of it, focuses its first focusable element, and :focus matches the host while focus is inside; the server writes shadowrootdelegatesfocus on the declarative shadow root.

StandardSchemaV1

interface StandardSchemaV1<Input = unknown, Output = Input> {
    /** The Standard Schema properties. */
    readonly "~standard": StandardSchemaV1.Props<Input, Output>;
}

The Standard Schema interface.

Stateless

type Stateless = Readonly<Record<string, never>>;

State of a component that keeps none (a pure view of its props). Its init is optional.

Store

interface Store<S, M extends Tagged> extends StoreRef<S> {
    readonly spec: StoreSpec<S, M>;
    /**
     * A fresh, independent instance. `initial` skips `init`'s state (a server seed, a test
     * fixture); `init`'s commands still run on the client.
     */
    instance(initial?: S): StoreInstance<S, M>;
    /** Checks a seed with `spec.schema` (ok as-is without one). */
    checkSeed(seed: unknown): SeedCheck<S>;
    /** Type-only marker for the message type. Never set at runtime. */
    readonly messageType?: (msg: M) => M;
}

StoreChanged

interface StoreChanged {
    readonly _tag: 'StoreChanged';
    /** The store's name. Narrow with `changed(store, msg)`. */
    readonly store: string;
    readonly state: unknown;
    readonly prev: unknown;
}

Framework message: a store this component reads changed (ADR 0013). Optional reducer.

StoreInstance

interface StoreInstance<S, M extends Tagged> {
    readonly store: Store<S, M>;
    readonly state: S;
    /** Feeds a message through the store's `update` and notifies subscribers on change. */
    send(msg: M): void;
    /** Called after every state change. Returns an unsubscribe function. */
    subscribe(listener: (state: S, prev: S) => void): () => void;
    /** Per-instance driver substitutions (test fakes). Checked before the spec's. */
    drivers: DriverOverrides;
    /** Stops running commands and drops subscribers. */
    dispose(): void;
    /** Set by the registry that holds the instance (store-to-store `send`). */
    bindScope(resolve: StoreResolver): void;
}

A live store: one per page on the client, one per request on the server, one per test.

StoreOverrides

type StoreOverrides = Readonly<Record<string, AnyStoreInstance>>;

Store instances substituted by store name (tests, islands).

StoreReader

type StoreReader = <S>(store: StoreRef<S>) => S;

Reads a declared store's current state, typed by the store (ADR 0013).

StoreRef

interface StoreRef<S> {
    readonly name: string;
    /** Type-only marker for the state type. Never set at runtime. */
    readonly stateType?: (state: S) => S;
}

What ctx.read() accepts: anything that names a store and carries its state type.

StoreResolver

type StoreResolver = (store: AnyStore) => AnyStoreInstance;

Finds a store's instance in a scope (a registry).

StoreSpec

interface StoreSpec<S, M extends Tagged> {
    /** Initial state (and optional commands, e.g. load a saved cart). */
    readonly init: () => Next<S, M>;
    readonly update: StoreUpdate<S, M>;
    /** Driver substitutions by name for this store's commands. */
    readonly drivers?: DriverOverrides;
    /**
     * Validates the server's page seed before the client uses it (any synchronous Standard
     * Schema). An invalid seed is reported with console.error and the store starts from `init`.
     */
    readonly schema?: StandardSchemaV1<unknown, S>;
}

StoreUpdate

type StoreUpdate<S, M extends Tagged> = {
    readonly [K in M['_tag']]: (state: S, msg: Variant<M, K>) => Next<S, M>;
};

One pure reducer per message tag, like a component's update but without context.

Styles

type Styles = StyleSource | string | readonly Styles[];

What spec.styles accepts: css values, plain CSS strings (e.g. import base from './base.css?inline') and arrays of them, nested freely. CSS is trusted author code: never interpolate user input.

StyleSource

interface StyleSource {
    readonly [STYLE]: true;
    readonly text: string;
}

What css returns: CSS text, mapped to one shared CSSStyleSheet in the browser.

SubscriptionContext

interface SubscriptionContext<I> {
    /** The command's input (a room code, a URL, a store key). */
    readonly input: I;
    /** Aborts when the command is switched away or its component disconnects. */
    readonly signal: AbortSignal;
    /**
     * Ends the subscription with an error: the source is released and the command's
     * `onFailure` gets it. Wrap the driver in `retry()` to subscribe again instead.
     */
    readonly fail: (error: unknown) => void;
}

SubscriptionOptions

interface SubscriptionOptions<E> {
    readonly concurrency?: Concurrency;
    readonly toError?: (cause: unknown) => E;
}

Driver options a subscription may set. concurrency defaults to 'switch'.

Tagged

interface Tagged {
    readonly _tag: string;
}

Every message is a tagged object; the tag is also the intent name used in markup.

TemplateResult

interface TemplateResult {
    readonly [SOURCE]: readonly string[] | TemplateObject;
    readonly values: readonly unknown[];
}

What html returns: a template plus this render's values.

Unsubscribe

type Unsubscribe = (() => void) | {
    readonly unsubscribe: () => void;
};

How to stop listening: a function, or an object with unsubscribe() (RxJS, XState).

Update

type Update<S, M extends Tagged, P = object> = {
    readonly [K in M['_tag']]: Reducer<S, M, Variant<M, K>, P>;
} & {
    readonly PropsChanged?: Reducer<S, M, PropsChanged<P>, P>;
    readonly IntentRejected?: Reducer<S, M, IntentRejected, P>;
    readonly StoreChanged?: Reducer<S, M, StoreChanged, P>;
    readonly Hydrated?: Reducer<S, M, Hydrated, P>;
    readonly Errored?: Reducer<S, M, Errored, P>;
    readonly FormReset?: Reducer<S, M, FormReset, P>;
    readonly FormDisabled?: Reducer<S, M, FormDisabled, P>;
    readonly FormStateRestore?: Reducer<S, M, FormStateRestore, P>;
};

One pure reducer per message tag (exhaustive by construction), plus optional reducers for framework messages. Reducers may return commands.

Variant

type Variant<M extends Tagged, K> = Extract<M, {
    readonly _tag: K;
}>;

The member of message union M whose tag is K: Variant<Msg, 'Add'>.

@gyral/core/storage

Functions

Name Summary
makeLocalStorageDriver JSON storage with typed reads/writes and cancellable native + own-write change streams.

makeLocalStorageDriver

function makeLocalStorageDriver<T>(schema: StandardSchemaV1<unknown, T>, options?: LocalStorageOptions<T>): Driver<StorageInput<T>, StorageOutput<T>, StorageError>;

JSON storage with typed reads/writes and cancellable native + own-write change streams.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
LocalStorageOptions
StorageChangeSource
StorageDecodeError
StorageError
StorageInput
StorageOutput
StoredValue Absence differs from a decoded value, even when the schema accepts null or undefined.

LocalStorageOptions

interface LocalStorageOptions<T> {
    /** Name for normal driver substitution. Default localStorage. */
    readonly name?: string;
    /** Keys and Clear are restricted to this namespace. Default empty (all keys). */
    readonly prefix?: string;
    /** Accessed only when a command runs. Default native localStorage. */
    readonly storage?: () => Storage;
    /** A transformed schema may need to encode model values back into its input shape. */
    readonly encode?: (value: T) => unknown;
}

StorageChangeSource

type StorageChangeSource = 'initial' | 'driver' | 'tab';

StorageDecodeError

interface StorageDecodeError {
    readonly _tag: 'StorageDecodeError';
    readonly key: string;
    readonly issues: readonly StandardSchemaV1.Issue[];
}

StorageError

type StorageError = StorageDecodeError | {
    readonly _tag: 'StorageAccessError';
    readonly operation: StorageInput<unknown>['_tag'];
    readonly key: string | undefined;
    readonly message: string;
};

StorageInput

type StorageInput<T> = {
    readonly _tag: 'Read';
    readonly key: string;
} | {
    readonly _tag: 'Write';
    readonly key: string;
    readonly value: T;
} | {
    readonly _tag: 'Remove';
    readonly key: string;
} | {
    readonly _tag: 'Clear';
} | {
    readonly _tag: 'Watch';
    readonly key: string;
};

StorageOutput

type StorageOutput<T> = {
    readonly _tag: 'StorageValue';
    readonly key: string;
    readonly entry: StoredValue<T>;
} | {
    readonly _tag: 'StorageWritten' | 'StorageRemoved';
    readonly key: string;
} | {
    readonly _tag: 'StorageCleared';
} | {
    readonly _tag: 'StorageChanged';
    readonly key: string;
    readonly source: StorageChangeSource;
    readonly entry: StoredValue<T>;
} | {
    readonly _tag: 'StorageDecodeFailed';
    readonly key: string;
    readonly source: StorageChangeSource;
    readonly error: StorageDecodeError;
};

StoredValue

type StoredValue<T> = {
    readonly found: false;
} | {
    readonly found: true;
    readonly value: T;
};

Absence differs from a decoded value, even when the schema accepts null or undefined.

@gyral/core/indexeddb

Functions

Name Summary
makeIndexedDBDriver Typed record operations and atomic batches over application-owned stores and migrations.

makeIndexedDBDriver

function makeIndexedDBDriver<S extends IndexedDBStores>(stores: S, options: IndexedDBOptions<S>): Driver<IndexedDBInput<S>, IndexedDBOutput<S>, IndexedDBError>;

Typed record operations and atomic batches over application-owned stores and migrations.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
IndexedDBError
IndexedDBInput
IndexedDBOperation
IndexedDBOptions
IndexedDBOutput
IndexedDBRange
IndexedDBRecord
IndexedDBResult
IndexedDBStores

IndexedDBError

type IndexedDBError = {
    readonly _tag: 'IndexedDBBlockedError';
    readonly database: string;
    readonly version: number;
} | {
    readonly _tag: 'IndexedDBInputError';
    readonly message: string;
} | {
    readonly _tag: 'IndexedDBAccessError';
    readonly database: string;
    readonly phase: 'encode' | 'open' | 'upgrade' | 'transaction';
    readonly name: string;
    readonly message: string;
} | {
    readonly _tag: 'IndexedDBDecodeError';
    readonly store: string;
    readonly issues: readonly StandardSchemaV1.Issue[];
    /** Native transaction completed before decoding. Its writes cannot be undone. */
    readonly committed: true;
};

IndexedDBInput

type IndexedDBInput<S extends IndexedDBStores> = IndexedDBOperation<S> | {
    readonly _tag: 'Transaction';
    readonly operations: readonly IndexedDBOperation<S>[];
};

IndexedDBOperation

type IndexedDBOperation<S extends IndexedDBStores> = {
    [K in keyof S & string]: {
        readonly _tag: 'Read' | 'Delete';
        readonly store: K;
        readonly key: IDBValidKey;
    } | {
        readonly _tag: 'Put';
        readonly store: K;
        readonly value: IndexedDBRecord<S[K]>;
        readonly key?: IDBValidKey;
    } | {
        readonly _tag: 'Query';
        readonly store: K;
        readonly index?: string;
        readonly range?: IndexedDBRange;
        readonly direction?: IDBCursorDirection;
        readonly limit: number;
    };
}[keyof S & string];

IndexedDBOptions

interface IndexedDBOptions<S extends IndexedDBStores> {
    readonly database: string;
    readonly version: number;
    readonly name?: string;
    readonly factory?: () => IDBFactory;
    /** Synchronous: queue migration requests before returning; never await. */
    readonly upgrade?: (db: IDBDatabase, oldVersion: number, newVersion: number, transaction: IDBTransaction) => void;
    readonly encode?: <K extends keyof S & string>(store: K, value: IndexedDBRecord<S[K]>) => unknown;
}

IndexedDBOutput

type IndexedDBOutput<S extends IndexedDBStores> = IndexedDBResult<S> | {
    readonly _tag: 'Committed';
    readonly results: readonly IndexedDBResult<S>[];
};

IndexedDBRange

type IndexedDBRange = {
    readonly _tag: 'Only';
    readonly key: IDBValidKey;
} | {
    readonly _tag: 'Lower' | 'Upper';
    readonly key: IDBValidKey;
    readonly open?: boolean;
} | {
    readonly _tag: 'Bound';
    readonly lower: IDBValidKey;
    readonly upper: IDBValidKey;
    readonly lowerOpen?: boolean;
    readonly upperOpen?: boolean;
};

IndexedDBRecord

type IndexedDBRecord<S extends StandardSchemaV1> = StandardSchemaV1.InferOutput<S>;

IndexedDBResult

type IndexedDBResult<S extends IndexedDBStores> = {
    [K in keyof S & string]: {
        readonly _tag: 'Record';
        readonly store: K;
        readonly key: IDBValidKey;
        readonly entry: {
            readonly found: false;
        } | {
            readonly found: true;
            readonly value: IndexedDBRecord<S[K]>;
        };
    } | {
        readonly _tag: 'Put' | 'Deleted';
        readonly store: K;
        readonly key: IDBValidKey;
    } | {
        readonly _tag: 'Records';
        readonly store: K;
        readonly records: readonly {
            readonly key: IDBValidKey;
            readonly value: IndexedDBRecord<S[K]>;
        }[];
    };
}[keyof S & string];

IndexedDBStores

type IndexedDBStores = Readonly<Record<string, StandardSchemaV1>>;

@gyral/core/vite

Functions

Name Summary
gyralVitePreset Spread into a Vite config, or into each Vitest project:

gyralVitePreset

function gyralVitePreset(options?: GyralViteOptions): GyralViteConfig;

Spread into a Vite config, or into each Vitest project:

export default defineConfig({ ...gyralVitePreset(), build: { … } });

With plugins of your own, list both: plugins: [...preset.plugins, mine()].

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
ComponentsOptions
ElementTypesOptions Where the preset looks for components and writes their typings.
GyralViteConfig
GyralViteOptions
TemplateCompilerOptions

ComponentsOptions

interface ComponentsOptions {
    /** Directories (relative to Vite's root) whose modules' `define()` tags are found. Default `['src']`. */
    readonly include?: readonly string[];
    /**
     * Tag → module (relative to Vite's root) for components the scan can't find: those defined
     * through a wrapper such as `defineMemberForm({ tag: 'member-form' })`, which calls
     * `define()` itself. Listed entries win over scanned ones.
     */
    readonly modules?: Readonly<Record<string, string>>;
}

ElementTypesOptions

interface ElementTypesOptions {
    /** Source directories to scan, relative to the Vite root. Default `['src']`. */
    readonly dirs?: readonly string[];
    /** The declaration file to write, relative to the Vite root. Default `.gyral/elements.d.ts`. */
    readonly out?: string;
}

Where the preset looks for components and writes their typings.

GyralViteConfig

interface GyralViteConfig {
    readonly plugins: Plugin[];
    readonly optimizeDeps: {
        readonly include: string[];
    };
}

GyralViteOptions

interface GyralViteOptions {
    /**
     * Generated element typings (`.gyral/elements.d.ts`, see `ElementTypesOptions`), or `false`
     * to write none. Default: on, scanning `src`.
     */
    readonly types?: ElementTypesOptions | false;
    /** Modules to pre-bundle in dev, so Vite doesn't discover them mid-run and reload. */
    readonly optimize?: readonly string[];
    /** Template compiler options (`vite build` only). */
    readonly compiler?: TemplateCompilerOptions;
    /**
     * The app is never server-rendered: leave the hydration code out of the browser bundle.
     * Default false.
     */
    readonly clientOnly?: boolean;
    /**
     * Automatic component loading (ADR 0016 addendum): every component defined under `include`
     * gets a client module the build splits out, and a loader (`virtual:gyral/components`, added
     * to the client build's input) imports a tag's module once that tag is in the page.
     * `renderPage({ components })` then links exactly the components a page rendered, so pages
     * need no client entry of their own and no side-effect imports. Default off.
     */
    readonly components?: boolean | ComponentsOptions;
}

TemplateCompilerOptions

interface TemplateCompilerOptions {
    /**
     * Module specifiers whose `html` (and `svg`) exports are the view layer's tags (default
     * `DEFAULT_TEMPLATE_SOURCES`). An import matches when its specifier is listed, or when it
     * resolves to the same module as a listed one; core's own view modules always match. A
     * source other than '@gyral/core' must also export `compiled` (and `compiledSvg` when it
     * exports `svg`).
     */
    readonly sources?: readonly string[];
    /**
     * Rule 7's extra check with parse5 (an optional peer dependency): `true` (default) uses
     * 'parse5' when it is installed, a string names the module to load, `false` skips it.
     */
    readonly parse5?: boolean | string;
}

@gyral/core/eslint

Constants

Name Summary
default

default

declare const _default: GyralPlugin;
export default _default;

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
GyralPlugin

GyralPlugin

interface GyralPlugin extends ESLint.Plugin {
    readonly rules: {
        readonly template: typeof templateRule;
        readonly 'each-row-purity': typeof rowPurityRule;
        readonly 'unused-intent': typeof unusedIntentRule;
    };
    readonly configs: {
        readonly recommended: Linter.Config;
    };
}