Skip to content
QueryWeave

@queryweave/core

Everything below is exported from @queryweave/core. Nothing else is.

function defineQueryModel<TDefs extends QueryParamDefinitions>(
definitions: TDefs,
options?: QueryModelOptions<TDefs>,
): QueryModel<TDefs>;

Composes named parameters into one model. Throws a TypeError if a key is the empty string or $, the reserved modelIssueKey.

options.name labels the model for diagnostics. options.refine takes refinements that run after every parameter decodes, reporting under modelIssueKey.

Member Returns Notes
name string | undefined Diagnostics only
params Readonly<TDefs> The definitions, for introspection
keys() readonly string[] Managed keys, in definition order
defaults() QueryModelDefaults<TDefs> Only parameters that have a default; required keys absent
decode(input) DecodeResult<Values> Never throws; a throwing codec or refinement becomes an issue
decodeAsync(input) Promise<DecodeResult<Values>> Required when validation is asynchronous; never rejects
encode(values) QueryOutput Canonical; omits defaults; managed keys only
normalize(input) QueryOutput decode then encode, recovering on failure
const modelIssueKey = "$";

The key used for model-level issues.

QueryModelValues, QueryModelDefaults, QueryModelKey, QueryPatch, QueryParamDefinitions, QueryParamValue, QueryParamPresenceOf, QueryModelOptions.

param.boolean(options?: BooleanParamOptions): QueryParamBuilder<boolean, "required">;
param.choice<const T extends string>(choices: readonly T[]): QueryParamBuilder<T, "required">;
param.custom<T>(codec: QueryCodec<T>, options?: CustomParamOptions): QueryParamBuilder<T, "required">;
param.date(options?: DateParamOptions): QueryParamBuilder<string, "required">;
param.datetime(options?: DatetimeParamOptions): QueryParamBuilder<Date, "required">;
param.integer(options?: IntegerParamOptions): QueryParamBuilder<number, "required">;
param.list<T>(item: QueryParam<T>, options?: ListParamOptions): QueryParamBuilder<readonly T[], "required">;
param.number(options?: NumberParamOptions): QueryParamBuilder<number, "required">;
param.text(options?: TextParamOptions): QueryParamBuilder<string, "required">;

Options by family:

Family Options
text allowEmpty, maxLength, minLength, trim
integer max, min
number max, min
boolean truthy, falsy
date max, min as YYYY-MM-DD strings
datetime max, min as Date values
list maxItems, minItems
custom consumesMultipleValues, kind

Each param method also has a named export with identical behavior and inference:

booleanParam(options?: BooleanParamOptions);
choiceParam<const T extends string>(choices: readonly T[]);
customParam<T>(codec: QueryCodec<T>, options?: CustomParamOptions);
dateParam(options?: DateParamOptions);
datetimeParam(options?: DatetimeParamOptions);
integerParam(options?: IntegerParamOptions);
listParam<T>(item: QueryParam<T>, options?: ListParamOptions);
numberParam(options?: NumberParamOptions);
textParam(options?: TextParamOptions);

Use named constructors when bundle size matters and a model only needs some built-in families. A bundler can then remove unrelated codecs. The param.* registry remains supported as the compact, ergonomic form when that distinction is not important.

Construction validates options: inverted bounds (min > max, minLength > maxLength, minItems > maxItems), a date bound that is not a real YYYY-MM-DD date, a datetime bound that is an invalid Date, an empty or overlapping boolean spelling set, a list whose item consumes repeated values, and nullable() on a list all throw a TypeError.

date keeps the YYYY-MM-DD string it decodes. datetime decodes RFC 3339 with Z or an offset into a Date and writes it in UTC, with milliseconds only when they are not zero; a time without an offset is invalid. Encoding an invalid Date, or one outside the years 0001 to 9999, throws.

