;
interface RecCore extends RecHandle {
readonly [RecTypeId]: true;
/** Place this REC with props: `yield* Child.with({ ... })`. */
with(props: P): RecPlacement;
/**
* Render a typed fallback for each error this REC's body can fail with. The
* handler map is exhaustive over the error channel `E` and checked at compile
* time, so you cannot forget a tag or invent one. Each failure — synchronous
* or async — renders its mapped node in place of the component; defects and
* any non-tagged errors are left to the nearest React error boundary.
*
* ```tsx
* const Profile = rec(function* () {
* const user = yield* query(fetchUser(id), id); // E = NotFound | Unauthorized
* return ;
* }).catch({
* NotFound: () => ,
* Unauthorized: () => ,
* });
* ```
*
* Returns a REC whose error channel keeps only the non-tagged remainder
* (usually `never`); the loading obligation `S` is untouched. It catches *this*
* REC's own `yield*`ed failures; a child REC handles its own (wrap it too).
*
* The fallback renders in place of the component and **recovers on its own**:
* effract watches the atoms the body read (a `query`'s key inputs among them)
* and re-runs the REC when one changes, so navigating to an input that no longer
* fails brings the component back. The order of the body's `yield*`s does not
* matter — a failure part-way through is handled wherever the failing yield sits.
*/
catch(handlers: CatchHandlers): REC, R, S, V, L>;
/**
* Opt into a **last-resort fallback** for everything `.catch` cannot name: a
* defect (a thrown error, an `Effect.die`), a non-tagged failure, or a tag the
* handler map did not list at runtime. Without it those always propagate to
* the nearest React error boundary — that default is unchanged; this is the
* explicit opt-out for a component that must stay standing no matter what
* (a widget in a dashboard, a third-party island).
*
* ```tsx
* const Widget = rec(function* () {
* const data = yield* query(load, key); // may die on a bug, not just fail
* return ;
* })
* .catch({ NotFound: () => }) // typed failures first
* .catchDefect((defect, { retry }) => ( // the unnameable remainder
*
* ));
* ```
*
* Deliberately not silent: tag handlers always win, the handler receives the
* original thrown value untouched, and in development every absorbed defect is
* reported through the devtools seam (`kind: 'defect:handled'`) with the
* pretty-printed `Cause` — so a swallowed bug is still visible while you build.
* Recovery matches `.catch`: the fallback re-mounts when a read atom changes,
* and `retry` does so on demand. Returns a REC whose error channel is fully
* discharged (`E = never`).
*/
catchDefect(handler: (defect: unknown, api: CatchApi) => ReactNode): REC
;
/**
* Handle this REC's loading state. A REC that `yield*`s a `suspend`/`query`
* carries a loading obligation `S`; `.suspense(fallback)` discharges it by
* placing the REC in a real `` boundary, so while it is pending the
* `fallback` renders in its place.
*
* ```tsx
* const Page = Profile.suspense(); // Page: REC<…, never> — obligation met
* ```
*
* Returns a REC with `S` back to `never`. One `.suspense` discharges the whole
* subtree beneath it (React Suspense catches every descendant that suspends),
* so a single boundary — at any ancestor, or at `mount` via `{ loading }` —
* satisfies the obligation the type system otherwise bubbles all the way up.
*/
suspense(fallback: ReactNode): REC;
/**
* Acknowledge this REC's volatility — the `.catchDefect` of scheduling. A body
* that reads a volatile atom (one born from a high-frequency source) carries a
* {@link VolatileNotHandled} obligation `V`, because that subscription
* re-renders the component at the source's own frequency. `.hot()` is the
* deliberate acknowledgment that this subtree is *meant* to run that hot —
* a ticker cell, a live chart leaf — and discharges `V` back to `never`.
*
* ```tsx
* const Ticker = rec(function* () {
* const p = yield* price; // price: volatile — V = Volatile
* return {p};
* }).hot(); // V discharged; keep the leaf small
* ```
*
* Deliberately not silent: in development the render ledger reports every
* `.hot()` subtree and its render frequency, so an acknowledged hot spot stays
* visible while you build — exactly how `.catchDefect` reports every absorbed
* defect. Prefer `defer(atom)` at the read when the UI can lag a frame; reach
* for `.hot()` only where source-frequency rendering is the point.
*/
hot(): REC
;
/**
* Memoize this REC's placement — **explicit and type-guided**, never
* automatic. The returned REC's client component is wrapped in `React.memo`
* with a *bounded* comparator: per prop, `Object.is`, or `Equal.equals` when
* both sides implement the `Equal` trait. Nothing ever deep-compares a plain
* structure — which is exactly why `.memo()` is compile-gated: it only
* returns a REC when every prop is **value-comparable** (a primitive, or an
* `Equal.Equal` implementor), and otherwise returns
* {@link MemoRequiresComparableProps} naming the offending prop(s). Hold
* functions in a service; give structured data an `Equal` implementation.
*
* ```tsx
* const Row = rec(function* (props: { id: string; count: number }) {
* return
{props.id}: {props.count};
* });
* const MemoRow = Row.memo(); // ✓ all props value-comparable
* // a parent re-render with value-equal props no longer re-runs Row's body
* ```
*
* Apply it once, at declaration (like `.catch` / `.hot()`): a `.memo()` call
* inside a render body would mint a fresh component type every render and
* defeat the bailout. A props-free REC qualifies trivially (there is nothing
* to compare) and skips parent-driven re-renders entirely — its own atom
* subscriptions still re-render it. Reach for `.memo()` when the dev render
* ledger attributes re-renders to `props` whose values did not change.
*/
memo(): [NonComparableProps] extends [never]
? REC
: MemoRequiresComparableProps>;
}
interface RecBareYield {
/** Place this REC without props: `yield* Child`. */
[Symbol.iterator](): Iterator, ReactNode>;
}
/**
* A React Effect Component. Yieldable (so `R` and `S` propagate), never a JSX
* element type — `` is a compile error by design. Props-free RECs can be
* yielded directly (`yield* Child`); RECs with props use `yield* Child.with(p)`.
* `E` carries the tagged failures its body can raise (`.catch` discharges them);
* `S` carries an unhandled loading obligation (`.suspense` discharges it);
* `V` carries an unhandled volatility obligation (`defer` at the read, or
* `.hot()`, discharges it); `L` carries the labels of the atoms the tree
* subscribes to — the set of writes that can re-render it (see
* {@link RendersOn}). `V` and `L` default so existing annotations stay valid.
*/
export type REC = RecCore
&
([Record] extends [P] ? RecBareYield : unknown);
/**
* The labels a component can re-render on — the union of label literals of
* every atom its tree subscribes to. The introspection half of the render
* channel: lock it in a type test and a new subscription (a new reason to
* re-render) becomes a compile-time diff, reviewable like any other.
*
* ```ts
* type _lock = RendersOn; // 'stats' | 'filter'
* ```
*/
// oxlint-disable-next-line typescript/no-explicit-any -- extraction position: `P`/`E` sit contravariantly in RecCore, so only `any` matches every instantiation; nothing leaks — the conditional yields `L` alone.
export type RendersOn = C extends RecCore ? L : never;
const makeRec = (
body: RecBody
,
name: string,
catchHandlers?: CatchDispatch,
suspenseFallback?: ReactNode,
defectHandler?: (defect: unknown, api: CatchApi) => ReactNode,
hotAcknowledged?: boolean,
memoized?: true,
): REC => {
// A `(props: P) => ...` body widens to the handle's `(props: any) => ...` for
// free (parameter bivariance), so no assertion is needed to store it.
const rec: RecCore
& RecBareYield = {
[RecTypeId]: true,
body,
displayName: name,
// `exactOptionalPropertyTypes`: only carry a key when its value exists, so a
// plain REC has no `catchHandlers`/`suspenseFallback` rather than undefined ones.
...(catchHandlers === undefined ? {} : { catchHandlers }),
...(suspenseFallback === undefined ? {} : { suspenseFallback }),
...(defectHandler === undefined ? {} : { defectHandler }),
...(hotAcknowledged === undefined ? {} : { hotAcknowledged }),
...(memoized === undefined ? {} : { memoized }),
with(props: P): RecPlacement {
return placement(rec, props);
},
catch(handlers: CatchHandlers): REC, R, S, V, L> {
// A handler is only ever invoked with the error whose `_tag` selected it,
// so erasing the per-tag parameter to `unknown` for storage is sound; the
// precise error type is recovered structurally at each `.catch` call site.
return makeRec
, R, S, V, L>(
body,
name,
handlers as unknown as CatchDispatch,
suspenseFallback,
defectHandler,
hotAcknowledged,
memoized,
);
},
catchDefect(handler: (defect: unknown, api: CatchApi) => ReactNode): REC {
return makeRec
(
body,
name,
catchHandlers,
suspenseFallback,
handler,
hotAcknowledged,
memoized,
);
},
suspense(fallback: ReactNode): REC
{
return makeRec
(
body,
name,
catchHandlers,
fallback,
defectHandler,
hotAcknowledged,
memoized,
);
},
hot(): REC
{
// The discharge is type-level; the stored marker only exists so the dev
// render ledger can report the acknowledged subtree (see `RecHandle`).
return makeRec
(
body,
name,
catchHandlers,
suspenseFallback,
defectHandler,
true,
memoized,
);
},
memo(): [NonComparableProps
] extends [never]
? REC
: MemoRequiresComparableProps> {
// The gate lives in the return type — the same return-position enforcement
// as `mount`'s branded errors — so the value is always the memoized REC;
// when a prop fails the gate the *type* is the branded tuple, and using
// the result as a REC is the compile error that names the offending props.
return makeRec(
body,
name,
catchHandlers,
suspenseFallback,
defectHandler,
hotAcknowledged,
true,
) as unknown as [NonComparableProps
] extends [never]
? REC
: MemoRequiresComparableProps>;
},
[Symbol.iterator](): Iterator, ReactNode> {
return placement(rec, {})[Symbol.iterator]();
},
};
Object.defineProperty(rec, 'name', { value: name });
return rec;
};
/**
* Define a React Effect Component. The body is a generator that may `yield*`
* Effect services and effects, `yield* hook(...)` for React hooks, `yield*
* suspend(...)` / `query(...)` for async data, and `yield* Child` / `yield* Child.with(props)` to
* place other RECs.
*
* The returned descriptor is server-safe: the same `mount(...)` renders a
* hook-free body on the server, and the very same value mounts on the client.
* Only a body that itself imports client-only APIs (React hooks) makes its
* *module* client-only — the `rec` wrapper never does.
*/
// The props-free overload carries a defaulted `Props` (never inferable — the
// body takes no parameter, so it is always `Record`) purely so
// its return type stays generic in `Props`: `.memo()`'s conditional return
// type then compares deferred-to-deferred against the implementation
// signature, which a concrete `REC, …>` cannot.
export function rec<
Eff extends AnyYield,
A extends ReactNode,
Props extends object = Record,
>(
body: () => Generator,
): REC<
Props,
ErrorsOf,
RequirementsOf,
SuspendsOf,
VolatilityOf,
SubscribesOf
>;
export function rec(
body: (props: Props) => Generator,
): REC<
Props,
ErrorsOf,
RequirementsOf,
SuspendsOf,
VolatilityOf,
SubscribesOf
>;
export function rec(
body: (props: Props) => Generator,
): REC<
Props,
ErrorsOf,
RequirementsOf,
SuspendsOf,
VolatilityOf,
SubscribesOf
> {
return makeRec<
Props,
ErrorsOf,
RequirementsOf,
SuspendsOf,
VolatilityOf,
SubscribesOf
>(body, body.name || 'EffractComponent');
}
/** A type error naming the services a runtime is missing for a REC's tree. */
export type MissingServices = readonly ['effract: runtime is missing', Missing];
/** A type error demanding a loading fallback for a tree that can still suspend. */
export type LoadingNotHandled = readonly [
'effract: loading not handled — add .suspense(fallback), or mount(layer, root, { loading })',
];
/** A type error demanding a scheduling decision for a tree that still reads a volatile atom. */
export type VolatileNotHandled = readonly [
'effract: volatile subscription not handled — defer(atom) the read, or mark the component .hot()',
];
/**
* The prop keys of `P` whose values `.memo()`'s bounded comparator cannot
* compare by value — anything that is neither a primitive nor an `Equal.Equal`
* implementor (a function, a plain object or array, a JSX node). `never` when
* every prop is value-comparable, which is exactly when `.memo()` compiles.
*/
type NonComparableProps = {
[K in keyof P]-?: P[K] extends string | number | boolean | bigint | symbol | null | undefined
? never
: P[K] extends Equal.Equal
? never
: K;
}[keyof P];
/** A type error naming the props whose values `.memo()` cannot soundly compare. */
export type MemoRequiresComparableProps = readonly [
'effract: .memo() requires value-comparable props — hold functions in a service and give structured data an Equal implementation; offending prop(s):',
K,
];
/**
* A no-service tree's requirement infers as `unknown` (Effect's requirement
* channel is contravariant, so `never` widens). Normalise that to `never` so a
* runtime-free tree mounts/serves under any layer.
*/
export type Effective = [unknown] extends [R] ? never : R;