;
addBlocker: (fn: BlockerFn) => () => void;
update: (patch: Partial) => void;
setRunning: (running: boolean) => void;
/** Finishes the current transition immediately. */
settle: () => void;
ended: boolean;
index: number;
stackSize: number;
root: RootProps;
readonly stackId: string;
readonly layerId: string;
}
interface LayerComponentProps
{
call: LayerCallContext
;
payload: P;
data?: D;
error?: E;
phase: LayerPhase;
transition: LayerTransition;
dismissing: boolean;
actionStatus: LayerActionStatus;
}
/** Framework adapters narrow this to their component type. */
type LayerComponent = unknown;
interface LayerOptions
{
/** @default "default" */
stack?: string;
/** Stable identity; same key + `upsert` → update existing instance. */
key: LayerKey;
component?: LayerComponent;
/** Enter duration in milliseconds; `call.settle()` finishes it early. @default 0 */
enteringDelay?: number;
/** Exit duration in milliseconds; `call.settle()` finishes it early. @default 0 */
exitingDelay?: number;
/** When true, reusing an active key updates its payload instead of stacking. */
upsert?: boolean;
/** Loads data before activation; dismissal aborts the signal. */
loadFn?: (ctx: {
payload: P;
signal: AbortSignal;
}) => Promise | D;
/** Validates payload before opening; the parsed output becomes the payload. */
validate?: Validator;
/** Props passed to every layer in the stack via `call.root`. */
rootProps?: RootProps;
}
/**
* Makes `payload` optional only when `P` admits `undefined`.
* Optional object properties alone do not make the payload omittable.
*/
type PayloadArg
= undefined extends P ? {
payload?: P;
} : {
payload: P;
};
/** Rest-tuple factory used by {@link EndArgs} and stack/handle variants. */
type ResponseArgTuple = undefined extends R ? [response?: R, opts?: Opts] : [response: R, opts?: Opts];
/**
* Rest-args for `call.end` / `call.dismiss` / `LayerStack.dismiss`.
* Response is optional only when `R` admits `undefined` — twin of {@link PayloadArg}.
* When response is optional, pass opts as the second arg (`end(undefined, { force: true })`);
* a lone object is treated as the response (not as opts).
*/
type EndArgs = ResponseArgTuple;
/** `LayerStack.dismissAll` rest-args (same gate as {@link EndArgs}). */
type DismissAllArgs = ResponseArgTuple;
/** `cancelQueued` rest-args (same gate as {@link EndArgs}). */
type CancelQueuedArgs = ResponseArgTuple;
/** `LayerHandle.dismiss` rest-args (same gate as {@link EndArgs}; opts may include `id`). */
type HandleDismissArgs = ResponseArgTuple;
type OpenLayerOptions = LayerOptions
& PayloadArg
;
/** Rejects keys that do not exist on `T`. */
type OmitKeyof = Omit;
/** Serial policy when a mounted layer's `loadFn` rejects. */
type SerialOnLoadError = "block" | "advance";
interface StackOptions {
/**
* Serial scope queues unmounted opens until the occupying layer leaves
* (`pending` / `active` / `error` for `onLoadError: "block"`).
* @default { strategy: "parallel" }
*/
scope?: {
strategy: "serial" | "parallel";
/**
* Serial only. `block` — keep `phase: "error"` until dismiss.
* `advance` — remove the failed layer and drain the next queued open.
* @default "block"
*/
onLoadError?: SerialOnLoadError;
};
/** Retains loaded data for same-key restoration. @default 0 */
gcTime?: number;
/** @default "skipBlocked" */
dismissAllMode?: DismissAllMode;
}
type StackDefaults = Record;
interface LayerClientOptions {
defaultStackOptions?: StackDefaults;
}
/**
* Coarse mutation label for devtools / {@link LayerClient#subscribeNotify}.
* `dismissAll` = bulk completion; `cancelAll` = teardown that rejects `open()`.
*/
type StackNotifyAction = "register" | "open" | "queue" | "update" | "setRunning" | "settle" | "dismiss" | "dismissVetoed" | "dismissAll" | "cancelAll" | "cancelQueued" | "phase" | "remove";
/** JSON-safe layer projection on {@link StackNotifyEvent}. */
interface LayerNotifyView {
id: string;
/** Display string from {@link keySignature}. */
key: string;
phase: LayerPhase;
transition: LayerTransition;
actionStatus: LayerActionStatus;
dismissing: boolean;
ended: boolean;
index: number;
stackSize: number;
payload?: unknown;
/** `true` when `payload` could not be JSON-cloned and was omitted. */
payloadTruncated?: boolean;
}
/** Emitted when a stack snapshot changes after a labeled mutation. */
interface StackNotifyEvent {
stackId: string;
seq: number;
ts: number;
action: StackNotifyAction;
active: LayerNotifyView[];
queued: LayerNotifyView[];
}
/**
* Module-augmentation point for app-wide type defaults.
* Augment `defaultError` to set the library-wide error type once.
*
* @example
* declare module "@stainless-code/layers" {
* interface Register { defaultError: AppError }
* }
*/
interface Register {}
/** App-wide error type configured through {@link Register}. */
type DefaultLayerError = Register extends {
defaultError: infer E;
} ? E : Error;
//#endregion
//#region src/utils.d.ts
/**
* Ensures a layer key is JSON-safe for {@link hashKey}.
* Allowed: `string` | `boolean` | `null` | finite `number` | plain objects | arrays of those.
* Throws {@link LayerKeyError} when a segment is outside that domain.
*
* @example
* ```ts
* import { assertLayerKey } from "@stainless-code/layers";
*
* assertLayerKey(["confirm", filterId ?? "none"]);
* ```
*/
declare function assertLayerKey(key: unknown): asserts key is LayerKey;
/**
* Serializes a key deterministically by sorting object properties recursively.
* Throws {@link LayerKeyError} when the key is not JSON-safe.
*/
declare function hashKey(key: LayerKey): string;
/**
* Produces the canonical identity used to compare layer keys.
* Throws {@link LayerKeyError} when the key is not JSON-safe (via {@link hashKey}).
*/
declare function keySignature(key: LayerKey): string;
/**
* Element-wise `Object.is` for arrays.
* Keeps key-filtered snapshot selections stable when `filter()` reallocates
* but the matched element refs are unchanged.
*/
declare function shallowArrayEqual(a: T, b: T): boolean;
//#endregion
//#region src/subscribable.d.ts
type Listener = () => void;
/**
* Base for stores whose listeners participate in {@link notifyManager} batching.
* Wrapping occurs once at subscription, so repeated notifications in one batch
* coalesce for framework and bare subscribers alike.
*/
declare class Subscribable {
protected listeners: Map;
subscribe(listener: Listener): () => void;
protected notify(): void;
protected onSubscribe?(): void;
protected onUnsubscribe?(): void;
get size(): number;
}
//#endregion
//#region src/notifyManager.d.ts
/**
* Coalesces listener calls by identity within a synchronous batch.
* Nested batches flush only when the outer batch completes; wrapped listeners
* called repeatedly during that interval run once.
*/
declare const notifyManager: {
batch(fn: () => T): T;
batchCalls(listener: () => void): () => void;
};
//#endregion
//#region src/controlledPromise.d.ts
type Resolve = (value: T | PromiseLike) => void;
type Reject = (reason?: unknown) => void;
/** Lets lifecycle code outside the executor settle a promise exactly once. */
declare class ControlledPromise {
readonly promise: Promise;
settled: boolean;
resolve: Resolve;
reject: Reject;
constructor();
}
//#endregion
//#region src/layer.d.ts
/** Runtime state and cancellation resources for one stack entry. */
declare class Layer {
#private;
readonly id: string;
readonly key: LayerKey;
readonly promise: ControlledPromise;
readonly abortController: AbortController;
readonly component?: unknown;
readonly enteringDelay: number;
readonly exitingDelay: number;
enterTimer?: ReturnType;
exitTimer?: ReturnType;
aborted: boolean;
dismissPending?: Promise;
constructor(opts: {
key: LayerKey;
payload: P;
index: number;
stackSize: number;
component?: unknown;
enteringDelay?: number;
exitingDelay?: number;
data?: D;
});
get state(): LayerState;
get blockers(): ReadonlySet;
addBlocker(fn: BlockerFn): () => void;
/** Returns true if the snapshot changed. */
setPartial(patch: Partial>): boolean;
setRunning(running: boolean): void;
/** Cancel any in-flight `loadFn`; subsequent resolutions are ignored. */
abort(): void;
resolve(response: R): void;
reject(error: E): void;
}
//#endregion
//#region src/errors.d.ts
/** Provides validator-independent details for one payload failure. */
interface ValidationIssue {
readonly message: string;
readonly path?: ReadonlyArray;
}
/** Normalizes payload failures across supported validator styles. */
declare class PayloadValidationError extends Error {
readonly issues: ReadonlyArray;
constructor(issues: ReadonlyArray, options?: {
cause?: unknown;
});
}
/**
* Narrows an unknown rejection to {@link PayloadValidationError}.
*
* @example
* ```ts
* import { isPayloadValidationError } from "@stainless-code/layers";
*
* function validationMessages(error: unknown) {
* return isPayloadValidationError(error)
* ? error.issues.map((issue) => issue.message)
* : [];
* }
* ```
*/
declare function isPayloadValidationError(value: unknown): value is PayloadValidationError;
/** Thrown when a layer key is not JSON-safe (from `assertLayerKey` / `hashKey`). */
declare class LayerKeyError extends Error {
readonly path: ReadonlyArray;
constructor(message: string, path?: ReadonlyArray);
}
/**
* Narrows an unknown synchronous throw to {@link LayerKeyError}.
*
* @example
* ```ts
* import { isLayerKeyError } from "@stainless-code/layers";
*
* try {
* client.open({ key: [maybeId], payload });
* } catch (error) {
* if (isLayerKeyError(error)) {
* console.error(error.path, error.message);
* }
* }
* ```
*/
declare function isLayerKeyError(value: unknown): value is LayerKeyError;
/**
* Why {@link LayerCancelledError} rejected an `open()` promise.
*
* - `parentDismiss` — child stack `cancelAll`'d when its parent layer dismissed
* - `groupDispose` — adapter / host cleaned up a `useLayerGroup` owner
* - `cancelAll` — explicit {@link LayerStack#cancelAll} / {@link LayerClient#cancelAll}
* - `stackDisconnect` — host disconnect (e.g. Lit `hostDisconnected`)
*/
type LayerCancelReason = "parentDismiss" | "groupDispose" | "cancelAll" | "stackDisconnect";
/**
* Rejects `open()` when a stack is torn down without a completion response
* (`cancelAll`, parent dismiss child clear, group dispose, `stackDisconnect`).
*/
declare class LayerCancelledError extends Error {
/** {@link LayerCancelReason} that triggered this rejection. */
readonly reason: LayerCancelReason;
constructor(reason?: LayerCancelReason);
}
/**
* Narrows an unknown rejection to {@link LayerCancelledError}.
* Treat any cancel reason as normal teardown unless you branch on
* {@link LayerCancelledError#reason}.
*
* @example
* ```ts
* import { isLayerCancelledError } from "@stainless-code/layers";
*
* try {
* await confirm.open(payload);
* } catch (error) {
* if (isLayerCancelledError(error)) return;
* throw error;
* }
* ```
*/
declare function isLayerCancelledError(value: unknown): value is LayerCancelledError;
//#endregion
//#region src/layerStack.d.ts
interface OpenOpts {
key: LayerKey;
payload: P;
component?: unknown;
enteringDelay?: number;
exitingDelay?: number;
upsert?: boolean;
loadFn?: (ctx: {
payload: P;
signal: AbortSignal;
}) => Promise | D;
validate?: Validator;
}
/**
* Manages the ordered layers for one surface.
* Snapshots retain their references across batched notifications until their contents change.
*/
declare class LayerStack
extends Subscribable {
#private;
readonly id: string;
readonly options: StackOptions;
constructor(id: string, options?: StackOptions);
/** Returns a referentially stable snapshot between mutations. */
getSnapshot: () => LayerState
[];
/** Returns serially queued layers, which are excluded from `getSnapshot`. */
getQueuedSnapshot: () => LayerState
[];
getLayer(id: string): Layer
| undefined;
find(key: LayerKey): Layer
| undefined;
addBlocker(fn: StackBlockerFn): () => void;
open(opts: OpenOpts
): Layer
;
/**
* Resolves the caller and aborts in-flight loading.
* Exiting layers remain mounted until their transition settles.
* Response may be omitted when `undefined extends R` ({@link EndArgs}).
*/
dismiss(layer: Layer
, ...args: EndArgs): Promise;
settle(layer: Layer): void;
/**
* Bulk-dismisses active and queued layers, completing every `open()` with
* `response` (including omitted/`undefined` when `undefined extends R` —
* {@link DismissAllArgs} / {@link EndArgs} gate).
* Honors {@link DismissAllMode}; does not reject — use {@link cancelAll} for
* teardown without a completion value.
*/
dismissAll(...args: DismissAllArgs): Promise;
/**
* Force-clears the stack and rejects every open/queued caller with
* {@link LayerCancelledError}. Skips blockers. System teardown path —
* use {@link dismissAll} when completing with a response.
*
* @param opts.reason - Propagated on each rejection.
* @default opts.reason `"cancelAll"`
*/
cancelAll(opts?: {
reason?: LayerCancelReason;
}): Promise;
/**
* Resolves and removes a serially queued layer without mounting it (skips blockers).
* No `id` → FIFO head for the key; `{ id }` → exact queued match.
* Response may be omitted when `undefined extends R` ({@link CancelQueuedArgs} /
* {@link EndArgs} gate).
*/
cancelQueued(key: LayerKey, ...args: CancelQueuedArgs): boolean;
update(layer: Layer, patch: Partial
): void;
setRunning(layer: Layer
, running: boolean): void;
}
//#endregion
//#region src/dataTag.d.ts
declare const dataTagSymbol: unique symbol;
declare const dataTagErrorSymbol: unique symbol;
/**
* Phantom-brands a {@link LayerKey} so response and error types survive inference
* without runtime metadata.
* Tagging is idempotent: the first tag wins, avoiding conflicting brands
* intersecting into `never`.
*/
type DataTag = Key extends {
[dataTagSymbol]: unknown;
[dataTagErrorSymbol]: unknown;
} ? Key : Key & {
[dataTagSymbol]: R;
[dataTagErrorSymbol]: E;
};
/** Lets generic APIs recover the response associated with a tagged key. */
type InferDataTagResponse = Key extends {
[dataTagSymbol]: infer R;
} ? R : never;
/** Lets generic APIs recover the error associated with a tagged key. */
type InferDataTagError = Key extends {
[dataTagErrorSymbol]: infer E;
} ? E : never;
/**
* Preserves a response default when a generic key is untagged.
*/
type ResponseOf = Key extends {
[dataTagSymbol]: infer R;
} ? R : Fallback;
/** Preserves an error default when a generic key is untagged. */
type ErrorOf = Key extends {
[dataTagErrorSymbol]: infer E;
} ? E : Fallback;
/**
* Adds response and error inference to a key without changing it at runtime.
*
* @example
* ```ts
* import { LayerClient, layerKey } from "@stainless-code/layers";
*
* const client = new LayerClient();
* const removeKey = layerKey()(["confirm", "remove"]);
* const ok = await client.open({ key: removeKey, payload: { title: "Remove?" } });
* // ^? boolean
* ```
*/
declare function layerKey(): (key: Key) => DataTag;
//#endregion
//#region src/layerClient.d.ts
/** Coordinates named layer stacks. */
declare class LayerClient {
#private;
constructor(opts?: LayerClientOptions);
/** Returns a stack, applying options only when creating it. */
ensureStack(id: string, options?: StackOptions): LayerStack;
bindChildStack(parentLayerId: string, childStackId: string): () => void;
/** Accepts validator input while storing its parsed output as the payload. */
open, P = InferValidatorOutput, R = void, E = DefaultLayerError, D = unknown, RootProps = unknown>(options: OmitKeyof, "payload" | "validate"> & {
validate: V;
payload: NoInfer>;
}): Promise;
/** Infers response and error types from a {@link DataTag} key. */
open(options: OmitKeyof & {
key: DataTag;
}, "validate">): Promise;
/** Opens a layer and resolves with its dismissal response. */
open(options: OmitKeyof, "validate">): Promise;
getStack(id?: string): LayerStack;
getStackIds(): string[];
/** Subscribes to first-time stack materialization. */
subscribeStacks(listener: (stackId: string) => void): () => void;
/** Subscribes to labeled stack snapshot transitions (devtools). */
subscribeNotify(listener: (event: StackNotifyEvent) => void): () => void;
/**
* Re-emits the current snapshot as a `register` notify for one stack, or all
* materialized stacks when `stackId` is omitted (devtools seed).
*/
seedNotify(stackId?: string): void;
/**
* Bulk-dismisses a stack, completing every `open()` with `response`
* (including omitted/`undefined` for void layers). Honors
* {@link DismissAllMode}; does not reject — prefer {@link cancelAll} for
* teardown without a completion value.
*/
dismissAll(stackId?: string, response?: unknown, opts?: DismissAllOptions): Promise;
/**
* Force-clears a stack and rejects every open/queued caller with
* {@link LayerCancelledError}. System teardown — prefer {@link dismissAll}
* when completing with a response.
*
* @param opts.reason - Propagated on each rejection.
* @default opts.reason `"cancelAll"`
*/
cancelAll(stackId?: string, opts?: {
reason?: LayerCancelReason;
}): Promise;
}
//#endregion
//#region src/layerOptions.d.ts
/**
* Preserves payload, response, error, and data inference for reusable layer options.
* Returns the same object at runtime; its {@link DataTag}-branded key lets
* `LayerClient.open` infer the response without an explicit generic.
*
* @example
* ```ts
* import { LayerClient, layerOptions } from "@stainless-code/layers";
*
* const client = new LayerClient();
* const confirm = layerOptions<{ title: string }, boolean>({
* key: ["confirm", "remove"],
* });
* const ok = await client.open({ ...confirm, payload: { title: "Remove?" } });
* // ^? boolean
* ```
*/
declare function layerOptions(options: LayerOptions
& {
key: Key;
}): LayerOptions
& {
key: DataTag;
};
//#endregion
//#region src/createLayer.d.ts
/** Prevents validated options from falling through to the unvalidated overload. */
type NoValidateOptions = Opts extends {
validate: Validator;
} ? never : Opts;
/**
* Identity-bound layer ops + escapes (`stack` / `client` / `options` / `current`).
* `open`/`upsert` take payload only; stack-level ops route via `.stack`.
*/
interface LayerHandle {
open: (payload: PayloadArg
["payload"]) => Promise;
upsert: (payload: PayloadArg["payload"]) => Promise;
/**
* Dismiss the bound instance (or `{ id }`).
* Response optional iff `undefined extends R` ({@link HandleDismissArgs} /
* {@link EndArgs} gate).
*/
dismiss: (...args: HandleDismissArgs) => Promise;
update: (patch: Partial, opts?: {
id?: string;
}) => void;
/**
* Resolves and removes a serially queued layer without mounting (skips blockers).
* No `id` → FIFO head for this key; `{ id }` → exact queued match.
* Response may be omitted when `undefined extends R` ({@link CancelQueuedArgs} /
* {@link EndArgs} gate).
*/
cancelQueued: (...args: CancelQueuedArgs) => boolean;
readonly client: LayerClient;
readonly stack: LayerStack;
readonly options: LayerOptions
& {
key: DataTag;
};
/** Live-checked bound instance (`null` when not in the stack). */
readonly current: Layer | null;
}
/**
* Validated handle: `open`/`upsert` take schema **input** ({@link OpenValidatePayload});
* `current`/`update` use parsed **output**.
*/
interface ValidatedLayerHandle, R, E, D, RP> extends Omit, R, E, D, RP>, "open" | "upsert"> {
open: (payload: OpenValidatePayload) => Promise;
upsert: (payload: OpenValidatePayload) => Promise;
}
/**
* Wire `layerOptions` + a {@link LayerClient} into a headless {@link LayerHandle}.
*
* @example
* ```ts
* const c = createLayer(confirm, client);
* const ok = await c.open({ title: "Remove?" });
* ```
*/
declare function createLayer, R, E = DefaultLayerError, D = unknown, RP = unknown>(options: LayerOptions, R, E, D, RP> & {
key: LayerKey;
validate: V;
}, client: LayerClient): ValidatedLayerHandle;
declare function createLayer(options: NoValidateOptions & {
key: LayerKey;
}>, client: LayerClient): LayerHandle;
//#endregion
//#region src/callContext.d.ts
/**
* Creates the framework-neutral imperative context passed to a layer component.
* Adapters provide rendering; stack and layer instances retain lifecycle control.
*/
declare function createCallContext
(stack: LayerStack
, layer: Layer
, state: LayerState
, rootProps?: RootProps): LayerCallContext
;
//#endregion
//#region src/layerGroup.d.ts
/** Customizes child-stack identity and lifecycle. */
interface LayerGroupOptions {
/**
* Distinguishes sibling groups owned by the same parent.
* @default "group"
*/
name?: string;
scope?: StackOptions["scope"];
gcTime?: StackOptions["gcTime"];
}
/** Controls a child stack bound to its parent layer's lifetime. */
interface LayerGroupHandle {
readonly stackId: string;
/** Opens a layer with the child stack pre-bound. */
open
(options: Omit, "stack">): Promise;
dismissAll(response?: unknown): void;
/** Removes the parent-lifetime binding when its owner unmounts. */
dispose(): void;
}
/** Derives a collision-free path id: `${parentStackId}~${parentLayerId}~${name}`. */
declare function childStackId(parent: Pick, "stackId" | "layerId">, name?: string): string;
/**
* Creates a child stack that is `cancelAll`'d (`LayerCancelledError`, reason
* `parentDismiss`) when its parent dismisses. Pass the {@link LayerClient} that
* owns `parent`; another client cannot observe the parent's lifetime.
*
* @example
* ```ts
* import {
* createLayerGroup,
* type LayerCallContext,
* type LayerClient,
* } from "@stainless-code/layers";
*
* declare const client: LayerClient;
* declare const parent: LayerCallContext;
*
* const group = createLayerGroup(client, parent, { name: "nested" });
* group.dismissAll();
* group.dispose();
* ```
*/
declare function createLayerGroup(client: LayerClient, parent: Pick, "stackId" | "layerId">, options?: LayerGroupOptions): LayerGroupHandle;
//#endregion
export { BlockerFn, CancelQueuedArgs, ControlledPromise, type DataTag, DefaultLayerError, DismissAllArgs, DismissAllMode, DismissAllOptions, DismissOptions, EndArgs, type ErrorOf, HandleDismissArgs, type InferDataTagError, type InferDataTagResponse, type InferValidatorInput, type InferValidatorOutput, Layer, LayerActionStatus, LayerCallContext, type LayerCancelReason, LayerCancelledError, LayerClient, LayerClientOptions, LayerComponent, LayerComponentProps, type LayerGroupHandle, type LayerGroupOptions, type LayerHandle, LayerKey, LayerKeyError, LayerNotifyView, LayerOptions, LayerPhase, LayerStack, LayerState, LayerTransition, OmitKeyof, OpenLayerOptions, type OpenValidatePayload, PayloadArg, PayloadValidationError, Register, type Reject, type Resolve, ResponseArgTuple, type ResponseOf, SerialOnLoadError, StackBlockerFn, StackDefaults, StackNotifyAction, StackNotifyEvent, StackOptions, type StandardSchemaV1, Subscribable, type ValidatedLayerHandle, type ValidationIssue, type Validator, assertLayerKey, childStackId, createCallContext, createLayer, createLayerGroup, hashKey, isLayerCancelledError, isLayerKeyError, isPayloadValidationError, keySignature, layerKey, layerOptions, notifyManager, shallowArrayEqual };