diff --git a/.changeset/serial-onload-error.md b/.changeset/serial-onload-error.md
new file mode 100644
index 0000000..793f52d
--- /dev/null
+++ b/.changeset/serial-onload-error.md
@@ -0,0 +1,5 @@
+---
+"@stainless-code/layers": patch
+---
+
+Serial stacks: failed `loadFn` occupies the lane by default (`onLoadError: "block"`), fixing leapfrog where a later open could mount while an error layer stayed up. Opt into `onLoadError: "advance"` to remove the failed layer and drain the queue.
diff --git a/apps/docs/content/concepts/glossary.mdx b/apps/docs/content/concepts/glossary.mdx
index fac4eaa..91e163d 100644
--- a/apps/docs/content/concepts/glossary.mdx
+++ b/apps/docs/content/concepts/glossary.mdx
@@ -44,7 +44,7 @@ From `LayerClient` down to `keySignature` — every term in code, docs, and issu
- **upsert** — with `upsert: true` (or handle `.upsert`), reusing an active key updates its payload instead of stacking (singleton toasts/progress).
- - **scope** — per-stack queueing on `StackOptions`: `scope: { strategy: "serial" | "parallel" }` (`parallel` when omitted); `serial` = one active layer at a time, `parallel` = stack freely.
+ - **scope** — per-stack queueing on `StackOptions`: `scope: { strategy: "serial" | "parallel", onLoadError?: "block" | "advance" }` (`parallel` / `block` when omitted); `serial` = one occupying layer at a time, `parallel` = stack freely. `onLoadError`: `block` (default) keeps a failed `loadFn` until dismiss; `advance` removes it and drains the queue.
- **gcTime** — keep dismissed layers in a cache so re-opening the same key restores `data` without re-running `loadFn`. One slot per key (last-dismissed-wins); evicted after `gcTime` with explicit teardown.
- **loadFn** — optional async load run on `open`; cancelable via `AbortController`; `pending` → `active` with `data` on success, `error` on throw.
diff --git a/apps/docs/content/concepts/lifecycle.mdx b/apps/docs/content/concepts/lifecycle.mdx
index 52594f9..9323958 100644
--- a/apps/docs/content/concepts/lifecycle.mdx
+++ b/apps/docs/content/concepts/lifecycle.mdx
@@ -16,8 +16,8 @@ stateDiagram-v2
pending --> active: loadFn resolves
pending --> error: loadFn throws
pending --> queued: serial scope (waiting)
- queued --> pending: active dismissed (has loadFn)
- queued --> active: active dismissed (no loadFn)
+ queued --> pending: occupant leaves (has loadFn)
+ queued --> active: occupant leaves (no loadFn)
active --> dismissed: end / dismiss
dismissed --> removed: exitingDelay or settle()
removed --> cached: gcTime > 0, data set
@@ -26,7 +26,7 @@ stateDiagram-v2
removed --> [*]: gcTime = 0
```
-**Serial scope** adds `queued`: later opens wait behind an active layer and are not in `getSnapshot()` — use `getQueuedSnapshot()` to observe them.
+**Serial scope** adds `queued`: later opens wait behind the occupying layer and are not in `getSnapshot()` — use `getQueuedSnapshot()` to observe them.
## The three axes
@@ -41,7 +41,7 @@ actionStatus: "idle" | "running"; // in-flight action
| Phase | Meaning |
| ----- | ------- |
| `pending` | `loadFn` in flight (cancelable via `AbortController`). |
-| `queued` | Serial scope only: waiting behind an active layer; not in `getSnapshot()`; visible via `getQueuedSnapshot()`. |
+| `queued` | Serial scope only: waiting behind the occupying layer; not in `getSnapshot()`; visible via `getQueuedSnapshot()`. |
| `active` | Mounted; component receives `call`, `payload`, `data`, `error`, `phase`, `transition`, `actionStatus`, `dismissing`. |
| `dismissed` | Caller's `await` resolved (`ended=true`); layer may still be mounted while `transition: "exiting"`. |
| `error` | `loadFn` threw; the caller's `await` rejects. |
diff --git a/apps/docs/content/concepts/stacks-scope-gc.mdx b/apps/docs/content/concepts/stacks-scope-gc.mdx
index e448c5f..d2e9c32 100644
--- a/apps/docs/content/concepts/stacks-scope-gc.mdx
+++ b/apps/docs/content/concepts/stacks-scope-gc.mdx
@@ -12,15 +12,18 @@ A **named stack** (`LayerStack`) is an ordered collection of active layers for o
Per-stack queueing is configured on `StackOptions`:
```ts
-scope: { strategy: "serial" | "parallel" } // parallel when omitted
+scope: {
+ strategy: "serial" | "parallel", // parallel when omitted
+ onLoadError?: "block" | "advance", // serial only; default "block"
+}
```
| Strategy | Behavior |
| -------- | -------- |
| `parallel` (default) | Layers stack freely; multiple `pending`/`active` layers at once. |
-| `serial` | Only one `pending`/`active` layer at a time; later opens queue as `phase: "queued"` (not in `getSnapshot()`). |
+| `serial` | One occupying layer at a time (`pending` / `active` / `error` under default `onLoadError: "block"`); later opens queue as `phase: "queued"` (not in `getSnapshot()`). |
-Queued layers are visible via `getQueuedSnapshot()`. When the active layer dismisses, the next queued layer activates. `dismissAll` drains the queue, resolving queued callers without mounting them.
+Queued layers are visible via `getQueuedSnapshot()`. The next queued layer activates when the occupant leaves — dismiss, or `onLoadError: "advance"` after a failed `loadFn`. `dismissAll` drains the queue without mounting queued callers.
:::tip
Use serial scope for one-at-a-time flows — onboarding steps, queued confirms, or any surface that must not show two layers simultaneously.
diff --git a/apps/docs/content/guides/error-handling.mdx b/apps/docs/content/guides/error-handling.mdx
index 35de6a8..fc9e251 100644
--- a/apps/docs/content/guides/error-handling.mdx
+++ b/apps/docs/content/guides/error-handling.mdx
@@ -91,6 +91,8 @@ try {
The loaded `data` is available on the layer state when `loadFn` succeeds; on error the layer enters `phase: "error"`.
+Serial stacks keep that error in the lane until dismiss (`onLoadError: "block"`). Use `onLoadError: "advance"` to skip it — [Serial queues](/guides/serial-queues).
+
:::note
A `Result`-returning `open` was deliberately rejected to preserve direct `await`. Narrow in `catch` instead. Bag-form `client.open({ …opts, payload })` rejects the same way.
:::
diff --git a/apps/docs/content/guides/serial-queues.mdx b/apps/docs/content/guides/serial-queues.mdx
index f1e71d3..08ef39e 100644
--- a/apps/docs/content/guides/serial-queues.mdx
+++ b/apps/docs/content/guides/serial-queues.mdx
@@ -1,6 +1,6 @@
---
title: Serial queues
-description: One active layer at a time with scope serial, getQueuedSnapshot, and queued-confirm flows.
+description: One occupying layer at a time with scope serial, getQueuedSnapshot, and queued-confirm flows.
search:
tags: ["guide"]
---
@@ -39,7 +39,7 @@ stack.subscribe(() => {
});
```
-Queued layers are not mounted — their components do not render until the active layer dismisses.
+Queued layers are not mounted — their components do not render until the occupying layer leaves.
## Onboarding flow
@@ -52,7 +52,7 @@ const steps = [
for (const step of steps) {
void client.open(step); // [!code highlight]
- // only one active; rest queue
+ // only one occupying; rest queue
}
```
@@ -97,6 +97,20 @@ stack.cancelQueued(["confirm", "remove"], false, { id: queuedState.id }); // [!c
On a wired handle, `confirmLayer.cancelQueued(false)` is FIFO for the bound key; `confirmLayer.cancelQueued(false, { id })` targets one row.
+## Handle failed loads
+
+By default (`onLoadError: "block"`), a rejecting `loadFn` leaves `phase: "error"` mounted. That layer still occupies the lane — queued work waits, and further opens queue — until you dismiss it from error UI or `catch`.
+
+For a silent skip (remove the failed layer and drain the next queued open):
+
+```ts
+client.ensureStack("onboarding", {
+ scope: { strategy: "serial", onLoadError: "advance" },
+});
+```
+
+Narrow rejections in [Error handling](/guides/error-handling).
+
## Edge cases
- **`dismissAll`** drains the queue — queued callers resolve without mounting.
diff --git a/apps/docs/content/reference/core-api.mdx b/apps/docs/content/reference/core-api.mdx
index ab3a061..376b062 100644
--- a/apps/docs/content/reference/core-api.mdx
+++ b/apps/docs/content/reference/core-api.mdx
@@ -77,7 +77,7 @@ class LayerStack {
| Method | Purpose |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `getSnapshot` | Stable-ref snapshot of mounted layers (`pending`/`active`/`dismissed`/`error`) |
-| `getQueuedSnapshot` | Serial-scope queue — layers waiting behind an active layer |
+| `getQueuedSnapshot` | Serial-scope queue — layers waiting behind the occupying layer |
| `subscribe` | Register for snapshot changes (batched via `notifyManager`) |
| `find` | Topmost mounted layer for a logical key (key signature; `findLast`) |
| `dismiss` | Resolve one layer; returns whether dismissal succeeded (blockers may veto) |
@@ -103,6 +103,7 @@ Immutable snapshot of one layer, consumed by selectors.
Per-stack options passed to `ensureStack` or `LayerClientOptions.defaultStackOptions`.
+
## layerOptions
diff --git a/docs/architecture.md b/docs/architecture.md
index 00e9d04..30ccea3 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -65,7 +65,7 @@ serial scope: later opens wait as `queued` (not mounted) — see `getQueuedSnaps
```
- `pending` — `loadFn` in flight (cancelable via `AbortController`).
-- `queued` — serial scope only: waiting behind an active layer; not in `getSnapshot()`; visible via `getQueuedSnapshot()`.
+- `queued` — serial scope only: waiting behind the occupying layer; not in `getSnapshot()`; visible via `getQueuedSnapshot()`.
- `active` — mounted; component receives `call` (`end`/`dismiss`/`update`/`setRunning`/`settle`/`ended`/`index`/`stackSize`/`root`/`stackId`/`layerId`/`addBlocker`), `payload`, `data`, `error`, `phase`, `transition`, `actionStatus`, `dismissing`.
- `dismissed` — caller's `await` resolved (`ended=true`); `transition: "exiting"` keeps the layer mounted for `exitingDelay` (or until `settle`); on removal, cached for `gcTime` so re-opening the same key restores `data` without re-running `loadFn`.
- `error` — `loadFn` threw; the caller's `await` rejects.
@@ -157,7 +157,7 @@ Narrow in a `catch` with the shipped guards (`isPayloadValidationError`) or the
## Stacks, scope, gcTime
- **Named stacks** — `LayerClient.getStack(id)`; isolated. `open({ stack })` picks one.
-- **`scope: { strategy: 'serial' }`** — only one `pending`/`active` layer at a time; later opens queue (`phase: 'queued'`, visible via `getQueuedSnapshot()`) and activate when the active one is dismissed. `parallel` (default) stacks freely. `dismissAll` (async; see § Blockers) drains the queue, resolving queued callers without mounting.
+- **`scope: { strategy: 'serial', onLoadError?: 'block' | 'advance' }`** — one occupying layer at a time (`pending` / `active` / `error` under default `onLoadError: 'block'`); later opens queue (`phase: 'queued'`, visible via `getQueuedSnapshot()`) and activate when the occupant leaves. `onLoadError: 'advance'` removes a failed `loadFn` layer and drains the next queued open. `parallel` (default) stacks freely. `dismissAll` (async; see § Blockers) drains the queue, resolving queued callers without mounting.
- **`gcTime`** — dismissed layers cached (in the internal `layerGcCache` module — a small removable-cache primitive) so re-opening the same key skips `loadFn`. **One slot per key signature — last-dismissed-wins**; the displaced entry is evicted with explicit teardown, and re-open cancels the timer. A cached layer is off-`getSnapshot()` and inherently unobserved, so the timer-runs-while-cached model already matches observer-gated gc. _Rejected: pausing gc while the stack is unmounted (would serve unbounded-stale data — no `staleTime`/refetch model — and risk retention), and a per-layer observer refcount (a stack-level proxy for it); revisit only alongside a real freshness model._
## Notifications
diff --git a/docs/glossary.md b/docs/glossary.md
index e3a454e..804ea78 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -28,7 +28,7 @@ Ubiquitous language for the `@stainless-code/layers` domain. Keep terms stable a
- **dismissing** — `LayerState.dismissing`: `true` while a user-intent dismiss is consulting blockers; `false` on veto or removal. Use to disable close UI during async confirm.
- **dismiss mode** — `DismissAllMode` for `stack.dismissAll`: `"skipBlocked"` (default — close permitted, leave blocked open), `"stopAtBlocked"` (halt at first blocked), `"force"` (bypass blockers). Default per stack via `StackOptions.dismissAllMode`.
- **upsert** — reusing an active key updates its payload instead of stacking (singleton toasts/progress).
-- **scope** — per-stack queueing on `StackOptions`: `scope: { strategy: "serial" | "parallel" }` (`parallel` when omitted); `serial` = one active layer at a time, `parallel` = stack freely.
+- **scope** — per-stack queueing on `StackOptions`: `scope: { strategy: "serial" | "parallel", onLoadError?: "block" | "advance" }` (`parallel` / `block` when omitted); `serial` = one occupying layer at a time, `parallel` = stack freely. Serial `onLoadError`: `block` keeps a failed `loadFn` as `phase: "error"` until dismiss; `advance` removes it and drains the queue.
- **gcTime** — keep dismissed layers in a cache so re-opening the same key restores `data` without re-running `loadFn`. One slot per key (last-dismissed-wins); evicted after `gcTime` with explicit teardown.
- **loadFn** — optional async load run on `open`; cancelable via `AbortController`; `pending` → `active` with `data` on success, `error` on throw.
- **Observer / selector** — adapters subscribe to `LayerStack.getSnapshot()` and project via a selector (React/Preact `useSyncExternalStore`, Solid `from`, Angular `signal`, Vue `shallowRef`, Lit reactive controllers, Alpine `Alpine.reactive` / data `states`, Svelte runes).
diff --git a/packages/core/skills/layers/SKILL.md b/packages/core/skills/layers/SKILL.md
index 6ec1ef2..b06fdaa 100644
--- a/packages/core/skills/layers/SKILL.md
+++ b/packages/core/skills/layers/SKILL.md
@@ -232,13 +232,13 @@ const serialClient = new LayerClient({
confirm: { scope: { strategy: "serial" }, gcTime: 5_000 },
},
});
-// only one confirm pending/active at a time; later open() queues (phase: "queued")
+// only one occupying layer at a time; later open() queues (phase: "queued")
const stack = serialClient.getStack("confirm");
stack.getQueuedSnapshot(); // inspect queued layers
stack.cancelQueued(["confirm", "remove"], false); // FIFO head; pass `{ id }` for exact queued
```
-Serial scope allows only one `pending` or `active` layer; later opens remain unmounted until their turn. `cancelQueued` resolves a waiting caller without mounting (FIFO, or `{ id }` for exact). `gcTime` caches dismissed data after removal, so reopening the same key restores `data` without rerunning `loadFn`.
+Serial scope allows only one occupying layer (`pending`, `active`, or `error` when `onLoadError` is `"block"`, the default); later opens remain unmounted until their turn. With `onLoadError: "advance"`, a rejecting `loadFn` removes the failed layer and drains the next queued open. `cancelQueued` resolves a waiting caller without mounting (FIFO, or `{ id }` for exact). `gcTime` caches dismissed data after removal, so reopening the same key restores `data` without rerunning `loadFn`.
## Nested layers
diff --git a/packages/core/src/layerStack.test.ts b/packages/core/src/layerStack.test.ts
index 5f211ba..7ad9f48 100644
--- a/packages/core/src/layerStack.test.ts
+++ b/packages/core/src/layerStack.test.ts
@@ -240,6 +240,153 @@ describe("LayerStack — scope serial", () => {
expect(stack.getQueuedSnapshot()).toHaveLength(0);
});
+ it("onLoadError block (default): error occupies lane; queued waits; no leapfrog", async () => {
+ let rejectLoad!: (error: Error) => void;
+ const stack = new LayerStack<{ n: number }, boolean, Error>("s", {
+ scope: { strategy: "serial" },
+ });
+ const a = stack.open({
+ key: ["a"],
+ payload: { n: 1 },
+ loadFn: () =>
+ new Promise((_, reject) => {
+ rejectLoad = reject;
+ }),
+ });
+ stack.open({ key: ["b"], payload: { n: 2 } });
+ expect(stack.getQueuedSnapshot()).toHaveLength(1);
+
+ rejectLoad(new Error("boom"));
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ expect(stack.getSnapshot()).toEqual([
+ expect.objectContaining({ phase: "error", payload: { n: 1 } }),
+ ]);
+ expect(stack.getQueuedSnapshot()).toEqual([
+ expect.objectContaining({ phase: "queued", payload: { n: 2 } }),
+ ]);
+
+ const c = stack.open({ key: ["c"], payload: { n: 3 } });
+ expect(stack.getSnapshot()).toHaveLength(1);
+ expect(stack.getQueuedSnapshot().map((l) => l.payload.n)).toEqual([2, 3]);
+ expect(c.state.phase).toBe("queued");
+
+ await stack.dismiss(a, false);
+ expect(stack.getSnapshot()).toEqual([
+ expect.objectContaining({ phase: "active", payload: { n: 2 } }),
+ ]);
+ expect(stack.getQueuedSnapshot()).toEqual([
+ expect.objectContaining({ payload: { n: 3 } }),
+ ]);
+ });
+
+ it("onLoadError advance: reject removes layer and drains queue", async () => {
+ let rejectLoad!: (error: Error) => void;
+ const stack = new LayerStack<{ n: number }, boolean, Error>("s", {
+ scope: { strategy: "serial", onLoadError: "advance" },
+ });
+ const a = stack.open({
+ key: ["a"],
+ payload: { n: 1 },
+ loadFn: () =>
+ new Promise((_, reject) => {
+ rejectLoad = reject;
+ }),
+ });
+ stack.open({ key: ["b"], payload: { n: 2 } });
+ expect(stack.getQueuedSnapshot()).toHaveLength(1);
+
+ rejectLoad(new Error("boom"));
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ expect(stack.getSnapshot()).toEqual([
+ expect.objectContaining({ phase: "active", payload: { n: 2 } }),
+ ]);
+ expect(stack.getQueuedSnapshot()).toHaveLength(0);
+
+ const c = stack.open({ key: ["c"], payload: { n: 3 } });
+ expect(c.state.phase).toBe("queued");
+ expect(stack.getQueuedSnapshot().map((l) => l.payload.n)).toEqual([3]);
+ });
+
+ it("onLoadError advance: empty queue clears the failed layer", async () => {
+ const stack = new LayerStack<{ n: number }, boolean, Error>("s", {
+ scope: { strategy: "serial", onLoadError: "advance" },
+ });
+ const a = stack.open({
+ key: ["a"],
+ payload: { n: 1 },
+ loadFn: async () => {
+ throw new Error("boom");
+ },
+ });
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ expect(stack.getSnapshot()).toHaveLength(0);
+ expect(stack.getQueuedSnapshot()).toHaveLength(0);
+ });
+
+ it("onLoadError advance: fires onLayerDismiss before remove", async () => {
+ const dismissed: string[] = [];
+ let phaseAtHook: string | undefined;
+ const stack = new LayerStack<{ n: number }, boolean, Error>("s", {
+ scope: { strategy: "serial", onLoadError: "advance" },
+ });
+ stack.onLayerDismiss = (layer) => {
+ dismissed.push(layer.id);
+ phaseAtHook = layer.state.phase;
+ };
+ const a = stack.open({
+ key: ["a"],
+ payload: { n: 1 },
+ loadFn: async () => {
+ throw new Error("boom");
+ },
+ });
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ expect(dismissed).toEqual([a.id]);
+ expect(phaseAtHook).toBe("dismissed");
+ });
+
+ it("onLoadError advance: dismiss on failed handle is a no-op for the next layer", async () => {
+ let rejectLoad!: (error: Error) => void;
+ const stack = new LayerStack<{ n: number }, boolean, Error>("s", {
+ scope: { strategy: "serial", onLoadError: "advance" },
+ });
+ const a = stack.open({
+ key: ["a"],
+ payload: { n: 1 },
+ loadFn: () =>
+ new Promise((_, reject) => {
+ rejectLoad = reject;
+ }),
+ });
+ const b = stack.open({ key: ["b"], payload: { n: 2 } });
+ rejectLoad(new Error("boom"));
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ expect(stack.getSnapshot()[0]?.id).toBe(b.id);
+
+ await expect(stack.dismiss(a, false)).resolves.toBe(true);
+ expect(stack.getSnapshot()).toEqual([
+ expect.objectContaining({ id: b.id, phase: "active" }),
+ ]);
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ });
+
+ it("onLoadError advance is ignored on parallel stacks", async () => {
+ const stack = new LayerStack<{ n: number }, boolean, Error>("s", {
+ scope: { strategy: "parallel", onLoadError: "advance" },
+ });
+ const a = stack.open({
+ key: ["a"],
+ payload: { n: 1 },
+ loadFn: async () => {
+ throw new Error("boom");
+ },
+ });
+ await expect(a.promise.promise).rejects.toThrow("boom");
+ expect(stack.getSnapshot()).toEqual([
+ expect.objectContaining({ phase: "error", payload: { n: 1 } }),
+ ]);
+ });
+
it("cancelQueued resolves a queued layer without mounting it", async () => {
const stack = new LayerStack<{ n: number }, boolean>("s", {
scope: { strategy: "serial" },
diff --git a/packages/core/src/layerStack.ts b/packages/core/src/layerStack.ts
index fadb9c6..7a515e3 100644
--- a/packages/core/src/layerStack.ts
+++ b/packages/core/src/layerStack.ts
@@ -101,9 +101,13 @@ export class LayerStack<
return this.options.scope?.strategy === "serial";
}
+ /** Serial occupancy — pending, active, or mounted error (block policy). */
#hasActive(): boolean {
return this.#layers.some(
- (l) => l.state.phase === "pending" || l.state.phase === "active",
+ (l) =>
+ l.state.phase === "pending" ||
+ l.state.phase === "active" ||
+ l.state.phase === "error",
);
}
@@ -223,6 +227,23 @@ export class LayerStack<
if (layer.aborted) {
return;
}
+ const isAdvance =
+ this.#serial &&
+ (this.options.scope?.onLoadError ?? "block") === "advance";
+ if (isAdvance) {
+ // Drop the failed occupant and drain — no lasting error UI.
+ // Mirror #commitDismiss: mark dismissed, then onLayerDismiss, then remove.
+ layer.reject(error as E);
+ layer.setPartial({
+ error: error as E,
+ phase: "dismissed",
+ transition: "settled",
+ ended: true,
+ });
+ this.onLayerDismiss?.(layer);
+ this.#remove(layer);
+ return;
+ }
layer.setPartial({ error: error as E, phase: "error" });
layer.reject(error as E);
this.#dispatch("phase", () => {
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index b30a12e..6d3e486 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -143,9 +143,24 @@ export type OpenLayerOptions<
/** Rejects keys that do not exist on `T`. */
export type OmitKeyof = Omit;
+/** Serial policy when a mounted layer's `loadFn` rejects. */
+export type SerialOnLoadError = "block" | "advance";
+
export interface StackOptions {
- /** Serial scope queues unmounted opens until the pending or active layer dismisses. @default { strategy: "parallel" } */
- scope?: { strategy: "serial" | "parallel" };
+ /**
+ * 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" */