@stainless-code/layers
Version:
Headless, UI-agnostic layer/stack manager for modal/dialog/drawer/popover/toast UI. Call a layer like an async function and await its response.
105 lines (91 loc) • 2.73 kB
text/typescript
import { ControlledPromise } from "./controlledPromise";
import type {
BlockerFn,
DefaultLayerError,
LayerKey,
LayerState,
} from "./types";
import { hashKey } from "./utils";
/** Distinguishes concurrent instances that share a key. */
let instanceCounter = 0;
/** Runtime state and cancellation resources for one stack entry. */
export class Layer<P = unknown, R = void, E = DefaultLayerError, D = unknown> {
readonly id: string;
readonly key: LayerKey;
readonly promise: ControlledPromise<R>;
readonly abortController: AbortController;
readonly component?: unknown;
readonly enteringDelay: number;
readonly exitingDelay: number;
enterTimer?: ReturnType<typeof setTimeout>;
exitTimer?: ReturnType<typeof setTimeout>;
aborted = false;
dismissPending?: Promise<boolean>;
#blockers = new Set<BlockerFn>();
#state: LayerState<P, R, E, D>;
constructor(opts: {
key: LayerKey;
payload: P;
index: number;
stackSize: number;
component?: unknown;
enteringDelay?: number;
exitingDelay?: number;
data?: D;
}) {
this.id = `${hashKey(opts.key)}#${++instanceCounter}`;
this.key = opts.key;
this.promise = new ControlledPromise<R>();
this.abortController = new AbortController();
this.component = opts.component;
this.enteringDelay = opts.enteringDelay ?? 0;
this.exitingDelay = opts.exitingDelay ?? 0;
this.#state = {
id: this.id,
key: opts.key,
payload: opts.payload,
data: opts.data,
phase: "pending",
transition: "settled",
dismissing: false,
actionStatus: "idle",
ended: false,
index: opts.index,
stackSize: opts.stackSize,
};
}
get state(): LayerState<P, R, E, D> {
return this.#state;
}
get blockers(): ReadonlySet<BlockerFn> {
return this.#blockers;
}
addBlocker(fn: BlockerFn): () => void {
this.#blockers.add(fn);
return () => this.#blockers.delete(fn);
}
/** Returns true if the snapshot changed. */
setPartial(patch: Partial<LayerState<P, R, E, D>>): boolean {
for (const key of Object.keys(patch) as (keyof LayerState<P, R, E, D>)[]) {
if (!Object.is(patch[key], this.#state[key])) {
this.#state = { ...this.#state, ...patch };
return true;
}
}
return false;
}
setRunning(running: boolean): void {
this.setPartial({ actionStatus: running ? "running" : "idle" });
}
/** Cancel any in-flight `loadFn`; subsequent resolutions are ignored. */
abort(): void {
this.aborted = true;
this.abortController.abort();
}
resolve(response: R): void {
this.promise.resolve(response);
}
reject(error: E): void {
this.promise.reject(error);
}
}