@gyral/core
Gyral core: Model-View-Intent web components on the web platform, with their own view layer.
npm install @gyral/coreWriting 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[]) => TemplateResultThe 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[]) => TemplateResultThe 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 nothingRenders 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.
HeadLink
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;
};
}