default(value: Exclude<TValue, undefined>): QueryParam<Exclude<TValue, undefined>, "default">;
describe(description: string): QueryParamBuilder<TValue, TPresence>;
nullable(): QueryParamBuilder<TValue | null, TPresence>;
optional(): QueryParamBuilder<TValue | undefined, "optional">;
refine<TOutput extends Decoded>(refinement: QueryRefinement<Decoded, TOutput>): QueryParamBuilder<TOutput | Absent, TPresence>;
refine<TOutput>(transform: QueryTransform<Decoded, TOutput>): QueryParamBuilder<TOutput | Absent, TPresence>;

Decoded is Exclude<TValue, null | undefined> and Absent is Extract<TValue, null | undefined>: a refinement never receives null or undefined, and they stay in the value type. The first overload validates or narrows; the second changes the type and therefore needs encode.

default() returns a QueryParam, not a builder, so it must be last in a chain. It throws a TypeError when the parameter’s own codec rejects the value, and stores a deep-frozen copy of arrays and plain objects. A Date default is copied too, and every read of it — a recovery, model.defaults(), defaultValue — returns a new Date.

kind, presence, consumesMultipleValues, description, codec, defaultValue.

QueryParamKind is "boolean" | "choice" | "custom" | "date" | "datetime" | "integer" | "list" | "number" | "text". QueryParamPresence is "default" | "optional" | "required". QueryParamBase is the non-generic part of a parameter (kind, presence, consumesMultipleValues, description), and QueryParamFactory is the type of param.

interface QueryCodec<TValue> {
decode(input: readonly string[], context: QueryDecodeContext): QueryValueResult<TValue>;
decodeAsync?(
input: readonly string[],
context: QueryDecodeContext,
): Promise<QueryValueResult<TValue>>;
encode(value: TValue, context: QueryEncodeContext): readonly string[];
}

QueryDecodeContext and QueryEncodeContext both carry key: string and path: readonly PropertyKey[].

function okValue<T>(value: T, issues?: readonly QueryIssue[]): QueryValueResult<T>;
function failValue<T>(issues: readonly QueryIssue[]): QueryValueResult<T>;

QueryValueResult<T> is one value’s outcome; DecodeResult<T> is a whole model’s, carrying partial instead of value when ok is false.

function createQueryIssue(init: QueryIssueInit): QueryIssue;
function hasQueryIssueCode(issues: readonly QueryIssue[], code: QueryIssueCode): boolean;

QueryIssue carries key, code, optional input, message, and optional path, and is frozen. QueryIssueInit is the same shape, accepted by createQueryIssue, which copies input and path and omits them when empty.

QueryIssueCode is "missing" | "empty" | "invalid" | "out_of_range" | "unknown_choice" | "unexpected_multiple_values" | "validation_failed" | "async_required". The union is closed; adding a member is a minor change under the stability policy.

interface QueryRefineContext {
readonly key: string;
readonly path: readonly PropertyKey[];
}
interface QueryRefinement<TInput, TOutput = TInput> {
readonly name?: string | undefined;
readonly async?: boolean | undefined;
refine(
value: TInput,
context: QueryRefineContext,
): QueryRefinementResult<TOutput> | Promise<QueryRefinementResult<TOutput>>;
encode?(value: TOutput): TInput;
}
interface QueryTransform<TInput, TOutput> extends QueryRefinement<TInput, TOutput> {
encode(value: TOutput): TInput;
}

QueryRefinementResult<T> is { ok: true; value: T } or { ok: false; issues: readonly QueryRefinementIssue[] }, where an issue carries message and an optional path. async: true tells the synchronous decode to report async_required without starting the refinement. encode is the inverse used when a refined value is written back; QueryTransform makes it mandatory for a refinement that changes the type.

function parseQueryString(source: string): QueryOutput;
function formatQueryString(entries: QueryOutput): string;
function normalizeQueryEntries(input: QueryInput): QueryOutput;
function selectQueryValues(entries: QueryOutput, key: string): readonly string[];
function queryOutputEquals(left: QueryOutput, right: QueryOutput): boolean;

QueryEntry is readonly [key: string, value: string], QueryOutput is readonly QueryEntry[], and QueryInput is string | Iterable<QueryEntry> | QueryRecordInput, where QueryRecordInput is Readonly<Record<string, string | readonly string[] | undefined>>. URLSearchParams satisfies the iterable form structurally, which is how it is accepted without a DOM library.

formatQueryString is byte-identical to URLSearchParams.prototype.toString, replacing a lone surrogate with U+FFFD as the platform does. parseQueryString decodes + as a space and each run of percent sequences on its own, so a malformed sequence or an invalid UTF-8 run keeps only itself verbatim; the platform would substitute U+FFFD for the bad bytes instead.

function createQueryRuntime<TDefs extends QueryParamDefinitions>(
options: QueryRuntimeOptions<TDefs>,
): QueryRuntime<TDefs>;

options is { model, adapter, navigation?, throttle? }. navigation defaults to "push". throttle is the least number of milliseconds between two writes and defaults to 0; a negative or non-finite value throws.

Member Returns
model the bound model
read() QuerySnapshot<Values>
settled() Promise<QuerySnapshot<Values>>
update(patch, options?) Promise<QueryTransitionResult>
replace(values, options?) Promise<QueryTransitionResult>
remove(keys, options?) Promise<QueryTransitionResult>
reset(keys?, options?) Promise<QueryTransitionResult>
transaction(mutate, options?) Promise<QueryTransitionResult>
subscribe(listener) () => void
dispose() void

Every operation rejects after dispose(). QueryTransitionOptions is { navigation?, signal? }; signal is a QueryAbortSignal, { readonly aborted: boolean; readonly reason?: unknown }, which any AbortSignal satisfies. Transitions run one at a time per runtime, in call order, each starting from the settled state; inside a throttle window they are held and written together when it closes. read() decodes only when the adapter’s entries changed since the last read. settled() resolves once no asynchronous decode is pending. subscribe takes a QuerySnapshotListener<Values>, (snapshot: QuerySnapshot<Values>) => void; every listener runs on every notification, and a listener error is rethrown after the loop.

type QueryStatus = "invalid" | "pending" | "valid";
interface QuerySnapshot<TValues> {
readonly status: QueryStatus;
readonly values: TValues;
readonly issues: readonly QueryIssue[];
readonly result: DecodeResult<TValues>;
}

values is always present and frozen. Use result.ok when you want narrowing. pending means an asynchronous refinement has not settled; values then carry the synchronous best effort and a settled snapshot follows through the subscription.

type QueryTransitionOutcome = "committed" | "redirected" | "refused" | "unchanged" | "cancelled";
interface QueryTransitionResult<TValues> {
readonly navigation: "push" | "replace";
readonly outcome: QueryTransitionOutcome;
readonly reason?: unknown;
readonly output: QueryOutput;
readonly snapshot: QuerySnapshot<TValues>;
}

Two outcomes are decided by the runtime. unchanged: the output equalled the adapter’s entries, so nothing was written and nobody was notified. cancelled: the transition was abandoned before it applied its change, because its signal aborted or the runtime was disposed; reason is then the signal’s reason and output is empty. The other outcomes come from the adapter, with its reason. Transitions written together in one throttle window share one result.

interface QuerySource {
read(): QueryInput;
}
type QueryNavigationOutcome = "committed" | "redirected" | "refused";
interface QueryNavigationResult {
readonly outcome: QueryNavigationOutcome;
readonly reason?: unknown;
}
interface QueryAdapter extends QuerySource {
push(next: QueryOutput): QueryNavigationResult | void | Promise<QueryNavigationResult | void>;
replace(next: QueryOutput): QueryNavigationResult | void | Promise<QueryNavigationResult | void>;
subscribe(listener: QueryChangeListener): () => void;
}

QueryChangeListener is (input: QueryInput) => void. QueryNavigationMode is "push" | "replace". Returning nothing from push or replace means committed; an adapter throws only for environment errors, never to report a refusal.

interface QueryBinding<TDefs, TView = QueryModelValues<TDefs>> {
readonly runtime: QueryRuntime<TDefs>;
readonly values: TView;
}

The shape every framework binding shares. @queryweave/vue extends it.

There is no useQueryState, useQueryStates, parseAs*, createParser, createLoader, createSerializer, withDefault, or tuple setter. These identities are rejected by ADR 0001 and a repository test fails the build if one appears.