Sign In

@tmonier/effract

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tmonier/effract - npm Package Compare versions

Comparing version
0.2.1
to
0.3.0
+89
dist/index.client.d.mts
import { _ as hook, a as rec, b as placement, c as Hook, d as RecBody, f as RecGenerator, g as Yieldable, h as RequirementsOf, i as RecTypeId, l as HookTypeId, m as RecPlacement, n as MissingServices, o as AnyEffect, p as RecHandle, r as REC, s as ErrorsOf, t as Effective, u as PlacementTypeId, v as isHook, y as isPlacement } from "./rec-core-Ds28YaL0.mjs";
import { ReactNode } from "react";
import * as ManagedRuntime from "effect/ManagedRuntime";
import { AtomRef } from "effect/unstable/reactivity";
import * as Layer from "effect/Layer";
//#region src/infrastructure/react/rec.d.ts
/**
* Mount a root REC under an Effect runtime — the **client** implementation of
* `mount`, selected everywhere except a React Server Component graph (where the
* sibling `../server/mount.ts` is chosen by the `react-server` condition). It is
* the boundary between effract and React: returns an ordinary React node and, at
* compile time, verifies that `layer` provides every service the REC's tree
* requires — the check lives on the `rec` argument, so there is no cast on the
* result.
*
* ```tsx
* createRoot(el).render(mount(AppLive, Dashboard));
* ```
*
* You import `mount` from `@tmonier/effract` in every file; where the module
* runs decides whether it renders interactively (here) or on the server.
*/
declare function mount<ROut, E, R>(layer: Layer.Layer<ROut, E, never>, rec: REC<Record<never, never>, R> & ([Effective<R>] extends [ROut] ? unknown : MissingServices<Exclude<Effective<R>, ROut>>)): ReactNode;
//#endregion
//#region src/infrastructure/react/runtime.d.ts
type AnyManagedRuntime = ManagedRuntime.ManagedRuntime<unknown, unknown>;
interface RuntimeProps<ROut, E> {
/** A self-contained layer (no open requirements) providing the subtree's services. */
readonly layer: Layer.Layer<ROut, E, never>;
readonly children?: ReactNode;
}
/**
* Provide an Effect runtime to a React subtree. Prefer `mount(layer, Root)`,
* which wraps the root REC in this provider and checks the tree's services at
* compile time. Reach for `Runtime` directly only to wrap non-REC React trees.
*/
declare function Runtime<ROut, E>({
layer,
children
}: RuntimeProps<ROut, E>): ReactNode;
/** Escape hatch: the underlying `ManagedRuntime`, for imperative `runPromise`/`runFork`. */
declare const useEffractRuntime: () => AnyManagedRuntime;
//#endregion
//#region src/infrastructure/react/reactivity.d.ts
type Ref<A> = AtomRef.AtomRef<A>;
/** Read an atom inside `observe`, subscribing the component to it. */
type Read = <A>(ref: Ref<A>) => A;
/** Create a reactive cell. Sugar for `AtomRef.make`. */
declare const atom: <A>(initial: A) => Ref<A>;
/**
* Subscribe to a derived view over one or more atoms. Re-renders precisely when
* a read atom changes.
*
* ```tsx
* const doubled = observe(($) => $(count) * 2);
* ```
*/
declare const observe: <A>(selector: (read: Read) => A) => A;
/** Read a single atom's value, subscribing the component to it. */
declare const useAtomValue: <A>(ref: Ref<A>) => A;
/** A stable setter for an atom, supporting both values and updater functions. */
declare const useAtomSet: <A>(ref: Ref<A>) => ((value: A | ((prev: A) => A)) => void);
/** Read and write a single atom — the `useState` shape, backed by Effect. */
declare const useAtom: <A>(ref: Ref<A>) => readonly [A, (value: A | ((prev: A) => A)) => void];
interface ObserveProps<A extends ReactNode> {
readonly children: (read: Read) => A;
}
/** The render-prop form of {@link observe}. */
declare const Observe: <A extends ReactNode>({
children
}: ObserveProps<A>) => ReactNode;
//#endregion
//#region src/index.client.d.ts
/**
* effract — write React components as Effect programs.
*
* This is the **client-graph** entry (the `default` export condition). Whether a
* component runs in a SPA, during SSR + hydration, or as a client island inside
* an RSC page, it renders here through the in-render interpreter. A React Server
* Component graph resolves the sibling `index.server`
* entry instead (via the package's `react-server` condition) — so you import
* `mount` from `@tmonier/effract` everywhere and the bundler picks where it runs.
*
* @packageDocumentation
*/
declare const VERSION = "0.1.0";
//#endregion
export { type AnyEffect, type Effective, type ErrorsOf, type Hook, HookTypeId, type MissingServices, Observe, type ObserveProps, PlacementTypeId, type REC, type Read, type RecBody, type RecGenerator, type RecHandle, type RecPlacement, RecTypeId, type RequirementsOf, Runtime, type RuntimeProps, VERSION, type Yieldable, atom, hook, isHook, isPlacement, mount, observe, placement, rec, useAtom, useAtomSet, useAtomValue, useEffractRuntime };
import { a as hook, c as placement, i as PlacementTypeId, n as rec, o as isHook, r as HookTypeId, s as isPlacement, t as RecTypeId } from "./rec-core-D7Bs_IJ7.mjs";
import { createContext, createElement, use, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as ManagedRuntime from "effect/ManagedRuntime";
import { jsx } from "react/jsx-runtime";
import * as Equal from "effect/Equal";
import { AtomRef } from "effect/unstable/reactivity";
//#region src/application/interpreter.ts
/**
* The interpreter — the bridge between React's fiber and Effect's fiber.
*
* React drives a component by calling its function during a render pass.
* `driveRec` runs *inside* that pass: it walks the component's generator
* synchronously, and for every `yield*` it decides who answers.
*
* - a lifted hook → the React hook already ran inline; unwrap its value
* - a service Tag → resolve it synchronously from the runtime's context
* - a sync Effect → run it synchronously, return its value
* - an async Effect → suspend through React's `use`, resuming on the retry
*
* Because the walk is synchronous and deterministic, the user's hook calls keep
* a stable order across renders — they are, and remain, ordinary React hooks.
* Nothing here forks React's reconciler; it cooperates with it.
*/
/**
* Resolve a single yielded Effect against the runtime. Synchronous effects
* (services, pure computation, ref reads) return immediately. An effect that
* cannot finish synchronously surfaces as an `AsyncFiberError`; we route it
* through React Suspense with a promise cached by encounter order, so the
* retry after the promise settles returns the value inline. Any other failure
* is a real error and is thrown to the nearest React error boundary.
*/
const resolveEffect = (effect, deps, state) => {
if (!Effect.isEffect(effect)) throw new TypeError("effract: a component body yielded a value that is neither an Effect nor a hook(...). Wrap React hooks with `hook(...)`, e.g. `yield* hook(useState(0))`.");
const exit = deps.executor.runSyncExit(effect);
if (Exit.isSuccess(exit)) return exit.value;
const squashed = Cause.squash(exit.cause);
if (Cause.isAsyncFiberError(squashed)) {
const index = state.index++;
let slot = deps.cache.get(index);
if (slot === void 0) {
slot = { promise: deps.executor.runPromise(effect) };
deps.cache.set(index, slot);
}
return deps.suspender.use(slot.promise);
}
throw squashed;
};
/**
* Run a React Effect Component body to its rendered result. Creates a fresh
* generator per render (generators are single-use); a Suspense retry simply
* runs this again from the top, replaying hooks in order and hitting the async
* cache for already-started work.
*/
const driveRec = (gen, deps) => {
const state = { index: 0 };
let step = gen.next();
while (!step.done) {
const instruction = step.value;
let result;
if (isHook(instruction)) result = instruction.value;
else if (isPlacement(instruction)) result = deps.placer.place(instruction);
else result = resolveEffect(instruction, deps, state);
step = gen.next(result);
}
return step.value;
};
//#endregion
//#region src/infrastructure/react/runtime.tsx
/**
* The runtime provider that `mount` wraps your tree in. It builds an Effect
* `ManagedRuntime` once from a `Layer` and hands it down through React context,
* where every effract component reads it. This is the seam where "server vs
* client" lives: provide a browser layer and the same components run in a SPA;
* provide a server layer and they run under Node or Bun — the
* components never change. Use `mount(layer, Root)`; `Runtime` is the low-level
* provider underneath it.
*
* Services are resolved up-front into the runtime's context (the RSC-style
* "resolve near the root" mode), so reading a service inside a component is a
* synchronous context lookup, not an async round-trip.
*/
const RuntimeContext = createContext(null);
const executorFromRuntime = (runtime) => ({
runSyncExit: (effect) => runtime.runSyncExit(effect),
runPromise: (effect) => runtime.runPromise(effect)
});
/**
* Provide an Effect runtime to a React subtree. Prefer `mount(layer, Root)`,
* which wraps the root REC in this provider and checks the tree's services at
* compile time. Reach for `Runtime` directly only to wrap non-REC React trees.
*/
function Runtime({ layer, children }) {
const runtimeRef = useRef(null);
if (runtimeRef.current === null) runtimeRef.current = ManagedRuntime.make(layer);
const runtime = runtimeRef.current;
const value = useMemo(() => ({
executor: executorFromRuntime(runtime),
runtime
}), [runtime]);
useEffect(() => () => void runtime.dispose(), [runtime]);
return /* @__PURE__ */ jsx(RuntimeContext.Provider, {
value,
children
});
}
const useRuntimeContext = () => {
const value = useContext(RuntimeContext);
if (value === null) throw new Error("effract: no runtime found above this component. Mount your root with mount(layer, Root).");
return value;
};
/** Internal: the executor the interpreter runs effects through. */
const useExecutor = () => useRuntimeContext().executor;
/** Escape hatch: the underlying `ManagedRuntime`, for imperative `runPromise`/`runFork`. */
const useEffractRuntime = () => useRuntimeContext().runtime;
//#endregion
//#region src/infrastructure/react/rec.tsx
/**
* The client binding for React Effect Components: it turns a (runtime-agnostic)
* REC descriptor into a real React component whose body effract interprets
* *inside* React's render pass, and provides `mount`, the boundary that supplies
* the browser runtime.
*
* const Dashboard = rec(function* () {
* const stats = yield* Stats; // a service
* const [n, setN] = yield* hook(useState(0)); // a real React hook
* return <main>{yield* StatBadge}{n}</main>; // a child REC, yielded
* });
*
* mount(AppLive, Dashboard); // ← compile error if AppLive lacks a needed service
*
* The descriptor itself (`rec`) lives in `../rec-core.tsx` and is
* server-safe; this module adds only the client half — the in-render
* interpreter and hook dispatch — so the same `rec(...)` value also `serve`s on
* the server. At runtime a REC renders as an ordinary React component (own
* fiber, hooks, reconciliation); the yield is only how it is placed and how `R`
* flows.
*/
const useSuspender = () => ({ use });
const useRenderCache = () => {
const ref = useRef(null);
if (ref.current === null) ref.current = /* @__PURE__ */ new Map();
return ref.current;
};
/**
* One React component per descriptor identity, cached so a yield-composed child
* keeps a stable React type across the parent's re-renders (React would
* otherwise remount it every render). The component interprets the descriptor's
* body in-render, resolving services, hooks, and nested placements.
*/
const fcCache = /* @__PURE__ */ new WeakMap();
const clientFcFor = (handle) => {
const cached = fcCache.get(handle);
if (cached !== void 0) return cached;
const fc = (props) => {
const executor = useExecutor();
const suspender = useSuspender();
const cache = useRenderCache();
return driveRec(handle.body(props), {
executor,
suspender,
cache,
placer: clientPlacer
});
};
Object.defineProperty(fc, "name", { value: handle.displayName });
fcCache.set(handle, fc);
return fc;
};
/**
* Places a child REC as a real React child element under the same runtime. The
* `place` method's parameter is bivariant (see {@link Placer}), so it names the
* ReactNode-bodied child it renders here — the interpreter still hands it the
* erased placement, and no cast is needed on either side.
*/
const clientPlacer = { place: (placement) => createElement(clientFcFor(placement.rec), placement.props) };
/**
* Mount a root REC under an Effect runtime — the **client** implementation of
* `mount`, selected everywhere except a React Server Component graph (where the
* sibling `../server/mount.ts` is chosen by the `react-server` condition). It is
* the boundary between effract and React: returns an ordinary React node and, at
* compile time, verifies that `layer` provides every service the REC's tree
* requires — the check lives on the `rec` argument, so there is no cast on the
* result.
*
* ```tsx
* createRoot(el).render(mount(AppLive, Dashboard));
* ```
*
* You import `mount` from `@tmonier/effract` in every file; where the module
* runs decides whether it renders interactively (here) or on the server.
*/
function mount(layer, rec) {
const root = createElement(clientFcFor(rec), {});
return createElement(Runtime, { layer }, root);
}
//#endregion
//#region src/infrastructure/react/reactivity.tsx
/**
* The signals bridge. Effect's reactive cell is `AtomRef`; this binds it to
* React so a component re-renders precisely when — and only when — an atom it
* actually read changes.
*
* observe($ => $(count) * 2) // a hook: read + auto-subscribe
* <Observe>{$ => <b>{$(count)}</b>}</Observe> // the same, as an element
* const [n, setN] = useAtom(count) // read + write a single atom
*
* `observe` tracks exactly the atoms touched during its selector and subscribes
* to that set, re-tracking on every change so dynamic dependencies stay
* correct. No `Effect.runSync` at the call site, no manual dependency arrays.
*/
/** Create a reactive cell. Sugar for `AtomRef.make`. */
const atom = (initial) => AtomRef.make(initial);
const depsEqual = (a, b) => {
if (a.size !== b.size) return false;
for (const [ref, value] of a) if (!b.has(ref) || !Equal.equals(value, b.get(ref))) return false;
return true;
};
/**
* Run the selector, tracking which atoms it reads. Returns a *stable* reference
* when the tracked atoms and their values are unchanged, so it is safe to call
* from `getSnapshot` without provoking a render loop.
*/
const compute = (state) => {
const nextDeps = /* @__PURE__ */ new Map();
const read = (ref) => {
const value = ref.value;
nextDeps.set(ref, value);
return value;
};
const next = state.selector(read);
if (state.initialized && depsEqual(state.deps, nextDeps)) {
state.deps = nextDeps;
return state.value;
}
state.deps = nextDeps;
state.value = next;
state.initialized = true;
return next;
};
/**
* Subscribe to a derived view over one or more atoms. Re-renders precisely when
* a read atom changes.
*
* ```tsx
* const doubled = observe(($) => $(count) * 2);
* ```
*/
const observe = (selector) => {
const stateRef = useRef(null);
if (stateRef.current === null) stateRef.current = {
selector,
deps: /* @__PURE__ */ new Map(),
value: void 0,
initialized: false
};
stateRef.current.selector = selector;
const subscribe = useCallback((onStoreChange) => {
const state = stateRef.current;
if (state === null) return () => {};
let unsubscribes = [];
const resubscribe = () => {
for (const unsub of unsubscribes) unsub();
unsubscribes = [...state.deps.keys()].map((ref) => ref.subscribe(handleChange));
};
function handleChange() {
compute(state);
resubscribe();
onStoreChange();
}
compute(state);
resubscribe();
return () => {
for (const unsub of unsubscribes) unsub();
};
}, []);
const getSnapshot = useCallback(() => compute(stateRef.current), []);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
};
/** Read a single atom's value, subscribing the component to it. */
const useAtomValue = (ref) => {
const subscribe = useCallback((onStoreChange) => ref.subscribe(onStoreChange), [ref]);
const getSnapshot = useCallback(() => ref.value, [ref]);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
};
/** A stable setter for an atom, supporting both values and updater functions. */
const useAtomSet = (ref) => useCallback((value) => {
if (typeof value === "function") ref.update(value);
else ref.set(value);
}, [ref]);
/** Read and write a single atom — the `useState` shape, backed by Effect. */
const useAtom = (ref) => [useAtomValue(ref), useAtomSet(ref)];
/** The render-prop form of {@link observe}. */
const Observe = ({ children }) => observe(children);
//#endregion
//#region src/index.client.ts
/**
* effract — write React components as Effect programs.
*
* This is the **client-graph** entry (the `default` export condition). Whether a
* component runs in a SPA, during SSR + hydration, or as a client island inside
* an RSC page, it renders here through the in-render interpreter. A React Server
* Component graph resolves the sibling `index.server`
* entry instead (via the package's `react-server` condition) — so you import
* `mount` from `@tmonier/effract` everywhere and the bundler picks where it runs.
*
* @packageDocumentation
*/
const VERSION = "0.1.0";
//#endregion
export { HookTypeId, Observe, PlacementTypeId, RecTypeId, Runtime, VERSION, atom, hook, isHook, isPlacement, mount, observe, placement, rec, useAtom, useAtomSet, useAtomValue, useEffractRuntime };
import { a as rec, b as placement, d as RecBody, f as RecGenerator, g as Yieldable, h as RequirementsOf, i as RecTypeId, m as RecPlacement, n as MissingServices, o as AnyEffect, p as RecHandle, r as REC, s as ErrorsOf, t as Effective, u as PlacementTypeId, y as isPlacement } from "./rec-core-Ds28YaL0.mjs";
import { ReactNode } from "react";
import * as Layer from "effect/Layer";
//#region src/infrastructure/server/mount.d.ts
/**
* Mount a root REC as an async React Server Component under an Effect runtime.
* Returns a component you export or place (`export default mount(AppLive, Page)`
* / `<Page />`). Verifies at compile time that `layer` provides every service the
* tree requires — the check lives on the `rec` argument, so there is no cast at
* the call site. Hook-bearing bodies reject at render (a hook is a client concept
* RSC forbids); those RECs are client islands and render wherever the browser is.
*/
declare function mount<ROut, E, R>(layer: Layer.Layer<ROut, E, never>, rec: REC<Record<never, never>, R> & ([Effective<R>] extends [ROut] ? unknown : MissingServices<Exclude<Effective<R>, ROut>>)): () => Promise<ReactNode>;
//#endregion
//#region src/application/server-driver.d.ts
/** Runs an effect against the in-scope runtime, resolving services and async work. */
type RunEffect = (effect: AnyEffect) => Promise<unknown>;
/**
* Drive a REC body to its rendered node on the server. Three kinds of yield are
* honoured, mirroring the client interpreter:
*
* - a child placement (`yield* Child`) → the child's body is driven *inline*
* against the same runtime, so a universal REC composed of universal RECs
* renders entirely on the server, with no client JavaScript.
* - an Effect/service → awaited against the request's runtime.
* - a hook → rejected: React Server Components have no render-pass hooks (this
* is React's rule, not effract's), so a hook-bearing body is a client REC.
*/
declare const driveServerRec: <A>(gen: RecGenerator<A>, run: RunEffect) => Promise<A>;
//#endregion
//#region src/index.server.d.ts
/**
* effract — the **server-graph** entry (the `react-server` export condition).
*
* A React Server Component graph resolves `@tmonier/effract` to *this* module.
* It exposes the same authoring surface — `rec` and the yield protocol — plus a
* `mount` that renders on the server: it drives the REC
* against an Effect runtime and returns an async React Server Component, with no
* hooks and no client JavaScript. You never choose it: import `mount` from
* `@tmonier/effract` and the bundler hands server components this version and
* client components the sibling `index.client` one.
*
* Client-only APIs (`Runtime`, `observe`, `atom`, …) are intentionally absent
* here — a component that needs them is a client island, and lives in the client
* graph.
*
* @packageDocumentation
*/
declare const VERSION = "0.1.0";
//#endregion
export { type AnyEffect, type Effective, type ErrorsOf, type MissingServices, PlacementTypeId, type REC, type RecBody, type RecGenerator, type RecHandle, type RecPlacement, RecTypeId, type RequirementsOf, type RunEffect, VERSION, type Yieldable, driveServerRec, isPlacement, mount, placement, rec };
import { c as placement, i as PlacementTypeId, n as rec, o as isHook, s as isPlacement, t as RecTypeId } from "./rec-core-D7Bs_IJ7.mjs";
import * as ManagedRuntime from "effect/ManagedRuntime";
//#region src/application/server-driver.ts
/**
* The server-side counterpart to the in-render interpreter. On the server there
* is no render pass to suspend and no hooks to honour — a React Server Component
* may simply be `async` and await its data. So this driver walks the same REC
* generator a component body produces, but resolves each yielded effect by
* awaiting it against the request's Effect runtime, and drives placed children
* inline (so a universal tree renders entirely on the server, no client JS).
*
* The body is identical to the one the client interprets; only who answers the
* yields differs. That is the whole point: one component, two fibers.
*/
/**
* Drive a REC body to its rendered node on the server. Three kinds of yield are
* honoured, mirroring the client interpreter:
*
* - a child placement (`yield* Child`) → the child's body is driven *inline*
* against the same runtime, so a universal REC composed of universal RECs
* renders entirely on the server, with no client JavaScript.
* - an Effect/service → awaited against the request's runtime.
* - a hook → rejected: React Server Components have no render-pass hooks (this
* is React's rule, not effract's), so a hook-bearing body is a client REC.
*/
const driveServerRec = async (gen, run) => {
let step = gen.next();
while (!step.done) {
const instruction = step.value;
if (isHook(instruction)) throw new Error("effract: React hooks are not available in Server Components. Yield only Effect services/effects here, or render this component on the client (a hook-bearing REC is a client island — it renders wherever the browser runtime is).");
if (isPlacement(instruction)) {
const node = await driveServerRec(instruction.rec.body(instruction.props), run);
step = gen.next(node);
continue;
}
const value = await run(instruction);
step = gen.next(value);
}
return step.value;
};
//#endregion
//#region src/infrastructure/server/mount.ts
/**
* One runtime per layer identity — the server analogue of the client `mount`'s
* per-boundary runtime. A concrete `ManagedRuntime<ROut, E>` widens to
* `ServerRuntime` (requirements are contravariant), so building it needs no cast.
*/
const runtimes = /* @__PURE__ */ new WeakMap();
const runtimeFor = (layer) => {
const existing = runtimes.get(layer);
if (existing !== void 0) return existing;
const runtime = ManagedRuntime.make(layer);
runtimes.set(layer, runtime);
return runtime;
};
/**
* Mount a root REC as an async React Server Component under an Effect runtime.
* Returns a component you export or place (`export default mount(AppLive, Page)`
* / `<Page />`). Verifies at compile time that `layer` provides every service the
* tree requires — the check lives on the `rec` argument, so there is no cast at
* the call site. Hook-bearing bodies reject at render (a hook is a client concept
* RSC forbids); those RECs are client islands and render wherever the browser is.
*/
function mount(layer, rec) {
const Root = () => {
const runtime = runtimeFor(layer);
return driveServerRec(rec.body({}), (effect) => runtime.runPromise(effect));
};
Object.defineProperty(Root, "name", { value: rec.displayName });
return Root;
}
//#endregion
//#region src/index.server.ts
/**
* effract — the **server-graph** entry (the `react-server` export condition).
*
* A React Server Component graph resolves `@tmonier/effract` to *this* module.
* It exposes the same authoring surface — `rec` and the yield protocol — plus a
* `mount` that renders on the server: it drives the REC
* against an Effect runtime and returns an async React Server Component, with no
* hooks and no client JavaScript. You never choose it: import `mount` from
* `@tmonier/effract` and the bundler hands server components this version and
* client components the sibling `index.client` one.
*
* Client-only APIs (`Runtime`, `observe`, `atom`, …) are intentionally absent
* here — a component that needs them is a client island, and lives in the client
* graph.
*
* @packageDocumentation
*/
const VERSION = "0.1.0";
//#endregion
export { PlacementTypeId, RecTypeId, VERSION, driveServerRec, isPlacement, mount, placement, rec };
//#region src/domain/protocol.ts
/** Brand identifying a lifted React hook instruction. */
const HookTypeId = Symbol.for("@tmonier/effract/Hook");
/**
* Lift an already-evaluated React hook result into the effract yield channel.
*
* ```ts
* const [tab, setTab] = yield* hook(useState('overview'));
* const ref = yield* hook(useRef<HTMLDivElement>(null));
* ```
*
* Because the component body runs synchronously inside React's render pass, the
* wrapped hook call obeys the Rules of Hooks: same order on every render.
*/
const hook = (value) => {
const self = {
[HookTypeId]: true,
value,
[Symbol.iterator]() {
let yielded = false;
return { next(sent) {
if (yielded) return {
done: true,
value: sent
};
yielded = true;
return {
done: false,
value: self
};
} };
}
};
return self;
};
/** Type guard: is this yielded instruction a lifted hook? */
const isHook = (u) => typeof u === "object" && u !== null && HookTypeId in u;
/** Brand identifying a child-REC placement instruction. */
const PlacementTypeId = Symbol.for("@tmonier/effract/Placement");
/**
* Construct a child placement. Single-shot iterable like {@link hook}: `yield*`
* hands the interpreter the placement instruction, and the value fed back in
* (the rendered child node) becomes the result of the `yield*`.
*/
const placement = (rec, props) => {
const self = {
[PlacementTypeId]: true,
rec,
props,
[Symbol.iterator]() {
let yielded = false;
return { next(sent) {
if (yielded) return {
done: true,
value: sent
};
yielded = true;
return {
done: false,
value: self
};
} };
}
};
return self;
};
/** Type guard: is this yielded instruction a child-REC placement? */
const isPlacement = (u) => typeof u === "object" && u !== null && PlacementTypeId in u;
//#endregion
//#region src/infrastructure/rec-core.tsx
/** Brand identifying a React Effect Component. */
const RecTypeId = Symbol.for("@tmonier/effract/Rec");
const makeRec = (body, name) => {
const rec = {
[RecTypeId]: true,
body,
displayName: name,
with(props) {
return placement(rec, props);
},
[Symbol.iterator]() {
return placement(rec, {})[Symbol.iterator]();
}
};
Object.defineProperty(rec, "name", { value: name });
return rec;
};
function rec(body) {
return makeRec(body, body.name || "EffractComponent");
}
//#endregion
export { hook as a, placement as c, PlacementTypeId as i, rec as n, isHook as o, HookTypeId as r, isPlacement as s, RecTypeId as t };
import { ReactNode } from "react";
import * as Effect from "effect/Effect";
//#region src/domain/protocol.d.ts
/**
* The one unavoidable `any` in the framework. The heterogeneous yield protocol
* must accept an Effect of *any* error and requirement variance — there is no
* other way to express "some Effect" as a generic constraint, which is why
* Effect's own `Effect.gen` is typed exactly this way. Precise `E` and `R` are
* recovered below through conditional inference, so this never leaks to users.
*/
type AnyEffect = Effect.Effect<any, any, any>;
/** Brand identifying a lifted React hook instruction. */
declare const HookTypeId: unique symbol;
type HookTypeId = typeof HookTypeId;
/**
* A React hook result lifted into the `yield*` channel. The hook itself has
* already executed (synchronously, during render) by the time the value is
* wrapped — `hook(useState(0))` calls `useState` inline. Wrapping it only makes
* the result yieldable, so a component body reads as one uniform stream of
* `yield*`s whether the value comes from Effect or from React.
*/
interface Hook<out A> {
readonly [HookTypeId]: true;
readonly value: A;
[Symbol.iterator](): Iterator<Hook<A>, A>;
}
/**
* Lift an already-evaluated React hook result into the effract yield channel.
*
* ```ts
* const [tab, setTab] = yield* hook(useState('overview'));
* const ref = yield* hook(useRef<HTMLDivElement>(null));
* ```
*
* Because the component body runs synchronously inside React's render pass, the
* wrapped hook call obeys the Rules of Hooks: same order on every render.
*/
declare const hook: <A>(value: A) => Hook<A>;
/** Type guard: is this yielded instruction a lifted hook? */
declare const isHook: (u: unknown) => u is Hook<unknown>;
/** Everything a component body may `yield*`: an Effect, a lifted hook, or a child placement. */
type Yieldable<A> = Effect.Effect<A, unknown, unknown> | Hook<A> | RecPlacement<A, unknown>;
/** The generator a React Effect Component body produces. */
type RecGenerator<A> = Generator<AnyEffect | Hook<unknown> | RecPlacement<unknown, unknown>, A, unknown>;
/** A component body: props in, a generator of yields ending in a rendered `A`. */
type RecBody<Props, A> = (props: Props) => RecGenerator<A>;
/**
* A stable handle to a component's body — the identity a placement points at.
* The client keys its per-descriptor React component on this object (so a child
* keeps a stable React type across re-renders); the server drives its `body`
* directly. Deliberately renderer-agnostic: it names *what* to run, not how.
*/
interface RecHandle<A> {
readonly body: (props: any) => RecGenerator<A>;
readonly displayName: string;
}
/** Brand identifying a child-REC placement instruction. */
declare const PlacementTypeId: unique symbol;
type PlacementTypeId = typeof PlacementTypeId;
/**
* The third kind of yieldable, alongside Effects and Hooks: the placement of a
* *child* REC into a parent's tree (`yield* Child` / `yield* Child.with(p)`).
*
* A placement carries only data — the child's stable `rec` handle and its
* `props` — never a bound React element. That is what lets one `rec(...)` value
* be shared across runtimes: the client turns a placement into a real React
* child fiber, the server drives the child's body inline. The phantom `R` makes
* the child's Effect requirements flow up through `yield*` to `mount`/`serve`,
* exactly as a yielded service's requirements do.
*/
interface RecPlacement<A, R> {
readonly [PlacementTypeId]: true;
readonly rec: RecHandle<A>;
readonly props: object;
/**
* Phantom carrier for the requirements this placement contributes upward.
* Covariant (an output position) so a concrete placement — e.g. one needing
* `Stats` — widens to `RecPlacement<_, unknown>` in a body's yield union, just
* as `Effect<_, _, Stats>` widens to `AnyEffect`. Never set at runtime.
*/
readonly _requirements?: R;
[Symbol.iterator](): Iterator<RecPlacement<A, R>, A>;
}
/**
* Construct a child placement. Single-shot iterable like {@link hook}: `yield*`
* hands the interpreter the placement instruction, and the value fed back in
* (the rendered child node) becomes the result of the `yield*`.
*/
declare const placement: <A, R>(rec: RecHandle<A>, props: object) => RecPlacement<A, R>;
/** Type guard: is this yielded instruction a child-REC placement? */
declare const isPlacement: (u: unknown) => u is RecPlacement<unknown, unknown>;
/**
* Distribute over the yield union and keep only its Effect members. Hooks are
* not Effects, so they drop away — leaving just what carries `E` and `R`.
*/
type EffectsOnly<Eff> = Eff extends AnyEffect ? Eff : never;
/**
* Re-express each yielded placement as an effect carrying only its child's
* requirements, so a placed child's `R` joins the parent's the same way a
* yielded service's does — one child, its whole subtree's services bubble up.
*/
type PlacementsAsEffects<Eff> = Eff extends RecPlacement<unknown, infer R> ? Effect.Effect<unknown, unknown, R> : never;
/**
* Recover the Effect requirement channel `R` from everything a body yields —
* services, effects, and placed child RECs. A body that needs both `A` and `B`
* requires `A & B`, which is exactly the intersection TypeScript infers from
* the contravariant requirement slot.
*/
type RequirementsOf<Eff> = [EffectsOnly<Eff> | PlacementsAsEffects<Eff>] extends [Effect.Effect<unknown, unknown, infer R>] ? R : never;
/** Recover the Effect error channel `E` (a union — any yielded effect may fail). */
type ErrorsOf<Eff> = [EffectsOnly<Eff>] extends [Effect.Effect<unknown, infer E, unknown>] ? E : never;
//#endregion
//#region src/infrastructure/rec-core.d.ts
/** Brand identifying a React Effect Component. */
declare const RecTypeId: unique symbol;
type RecTypeId = typeof RecTypeId;
interface RecCore<P, R> extends RecHandle<ReactNode> {
readonly [RecTypeId]: true;
/** Place this REC with props: `yield* Child.with({ ... })`. */
with(props: P): RecPlacement<ReactNode, R>;
}
interface RecBareYield<R> {
/** Place this REC without props: `yield* Child`. */
[Symbol.iterator](): Iterator<RecPlacement<ReactNode, R>, ReactNode>;
}
/**
* A React Effect Component. Yieldable (so `R` propagates), never a JSX element
* type — `<Rec />` is a compile error by design. Props-free RECs can be yielded
* directly (`yield* Child`); RECs with props use `yield* Child.with(props)`.
*/
type REC<P, R> = RecCore<P, R> & ([Record<never, never>] extends [P] ? RecBareYield<R> : unknown);
/**
* Define a React Effect Component. The body is a generator that may `yield*`
* Effect services and effects, `yield* hook(...)` for React hooks, 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.
*/
declare function rec<Eff extends AnyEffect | Hook<unknown> | RecPlacement<ReactNode, unknown>, A extends ReactNode>(body: () => Generator<Eff, A, never>): REC<Record<never, never>, RequirementsOf<Eff>>;
declare function rec<Eff extends AnyEffect | Hook<unknown> | RecPlacement<ReactNode, unknown>, A extends ReactNode, Props extends object>(body: (props: Props) => Generator<Eff, A, never>): REC<Props, RequirementsOf<Eff>>;
/** A type error naming the services a runtime is missing for a REC's tree. */
type MissingServices<Missing> = readonly ['effract: runtime is missing', Missing];
/**
* 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.
*/
type Effective<R> = [unknown] extends [R] ? never : R;
//#endregion
export { hook as _, rec as a, placement as b, Hook as c, RecBody as d, RecGenerator as f, Yieldable as g, RequirementsOf as h, RecTypeId as i, HookTypeId as l, RecPlacement as m, MissingServices as n, AnyEffect as o, RecHandle as p, REC as r, ErrorsOf as s, Effective as t, PlacementTypeId as u, isHook as v, isPlacement as y };
/**
* The server driver, exercised without React. It proves the RSC half of the
* thesis: the same REC body the client interprets resolves on the server by
* awaiting its yields against an Effect runtime, drives placed children inline,
* and rejects hooks (which RSC forbids) with a clear error.
*/
import { describe, expect, it } from 'vitest';
import * as Context from 'effect/Context';
import * as Effect from 'effect/Effect';
import * as Layer from 'effect/Layer';
import * as ManagedRuntime from 'effect/ManagedRuntime';
import { hook, placement, type RecHandle } from '#domain/protocol.ts';
import { driveServerRec, type RunEffect } from '#application/server-driver.ts';
class Stats extends Context.Service<Stats, { readonly total: number }>()('test/Stats') {}
const runnerFor = (layer: Layer.Layer<never, never, never>): RunEffect => {
const runtime = ManagedRuntime.make(layer) as unknown as ManagedRuntime.ManagedRuntime<
unknown,
unknown
>;
return (effect) => runtime.runPromise(effect);
};
describe('driveServerRec', () => {
it('resolves services and awaits async effects', async () => {
const body = function* () {
const stats = yield* Stats;
const delayed = yield* Effect.promise(() => Promise.resolve(2));
return `${stats.total}x${delayed}`;
};
const result = await driveServerRec(body(), runnerFor(Layer.succeed(Stats)({ total: 5 })));
expect(result).toBe('5x2');
});
it('rejects React hooks — RSC has no render-pass hooks', async () => {
const body = function* () {
const value = yield* hook(1);
return value;
};
await expect(driveServerRec(body(), runnerFor(Layer.empty))).rejects.toThrow(
/hooks are not available/,
);
});
it('drives a placed child REC inline — universal composition, no client JS', async () => {
// A child body and a placement of it, built from the pure protocol (no
// renderer) — the interpreter drives the placed child on the server too.
const childBody = function* () {
const stats = yield* Stats;
return `child:${stats.total}`;
};
const child: RecHandle<string> = { body: childBody, displayName: 'Child' };
const parent = function* () {
const rendered = yield* placement(child, {});
return ['parent', rendered];
};
const node = await driveServerRec(parent(), runnerFor(Layer.succeed(Stats)({ total: 7 })));
expect(node).toEqual(['parent', 'child:7']);
});
});
/**
* The server-side counterpart to the in-render interpreter. On the server there
* is no render pass to suspend and no hooks to honour — a React Server Component
* may simply be `async` and await its data. So this driver walks the same REC
* generator a component body produces, but resolves each yielded effect by
* awaiting it against the request's Effect runtime, and drives placed children
* inline (so a universal tree renders entirely on the server, no client JS).
*
* The body is identical to the one the client interprets; only who answers the
* yields differs. That is the whole point: one component, two fibers.
*/
import { isHook, isPlacement, type AnyEffect, type RecGenerator } from '#domain/protocol.ts';
/** Runs an effect against the in-scope runtime, resolving services and async work. */
export type RunEffect = (effect: AnyEffect) => Promise<unknown>;
/**
* Drive a REC body to its rendered node on the server. Three kinds of yield are
* honoured, mirroring the client interpreter:
*
* - a child placement (`yield* Child`) → the child's body is driven *inline*
* against the same runtime, so a universal REC composed of universal RECs
* renders entirely on the server, with no client JavaScript.
* - an Effect/service → awaited against the request's runtime.
* - a hook → rejected: React Server Components have no render-pass hooks (this
* is React's rule, not effract's), so a hook-bearing body is a client REC.
*/
export const driveServerRec = async <A>(gen: RecGenerator<A>, run: RunEffect): Promise<A> => {
let step = gen.next();
while (!step.done) {
const instruction = step.value;
if (isHook(instruction)) {
throw new Error(
'effract: React hooks are not available in Server Components. Yield only Effect ' +
'services/effects here, or render this component on the client (a hook-bearing REC is a ' +
'client island — it renders wherever the browser runtime is).',
);
}
if (isPlacement(instruction)) {
const node = await driveServerRec(instruction.rec.body(instruction.props), run);
step = gen.next(node);
continue;
}
// Hooks and placements are handled above, so this is an Effect.
const value = await run(instruction);
step = gen.next(value);
}
return step.value;
};
/**
* effract — write React components as Effect programs.
*
* This is the **client-graph** entry (the `default` export condition). Whether a
* component runs in a SPA, during SSR + hydration, or as a client island inside
* an RSC page, it renders here through the in-render interpreter. A React Server
* Component graph resolves the sibling `index.server`
* entry instead (via the package's `react-server` condition) — so you import
* `mount` from `@tmonier/effract` everywhere and the bundler picks where it runs.
*
* @packageDocumentation
*/
export const VERSION = '0.1.0';
// --- the yield protocol ---
export {
hook,
isHook,
HookTypeId,
isPlacement,
placement,
PlacementTypeId,
} from '#domain/protocol.ts';
export type {
AnyEffect,
Hook,
Yieldable,
RecBody,
RecGenerator,
RecHandle,
RecPlacement,
RequirementsOf,
ErrorsOf,
} from '#domain/protocol.ts';
// --- components ---
// `rec` comes from the server-safe descriptor module directly — NOT via the
// `'use client'` `react/rec.tsx` — so a server module importing `rec` is not
// tagged as a client function by an RSC bundler. `mount` here is the client one.
export { rec, RecTypeId } from '#infrastructure/rec-core.tsx';
export type { REC, MissingServices, Effective } from '#infrastructure/rec-core.tsx';
export { mount } from '#infrastructure/react/rec.tsx';
// --- the runtime boundary (mount is canonical; Runtime is the low-level provider) ---
export { Runtime, useEffractRuntime } from '#infrastructure/react/runtime.tsx';
export type { RuntimeProps } from '#infrastructure/react/runtime.tsx';
// --- reactivity ---
export {
observe,
Observe,
atom,
useAtom,
useAtomValue,
useAtomSet,
} from '#infrastructure/react/reactivity.tsx';
export type { Read, ObserveProps } from '#infrastructure/react/reactivity.tsx';
/**
* The server entry's public surface. Client-only APIs — hooks and signals — must
* NOT be exported here: a Server Component resolves `@tmonier/effract` through the
* `react-server` condition to this entry, so their *absence* is exactly what makes
* `hook(...)` / `observe(...)` a compile error in a Server Component instead of a
* runtime surprise. This test guards that surface so the enforcement can't
* silently regress (a `next build` proves it end-to-end; this proves it in unit).
*/
import { describe, expect, it } from 'vitest';
import * as server from './index.server.ts';
describe('server entry surface (react-server condition)', () => {
it('exports the authoring + server-render API', () => {
expect(typeof server.rec).toBe('function');
expect(typeof server.mount).toBe('function');
expect(typeof server.driveServerRec).toBe('function');
});
it('does NOT export client-only APIs — they cannot exist in a Server Component', () => {
for (const name of ['hook', 'observe', 'atom', 'useAtom', 'useAtomValue', 'Runtime', 'view']) {
expect(name in server, `${name} must not be exported from the server entry`).toBe(false);
}
});
});
/**
* effract — the **server-graph** entry (the `react-server` export condition).
*
* A React Server Component graph resolves `@tmonier/effract` to *this* module.
* It exposes the same authoring surface — `rec` and the yield protocol — plus a
* `mount` that renders on the server: it drives the REC
* against an Effect runtime and returns an async React Server Component, with no
* hooks and no client JavaScript. You never choose it: import `mount` from
* `@tmonier/effract` and the bundler hands server components this version and
* client components the sibling `index.client` one.
*
* Client-only APIs (`Runtime`, `observe`, `atom`, …) are intentionally absent
* here — a component that needs them is a client island, and lives in the client
* graph.
*
* @packageDocumentation
*/
export const VERSION = '0.1.0';
// --- the yield protocol ---
// NOTE: `hook` is deliberately *not* exported here. React hooks are a client
// render-pass concept RSC has no equivalent for, so `hook(...)` does not exist
// in the server graph: reach for it in a Server Component and it is a compile
// error ("hook is not exported"), not a runtime surprise. (`observe`/`atom` are
// absent for the same reason — they live only in the client entry.)
export { isPlacement, placement, PlacementTypeId } from '#domain/protocol.ts';
export type {
AnyEffect,
Yieldable,
RecBody,
RecGenerator,
RecHandle,
RecPlacement,
RequirementsOf,
ErrorsOf,
} from '#domain/protocol.ts';
// --- components ---
export { rec, RecTypeId } from '#infrastructure/rec-core.tsx';
export type { REC, MissingServices, Effective } from '#infrastructure/rec-core.tsx';
export { mount } from '#infrastructure/server/mount.ts';
// --- the low-level server driver, for custom pipelines (per-request runtimes, Flight) ---
export { driveServerRec } from '#application/server-driver.ts';
export type { RunEffect } from '#application/server-driver.ts';
/**
* The React Effect Component *descriptor* — the server-safe half of a REC.
*
* This module deliberately carries no `'use client'` and imports no React
* hooks, no interpreter, and no runtime provider. A `rec(...)` value is pure
* data: the component's body plus a stable identity. That is what lets a single
* declaration be shared across runtimes — the same descriptor is turned into a
* live client fiber by the client `mount` (see `./react/rec.tsx`) and driven on
* the server by the server `mount` (see `./server/mount.ts`, selected by the
* `react-server` export condition). Neither the body nor the descriptor is
* client-only; only the *client interpreter* is.
*
* A REC is still **not** a React element type — `<Rec />` is a compile error by
* design. You compose RECs the way you compose Effects: with `yield*`, which is
* how a child's Effect requirements (`R`) bubble up the tree to the one place
* that knows the runtime (`mount`), where they are verified at compile time.
*/
import type { ReactNode } from 'react';
import {
placement,
type AnyEffect,
type Hook,
type RecBody,
type RecHandle,
type RecPlacement,
type RequirementsOf,
} from '#domain/protocol.ts';
/** Brand identifying a React Effect Component. */
export const RecTypeId: unique symbol = Symbol.for('@tmonier/effract/Rec');
export type RecTypeId = typeof RecTypeId;
interface RecCore<P, R> extends RecHandle<ReactNode> {
readonly [RecTypeId]: true;
/** Place this REC with props: `yield* Child.with({ ... })`. */
with(props: P): RecPlacement<ReactNode, R>;
}
interface RecBareYield<R> {
/** Place this REC without props: `yield* Child`. */
[Symbol.iterator](): Iterator<RecPlacement<ReactNode, R>, ReactNode>;
}
/**
* A React Effect Component. Yieldable (so `R` propagates), never a JSX element
* type — `<Rec />` is a compile error by design. Props-free RECs can be yielded
* directly (`yield* Child`); RECs with props use `yield* Child.with(props)`.
*/
export type REC<P, R> = RecCore<P, R> &
([Record<never, never>] extends [P] ? RecBareYield<R> : unknown);
const makeRec = <P extends object, R>(body: RecBody<P, ReactNode>, name: string): REC<P, R> => {
// 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<P, R> & RecBareYield<R> = {
[RecTypeId]: true,
body,
displayName: name,
with(props: P): RecPlacement<ReactNode, R> {
return placement<ReactNode, R>(rec, props);
},
[Symbol.iterator](): Iterator<RecPlacement<ReactNode, R>, ReactNode> {
return placement<ReactNode, R>(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, 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.
*/
export function rec<
Eff extends AnyEffect | Hook<unknown> | RecPlacement<ReactNode, unknown>,
A extends ReactNode,
>(body: () => Generator<Eff, A, never>): REC<Record<never, never>, RequirementsOf<Eff>>;
export function rec<
Eff extends AnyEffect | Hook<unknown> | RecPlacement<ReactNode, unknown>,
A extends ReactNode,
Props extends object,
>(body: (props: Props) => Generator<Eff, A, never>): REC<Props, RequirementsOf<Eff>>;
export function rec<
Eff extends AnyEffect | Hook<unknown> | RecPlacement<ReactNode, unknown>,
A extends ReactNode,
Props extends object,
>(body: (props: Props) => Generator<Eff, A, never>): REC<Props, RequirementsOf<Eff>> {
return makeRec<Props, RequirementsOf<Eff>>(body, body.name || 'EffractComponent');
}
/** A type error naming the services a runtime is missing for a REC's tree. */
export type MissingServices<Missing> = readonly ['effract: runtime is missing', Missing];
/**
* 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<R> = [unknown] extends [R] ? never : R;
/**
* The server `mount` — the implementation the `react-server` condition selects.
* It proves the ergonomic half of the thesis: the same `mount(layer, Root)` call
* a client makes returns, on the server, an async React Server Component that
* resolves the tree's services against a runtime built from the layer — reused
* across renders, with no per-call plumbing.
*/
import { describe, expect, it } from 'vitest';
import * as Context from 'effect/Context';
import * as Layer from 'effect/Layer';
import { rec } from '#infrastructure/rec-core.tsx';
import { mount } from '#infrastructure/server/mount.ts';
class Stats extends Context.Service<Stats, { readonly total: number }>()('test/Stats') {}
describe('server mount', () => {
it('returns an async component that resolves services from the layer', async () => {
const Badge = rec(function* () {
const stats = yield* Stats;
return `total:${stats.total}`;
});
const Root = mount(Layer.succeed(Stats)({ total: 9 }), Badge);
expect(await Root()).toBe('total:9');
});
it('reuses one runtime per layer identity across renders', async () => {
// The layer builds its service once; a second render that rebuilt the
// runtime would bump the counter to 2.
let builds = 0;
const layer = Layer.sync(Stats)(() => {
builds += 1;
return { total: builds };
});
const Badge = rec(function* () {
const stats = yield* Stats;
return `total:${stats.total}`;
});
const Root = mount(layer, Badge);
expect(await Root()).toBe('total:1');
expect(await Root()).toBe('total:1');
expect(builds).toBe(1);
});
});
/**
* `mount` — the *server* implementation, selected automatically in a React
* Server Component graph via the package's `react-server` export condition.
*
* It is the exact twin of the client `mount` (see `../react/rec.tsx`): same
* name, same arguments, the same compile-time check that `layer` provides every
* service the tree needs. The difference is invisible to you — the bundler hands
* server components this version, which drives the REC against an Effect runtime
* and returns an *async React Server Component* (no hooks, no client JS). You
* import `mount` from `@tmonier/effract` in every file; where it runs decides
* which implementation you get.
*
* ```tsx
* // app/page.tsx — a React Server Component
* const Page = rec(function* () {
* return <main>{yield* StatsBadge}</main>; // resolved on the server
* });
* export default mount(AppLive, Page); // same call as on the client
* ```
*/
import type { ReactNode } from 'react';
import type * as Layer from 'effect/Layer';
import * as ManagedRuntime from 'effect/ManagedRuntime';
import { driveServerRec } from '#application/server-driver.ts';
import type { Effective, MissingServices, REC } from '#infrastructure/rec-core.tsx';
/**
* A self-contained runtime: every service the tree needs is already provided.
* Typed with `never` requirements so any concrete app runtime (which provides
* *some* services) is assignable — the requirement channel is contravariant.
*/
type ServerRuntime = ManagedRuntime.ManagedRuntime<never, unknown>;
/**
* One runtime per layer identity — the server analogue of the client `mount`'s
* per-boundary runtime. A concrete `ManagedRuntime<ROut, E>` widens to
* `ServerRuntime` (requirements are contravariant), so building it needs no cast.
*/
const runtimes = new WeakMap<object, ServerRuntime>();
const runtimeFor = <ROut, E>(layer: Layer.Layer<ROut, E, never>): ServerRuntime => {
const existing = runtimes.get(layer);
if (existing !== undefined) {
return existing;
}
const runtime = ManagedRuntime.make(layer);
runtimes.set(layer, runtime);
return runtime;
};
/**
* Mount a root REC as an async React Server Component under an Effect runtime.
* Returns a component you export or place (`export default mount(AppLive, Page)`
* / `<Page />`). Verifies at compile time that `layer` provides every service the
* tree requires — the check lives on the `rec` argument, so there is no cast at
* the call site. Hook-bearing bodies reject at render (a hook is a client concept
* RSC forbids); those RECs are client islands and render wherever the browser is.
*/
export function mount<ROut, E, R>(
layer: Layer.Layer<ROut, E, never>,
rec: REC<Record<never, never>, R> &
([Effective<R>] extends [ROut] ? unknown : MissingServices<Exclude<Effective<R>, ROut>>),
): () => Promise<ReactNode> {
const Root = (): Promise<ReactNode> => {
const runtime = runtimeFor(layer);
// The generator walk erases each yield to `AnyEffect`, but `mount`'s
// signature has already proven `layer` provides the tree's services. Coercing
// the erased effect to what this runtime accepts is the one Effect-boundary
// step — and it lives here, once, not at any call site.
return driveServerRec(rec.body({}), (effect) =>
runtime.runPromise(effect as Parameters<typeof runtime.runPromise>[0]),
);
};
Object.defineProperty(Root, 'name', { value: rec.displayName });
return Root;
}
+9
-3
{
"name": "@tmonier/effract",
"version": "0.2.1",
"version": "0.3.0",
"description": "React components written as Effect programs. yield* services and React hooks in one render pass; run the same component anywhere.",

@@ -36,4 +36,10 @@ "keywords": [

".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
"react-server": {
"types": "./dist/index.server.d.mts",
"import": "./dist/index.server.mjs"
},
"default": {
"types": "./dist/index.client.d.mts",
"import": "./dist/index.client.mjs"
}
}

@@ -40,0 +46,0 @@ },

# @tmonier/effract
Write React components as Effect programs. The same component runs in a SPA, on a Bun/Node server, in a Web
Worker, or as a React Server Component.
Write React components as Effect programs. The same component — and the same `mount` — runs in a SPA,
during SSR, or as a React Server Component. One package, one import; the client/server split is chosen
by the bundler, never by you.

@@ -29,8 +30,10 @@ **Docs & guide → [effract.tmonier.com](https://effract.tmonier.com)**

- `component` / `view` — hook-capable and resolve-up-front RECs. A REC is **not** a JSX element; place it
- `rec` / `view` — hook-capable and resolve-up-front RECs. A REC is **not** a JSX element; place it
with `{yield* Rec}` inside another component's JSX.
- `hook` — lift a React hook into the `yield*` channel.
- `mount(layer, RootRec)` — build the Effect runtime once and return a `ReactNode` to render. Verifies at
compile time that the layer provides every service the tree needs.
- `atom`, `observe`, `<Observe>`, `useAtom` — the signals bridge.
- `mount(layer, RootRec)` — the one boundary, client **and** server. Builds the Effect runtime once and
verifies at compile time that the layer provides every service the tree needs. In a React Server
Component graph the bundler's `react-server` condition gives it a server implementation (renders on the
server, no client JS); everywhere else it renders interactively. Same import in every file.
- `atom`, `observe`, `<Observe>`, `useAtom` — the signals bridge (client).

@@ -37,0 +40,0 @@ See the [project README](https://github.com/get-tmonier/effract#readme) and

@@ -14,3 +14,9 @@ /**

import { driveRec } from '#application/interpreter.ts';
import type { Executor, InterpreterDeps, RenderCache, Suspender } from '#application/ports.ts';
import type {
Executor,
InterpreterDeps,
Placer,
RenderCache,
Suspender,
} from '#application/ports.ts';

@@ -36,2 +42,8 @@ class Stats extends Context.Service<Stats, { readonly total: number }>()('test/Stats') {}

const neverPlace: Placer = {
place: () => {
throw new Error('did not expect to place a child');
},
};
const makeDeps = (

@@ -44,2 +56,3 @@ layer: Layer.Layer<never, never, never>,

cache: new Map(),
placer: neverPlace,
});

@@ -114,2 +127,3 @@

suspender: { use: () => 99 as never },
placer: neverPlace,
};

@@ -141,5 +155,10 @@ expect(driveRec(body(), retryDeps)).toBe(99);

expect(() =>
driveRec(body(), { executor: executorWith(Layer.empty), suspender: neverSuspend, cache }),
driveRec(body(), {
executor: executorWith(Layer.empty),
suspender: neverSuspend,
cache,
placer: neverPlace,
}),
).toThrow(TypeError);
});
});

@@ -20,3 +20,3 @@ /**

import * as Exit from 'effect/Exit';
import { isHook, type AnyEffect, type RecGenerator } from '#domain/protocol.ts';
import { isHook, isPlacement, type AnyEffect, type RecGenerator } from '#domain/protocol.ts';
import type { InterpreterDeps } from '#application/ports.ts';

@@ -36,7 +36,3 @@

*/
export const resolveEffect = (
effect: AnyEffect,
deps: InterpreterDeps,
state: DriveState,
): unknown => {
const resolveEffect = (effect: AnyEffect, deps: InterpreterDeps, state: DriveState): unknown => {
if (!Effect.isEffect(effect)) {

@@ -80,5 +76,12 @@ throw new TypeError(

const instruction = step.value;
const result = isHook(instruction)
? instruction.value
: resolveEffect(instruction, deps, state);
let result: unknown;
if (isHook(instruction)) {
// The hook already ran inline during render; unwrap its value.
result = instruction.value;
} else if (isPlacement(instruction)) {
// A child REC: hand it to the renderer to place as a real React child.
result = deps.placer.place(instruction);
} else {
result = resolveEffect(instruction, deps, state);
}
step = gen.next(result);

@@ -85,0 +88,0 @@ }

@@ -8,3 +8,3 @@ /**

import type * as Exit from 'effect/Exit';
import type { AnyEffect } from '#domain/protocol.ts';
import type { AnyEffect, RecPlacement } from '#domain/protocol.ts';

@@ -44,2 +44,15 @@ /**

/**
* Turns a child-REC placement into a rendered node. Injected because *how* a
* child is placed is renderer-specific — on the client it becomes a real React
* child fiber (`createElement`); the interpreter itself must stay React-free, so
* it types the child's node as `unknown` and feeds the result straight back into
* the generator. `place` is a *method* deliberately: its parameter is bivariant,
* so the concrete renderer can declare the node type it actually produces (e.g.
* `ReactNode`) without the interpreter — or the renderer — needing a cast.
*/
export interface Placer {
place(placement: RecPlacement<unknown, unknown>): unknown;
}
export interface InterpreterDeps {

@@ -49,2 +62,3 @@ readonly executor: Executor;

readonly cache: RenderCache;
readonly placer: Placer;
}

@@ -81,7 +81,11 @@ /**

/** Everything a component body may `yield*`: an Effect, or a lifted hook. */
export type Yieldable<A> = Effect.Effect<A, unknown, unknown> | Hook<A>;
/** Everything a component body may `yield*`: an Effect, a lifted hook, or a child placement. */
export type Yieldable<A> = Effect.Effect<A, unknown, unknown> | Hook<A> | RecPlacement<A, unknown>;
/** The generator a React Effect Component body produces. */
export type RecGenerator<A> = Generator<AnyEffect | Hook<unknown>, A, unknown>;
export type RecGenerator<A> = Generator<
AnyEffect | Hook<unknown> | RecPlacement<unknown, unknown>,
A,
unknown
>;

@@ -92,2 +96,73 @@ /** A component body: props in, a generator of yields ending in a rendered `A`. */

/**
* A stable handle to a component's body — the identity a placement points at.
* The client keys its per-descriptor React component on this object (so a child
* keeps a stable React type across re-renders); the server drives its `body`
* directly. Deliberately renderer-agnostic: it names *what* to run, not how.
*/
export interface RecHandle<A> {
// oxlint-disable-next-line typescript/no-explicit-any -- a stored body accepts whatever props its placement supplies; precise props are recovered at the call site.
readonly body: (props: any) => RecGenerator<A>;
readonly displayName: string;
}
/** Brand identifying a child-REC placement instruction. */
export const PlacementTypeId = Symbol.for('@tmonier/effract/Placement');
export type PlacementTypeId = typeof PlacementTypeId;
/**
* The third kind of yieldable, alongside Effects and Hooks: the placement of a
* *child* REC into a parent's tree (`yield* Child` / `yield* Child.with(p)`).
*
* A placement carries only data — the child's stable `rec` handle and its
* `props` — never a bound React element. That is what lets one `rec(...)` value
* be shared across runtimes: the client turns a placement into a real React
* child fiber, the server drives the child's body inline. The phantom `R` makes
* the child's Effect requirements flow up through `yield*` to `mount`/`serve`,
* exactly as a yielded service's requirements do.
*/
export interface RecPlacement<A, R> {
readonly [PlacementTypeId]: true;
readonly rec: RecHandle<A>;
readonly props: object;
/**
* Phantom carrier for the requirements this placement contributes upward.
* Covariant (an output position) so a concrete placement — e.g. one needing
* `Stats` — widens to `RecPlacement<_, unknown>` in a body's yield union, just
* as `Effect<_, _, Stats>` widens to `AnyEffect`. Never set at runtime.
*/
readonly _requirements?: R;
[Symbol.iterator](): Iterator<RecPlacement<A, R>, A>;
}
/**
* Construct a child placement. Single-shot iterable like {@link hook}: `yield*`
* hands the interpreter the placement instruction, and the value fed back in
* (the rendered child node) becomes the result of the `yield*`.
*/
export const placement = <A, R>(rec: RecHandle<A>, props: object): RecPlacement<A, R> => {
const self: RecPlacement<A, R> = {
[PlacementTypeId]: true,
rec,
props,
[Symbol.iterator]() {
let yielded = false;
return {
next(sent?: unknown): IteratorResult<RecPlacement<A, R>, A> {
if (yielded) {
return { done: true, value: sent as A };
}
yielded = true;
return { done: false, value: self };
},
};
},
};
return self;
};
/** Type guard: is this yielded instruction a child-REC placement? */
export const isPlacement = (u: unknown): u is RecPlacement<unknown, unknown> =>
typeof u === 'object' && u !== null && PlacementTypeId in u;
/**
* Distribute over the yield union and keep only its Effect members. Hooks are

@@ -99,7 +174,16 @@ * not Effects, so they drop away — leaving just what carries `E` and `R`.

/**
* Recover the Effect requirement channel `R` from everything a body yields.
* A body that needs both `A` and `B` requires `A & B`, which is exactly the
* intersection TypeScript infers from the contravariant requirement slot.
* Re-express each yielded placement as an effect carrying only its child's
* requirements, so a placed child's `R` joins the parent's the same way a
* yielded service's does — one child, its whole subtree's services bubble up.
*/
export type RequirementsOf<Eff> = [EffectsOnly<Eff>] extends [
type PlacementsAsEffects<Eff> =
Eff extends RecPlacement<unknown, infer R> ? Effect.Effect<unknown, unknown, R> : never;
/**
* Recover the Effect requirement channel `R` from everything a body yields —
* services, effects, and placed child RECs. A body that needs both `A` and `B`
* requires `A & B`, which is exactly the intersection TypeScript infers from
* the contravariant requirement slot.
*/
export type RequirementsOf<Eff> = [EffectsOnly<Eff> | PlacementsAsEffects<Eff>] extends [
Effect.Effect<unknown, unknown, infer R>,

@@ -106,0 +190,0 @@ ]

@@ -8,3 +8,3 @@ /**

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { Observe, atom, observe, useAtom } from '../../index.ts';
import { Observe, atom, observe, useAtom } from '../../index.client.ts';

@@ -11,0 +11,0 @@ Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true);

@@ -14,3 +14,3 @@ /**

import * as Layer from 'effect/Layer';
import { rec, hook, mount } from '../../index.ts';
import { rec, hook, mount } from '../../index.client.ts';

@@ -17,0 +17,0 @@ Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true);

'use client';
/**
* React Effect Components (RECs) and the boundary that mounts them.
* The client binding for React Effect Components: it turns a (runtime-agnostic)
* REC descriptor into a real React component whose body effract interprets
* *inside* React's render pass, and provides `mount`, the boundary that supplies
* the browser runtime.
*
* A REC is the unit of composition in effract. Crucially it is **not** a React
* element type — `<Dashboard />` is a compile error. You compose RECs the way
* you compose Effects: with `yield*`. That is what lets a component's Effect
* requirements (`R`) bubble up the tree to the one place that knows the runtime,
* the `mount` boundary, where they are verified at compile time.
*
* const Dashboard = rec(function* () {

@@ -20,4 +17,8 @@ * const stats = yield* Stats; // a service

*
* At runtime a REC still renders as an ordinary React component (own fiber,
* hooks, reconciliation); the yield is only how it is placed and how `R` flows.
* The descriptor itself (`rec`) lives in `../rec-core.tsx` and is
* server-safe; this module adds only the client half — the in-render
* interpreter and hook dispatch — so the same `rec(...)` value also `serve`s on
* the server. At runtime a REC renders as an ordinary React component (own
* fiber, hooks, reconciliation); the yield is only how it is placed and how `R`
* flows.
*/

@@ -32,35 +33,9 @@ import {

} from 'react';
import * as Effect from 'effect/Effect';
import type * as Layer from 'effect/Layer';
import { driveRec, resolveEffect } from '#application/interpreter.ts';
import type { RenderCache, Suspender } from '#application/ports.ts';
import type { AnyEffect, Hook, RequirementsOf } from '#domain/protocol.ts';
import { driveRec } from '#application/interpreter.ts';
import type { Placer, RenderCache, Suspender } from '#application/ports.ts';
import type { RecHandle, RecPlacement } from '#domain/protocol.ts';
import type { Effective, MissingServices, REC } from '#infrastructure/rec-core.tsx';
import { Runtime, useExecutor } from '#infrastructure/react/runtime.tsx';
/** Brand identifying a React Effect Component. */
export const RecTypeId: unique symbol = Symbol.for('@tmonier/effract/Rec');
export type RecTypeId = typeof RecTypeId;
/** A rendered child: an Effect producing a React element, carrying its requirements. */
type Rendered<R> = Effect.Effect<ReactElement, never, R>;
interface RecCore<P, R> {
readonly [RecTypeId]: true;
/** Place this REC with props: `yield* Child.with({ ... })`. */
with(props: P): Rendered<R>;
}
interface RecBareYield<R> {
/** Place this REC without props: `yield* Child`. */
[Symbol.iterator](): Iterator<Rendered<R>, ReactElement>;
}
/**
* A React Effect Component. Yieldable (so `R` propagates), never a JSX element
* type — `<Rec />` is a compile error by design. Props-free RECs can be yielded
* directly (`yield* Child`); RECs with props use `yield* Child.with(props)`.
*/
export type REC<P, R> = RecCore<P, R> &
([Record<never, never>] extends [P] ? RecBareYield<R> : unknown);
const useSuspender = (): Suspender => ({ use });

@@ -76,94 +51,45 @@

const makeRec = <P extends object, R>(fc: FunctionComponent<P>, name: string): REC<P, R> => {
// The child resolves its own services when React renders it, so this
// render-effect requires nothing at runtime. The phantom `R` is asserted here
// — the single intentional assertion — so requirements propagate as a type.
const rendered = (props: P): Rendered<R> =>
Effect.succeed(createElement(fc, props)) as Effect.Effect<ReactElement, never, R>;
const rec: RecCore<P, R> & RecBareYield<R> = {
[RecTypeId]: true,
with: rendered,
[Symbol.iterator](): Iterator<Rendered<R>, ReactElement> {
let yielded = false;
return {
next(sent?: unknown): IteratorResult<Rendered<R>, ReactElement> {
if (yielded) {
return { done: true, value: sent as ReactElement };
}
yielded = true;
return { done: false, value: rendered({} as P) };
},
};
},
};
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, and
* `yield* Child` / `yield* Child.with(props)` to place other RECs.
* One React component per descriptor identity, cached so a yield-composed child
* keeps a stable React type across the parent's re-renders (React would
* otherwise remount it every render). The component interprets the descriptor's
* body in-render, resolving services, hooks, and nested placements.
*/
export function rec<Eff extends AnyEffect | Hook<unknown>, A extends ReactNode>(
body: () => Generator<Eff, A, never>,
): REC<Record<never, never>, RequirementsOf<Eff>>;
export function rec<
Eff extends AnyEffect | Hook<unknown>,
A extends ReactNode,
Props extends object,
>(body: (props: Props) => Generator<Eff, A, never>): REC<Props, RequirementsOf<Eff>>;
export function rec<
Eff extends AnyEffect | Hook<unknown>,
A extends ReactNode,
Props extends object,
>(body: (props: Props) => Generator<Eff, A, never>): REC<Props, RequirementsOf<Eff>> {
const fc: FunctionComponent<Props> = (props) => {
const executor = useExecutor();
const suspender = useSuspender();
const cache = useRenderCache();
return driveRec(body(props), { executor, suspender, cache });
};
return makeRec<Props, RequirementsOf<Eff>>(fc, body.name || 'EffractComponent');
}
const fcCache = new WeakMap<RecHandle<ReactNode>, FunctionComponent<object>>();
/**
* The resolve-up-front mode: a pure Effect (no hooks) rendered to a node.
* Returns a REC, composed the same way (`yield* Banner`).
*/
export function view<A extends ReactNode, E, R>(
render: Effect.Effect<A, E, R>,
): REC<Record<never, never>, R>;
export function view<A extends ReactNode, E, R, Props extends object>(
render: (props: Props) => Effect.Effect<A, E, R>,
): REC<Props, R>;
export function view<A extends ReactNode, E, R, Props extends object>(
render: Effect.Effect<A, E, R> | ((props: Props) => Effect.Effect<A, E, R>),
): REC<Props, R> {
const fc: FunctionComponent<Props> = (props) => {
const clientFcFor = (handle: RecHandle<ReactNode>): FunctionComponent<object> => {
const cached = fcCache.get(handle);
if (cached !== undefined) {
return cached;
}
const fc: FunctionComponent<object> = (props) => {
const executor = useExecutor();
const suspender = useSuspender();
const cache = useRenderCache();
const effect = (typeof render === 'function' ? render(props) : render) as AnyEffect;
return resolveEffect(effect, { executor, suspender, cache }, { index: 0 }) as ReactNode;
return driveRec(handle.body(props), { executor, suspender, cache, placer: clientPlacer });
};
return makeRec<Props, R>(fc, 'EffractView');
}
Object.defineProperty(fc, 'name', { value: handle.displayName });
fcCache.set(handle, fc);
return fc;
};
/** A type error naming the services a runtime is missing for a REC's tree. */
export type MissingServices<Missing> = readonly ['effract: runtime is missing', Missing];
/**
* 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 under any layer.
* Places a child REC as a real React child element under the same runtime. The
* `place` method's parameter is bivariant (see {@link Placer}), so it names the
* ReactNode-bodied child it renders here — the interpreter still hands it the
* erased placement, and no cast is needed on either side.
*/
type Effective<R> = [unknown] extends [R] ? never : R;
const clientPlacer: Placer = {
place: (placement: RecPlacement<ReactNode, unknown>): ReactElement =>
createElement(clientFcFor(placement.rec), placement.props),
};
/**
* Mount a root REC under an Effect runtime. This is the boundary between
* effract and React: it returns an ordinary React node and, at compile time,
* verifies that `layer` provides every service the REC's tree requires — the
* check lives on the `rec` argument, so there is no cast on the result.
* Mount a root REC under an Effect runtime — the **client** implementation of
* `mount`, selected everywhere except a React Server Component graph (where the
* sibling `../server/mount.ts` is chosen by the `react-server` condition). It is
* the boundary between effract and React: returns an ordinary React node and, at
* compile time, verifies that `layer` provides every service the REC's tree
* requires — the check lives on the `rec` argument, so there is no cast on the
* result.
*

@@ -173,2 +99,5 @@ * ```tsx

* ```
*
* You import `mount` from `@tmonier/effract` in every file; where the module
* runs decides whether it renders interactively (here) or on the server.
*/

@@ -180,5 +109,4 @@ export function mount<ROut, E, R>(

): ReactNode {
// The render-effect requires nothing at runtime (asserted: `R` is phantom).
const element = Effect.runSync(rec.with({}) as Rendered<never>);
return createElement(Runtime<ROut, E>, { layer }, element);
const root = createElement(clientFcFor(rec as unknown as RecHandle<ReactNode>), {});
return createElement(Runtime<ROut, E>, { layer }, root);
}

@@ -8,3 +8,3 @@ 'use client';

* client" lives: provide a browser layer and the same components run in a SPA;
* provide a server layer and they run under Node, Bun, or a Web Worker — the
* provide a server layer and they run under Node or Bun — the
* components never change. Use `mount(layer, Root)`; `Runtime` is the low-level

@@ -11,0 +11,0 @@ * provider underneath it.

import { ReactElement, ReactNode } from "react";
import * as Effect from "effect/Effect";
import * as ManagedRuntime from "effect/ManagedRuntime";
import { AtomRef } from "effect/unstable/reactivity";
import * as Layer from "effect/Layer";
//#region src/domain/protocol.d.ts
/**
* The one unavoidable `any` in the framework. The heterogeneous yield protocol
* must accept an Effect of *any* error and requirement variance — there is no
* other way to express "some Effect" as a generic constraint, which is why
* Effect's own `Effect.gen` is typed exactly this way. Precise `E` and `R` are
* recovered below through conditional inference, so this never leaks to users.
*/
type AnyEffect = Effect.Effect<any, any, any>;
/** Brand identifying a lifted React hook instruction. */
declare const HookTypeId: unique symbol;
type HookTypeId = typeof HookTypeId;
/**
* A React hook result lifted into the `yield*` channel. The hook itself has
* already executed (synchronously, during render) by the time the value is
* wrapped — `hook(useState(0))` calls `useState` inline. Wrapping it only makes
* the result yieldable, so a component body reads as one uniform stream of
* `yield*`s whether the value comes from Effect or from React.
*/
interface Hook<out A> {
readonly [HookTypeId]: true;
readonly value: A;
[Symbol.iterator](): Iterator<Hook<A>, A>;
}
/**
* Lift an already-evaluated React hook result into the effract yield channel.
*
* ```ts
* const [tab, setTab] = yield* hook(useState('overview'));
* const ref = yield* hook(useRef<HTMLDivElement>(null));
* ```
*
* Because the component body runs synchronously inside React's render pass, the
* wrapped hook call obeys the Rules of Hooks: same order on every render.
*/
declare const hook: <A>(value: A) => Hook<A>;
/** Type guard: is this yielded instruction a lifted hook? */
declare const isHook: (u: unknown) => u is Hook<unknown>;
/** Everything a component body may `yield*`: an Effect, or a lifted hook. */
type Yieldable<A> = Effect.Effect<A, unknown, unknown> | Hook<A>;
/** The generator a React Effect Component body produces. */
type RecGenerator<A> = Generator<AnyEffect | Hook<unknown>, A, unknown>;
/** A component body: props in, a generator of yields ending in a rendered `A`. */
type RecBody<Props, A> = (props: Props) => RecGenerator<A>;
/**
* Distribute over the yield union and keep only its Effect members. Hooks are
* not Effects, so they drop away — leaving just what carries `E` and `R`.
*/
type EffectsOnly<Eff> = Eff extends AnyEffect ? Eff : never;
/**
* Recover the Effect requirement channel `R` from everything a body yields.
* A body that needs both `A` and `B` requires `A & B`, which is exactly the
* intersection TypeScript infers from the contravariant requirement slot.
*/
type RequirementsOf<Eff> = [EffectsOnly<Eff>] extends [Effect.Effect<unknown, unknown, infer R>] ? R : never;
/** Recover the Effect error channel `E` (a union — any yielded effect may fail). */
type ErrorsOf<Eff> = [EffectsOnly<Eff>] extends [Effect.Effect<unknown, infer E, unknown>] ? E : never;
//#endregion
//#region src/infrastructure/react/rec.d.ts
/** Brand identifying a React Effect Component. */
declare const RecTypeId: unique symbol;
type RecTypeId = typeof RecTypeId;
/** A rendered child: an Effect producing a React element, carrying its requirements. */
type Rendered<R> = Effect.Effect<ReactElement, never, R>;
interface RecCore<P, R> {
readonly [RecTypeId]: true;
/** Place this REC with props: `yield* Child.with({ ... })`. */
with(props: P): Rendered<R>;
}
interface RecBareYield<R> {
/** Place this REC without props: `yield* Child`. */
[Symbol.iterator](): Iterator<Rendered<R>, ReactElement>;
}
/**
* A React Effect Component. Yieldable (so `R` propagates), never a JSX element
* type — `<Rec />` is a compile error by design. Props-free RECs can be yielded
* directly (`yield* Child`); RECs with props use `yield* Child.with(props)`.
*/
type REC<P, R> = RecCore<P, R> & ([Record<never, never>] extends [P] ? RecBareYield<R> : unknown);
/**
* Define a React Effect Component. The body is a generator that may `yield*`
* Effect services and effects, `yield* hook(...)` for React hooks, and
* `yield* Child` / `yield* Child.with(props)` to place other RECs.
*/
declare function rec<Eff extends AnyEffect | Hook<unknown>, A extends ReactNode>(body: () => Generator<Eff, A, never>): REC<Record<never, never>, RequirementsOf<Eff>>;
declare function rec<Eff extends AnyEffect | Hook<unknown>, A extends ReactNode, Props extends object>(body: (props: Props) => Generator<Eff, A, never>): REC<Props, RequirementsOf<Eff>>;
/**
* The resolve-up-front mode: a pure Effect (no hooks) rendered to a node.
* Returns a REC, composed the same way (`yield* Banner`).
*/
declare function view<A extends ReactNode, E, R>(render: Effect.Effect<A, E, R>): REC<Record<never, never>, R>;
declare function view<A extends ReactNode, E, R, Props extends object>(render: (props: Props) => Effect.Effect<A, E, R>): REC<Props, R>;
/** A type error naming the services a runtime is missing for a REC's tree. */
type MissingServices<Missing> = readonly ['effract: runtime is missing', Missing];
/**
* 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 under any layer.
*/
type Effective<R> = [unknown] extends [R] ? never : R;
/**
* Mount a root REC under an Effect runtime. This is the boundary between
* effract and React: it returns an ordinary React node and, at compile time,
* verifies that `layer` provides every service the REC's tree requires — the
* check lives on the `rec` argument, so there is no cast on the result.
*
* ```tsx
* createRoot(el).render(mount(AppLive, Dashboard));
* ```
*/
declare function mount<ROut, E, R>(layer: Layer.Layer<ROut, E, never>, rec: REC<Record<never, never>, R> & ([Effective<R>] extends [ROut] ? unknown : MissingServices<Exclude<Effective<R>, ROut>>)): ReactNode;
//#endregion
//#region src/infrastructure/react/runtime.d.ts
type AnyManagedRuntime = ManagedRuntime.ManagedRuntime<unknown, unknown>;
interface RuntimeProps<ROut, E> {
/** A self-contained layer (no open requirements) providing the subtree's services. */
readonly layer: Layer.Layer<ROut, E, never>;
readonly children?: ReactNode;
}
/**
* Provide an Effect runtime to a React subtree. Prefer `mount(layer, Root)`,
* which wraps the root REC in this provider and checks the tree's services at
* compile time. Reach for `Runtime` directly only to wrap non-REC React trees.
*/
declare function Runtime<ROut, E>({
layer,
children
}: RuntimeProps<ROut, E>): ReactNode;
/** Escape hatch: the underlying `ManagedRuntime`, for imperative `runPromise`/`runFork`. */
declare const useEffractRuntime: () => AnyManagedRuntime;
//#endregion
//#region src/infrastructure/react/reactivity.d.ts
type Ref<A> = AtomRef.AtomRef<A>;
/** Read an atom inside `observe`, subscribing the component to it. */
type Read = <A>(ref: Ref<A>) => A;
/** Create a reactive cell. Sugar for `AtomRef.make`. */
declare const atom: <A>(initial: A) => Ref<A>;
/**
* Subscribe to a derived view over one or more atoms. Re-renders precisely when
* a read atom changes.
*
* ```tsx
* const doubled = observe(($) => $(count) * 2);
* ```
*/
declare const observe: <A>(selector: (read: Read) => A) => A;
/** Read a single atom's value, subscribing the component to it. */
declare const useAtomValue: <A>(ref: Ref<A>) => A;
/** A stable setter for an atom, supporting both values and updater functions. */
declare const useAtomSet: <A>(ref: Ref<A>) => ((value: A | ((prev: A) => A)) => void);
/** Read and write a single atom — the `useState` shape, backed by Effect. */
declare const useAtom: <A>(ref: Ref<A>) => readonly [A, (value: A | ((prev: A) => A)) => void];
interface ObserveProps<A extends ReactNode> {
readonly children: (read: Read) => A;
}
/** The render-prop form of {@link observe}. */
declare const Observe: <A extends ReactNode>({
children
}: ObserveProps<A>) => ReactNode;
//#endregion
//#region src/index.d.ts
/**
* effract — write React components as Effect programs.
*
* The same component runs in a SPA, on a Bun/Node server, in a Web Worker, or
* as a React Server Component. "Server vs client" is an Effect runtime detail,
* supplied by `mount(...)` — not an architectural fork.
*
* @packageDocumentation
*/
declare const VERSION = "0.1.0";
//#endregion
export { type AnyEffect, type ErrorsOf, type Hook, HookTypeId, type MissingServices, Observe, type ObserveProps, type REC, type Read, type RecBody, type RecGenerator, RecTypeId, type RequirementsOf, Runtime, type RuntimeProps, VERSION, type Yieldable, atom, hook, isHook, mount, observe, rec, useAtom, useAtomSet, useAtomValue, useEffractRuntime, view };
import { createContext, createElement, use, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
import * as Effect from "effect/Effect";
import * as Cause from "effect/Cause";
import * as Exit from "effect/Exit";
import * as ManagedRuntime from "effect/ManagedRuntime";
import { jsx } from "react/jsx-runtime";
import * as Equal from "effect/Equal";
import { AtomRef } from "effect/unstable/reactivity";
//#region src/domain/protocol.ts
/** Brand identifying a lifted React hook instruction. */
const HookTypeId = Symbol.for("@tmonier/effract/Hook");
/**
* Lift an already-evaluated React hook result into the effract yield channel.
*
* ```ts
* const [tab, setTab] = yield* hook(useState('overview'));
* const ref = yield* hook(useRef<HTMLDivElement>(null));
* ```
*
* Because the component body runs synchronously inside React's render pass, the
* wrapped hook call obeys the Rules of Hooks: same order on every render.
*/
const hook = (value) => {
const self = {
[HookTypeId]: true,
value,
[Symbol.iterator]() {
let yielded = false;
return { next(sent) {
if (yielded) return {
done: true,
value: sent
};
yielded = true;
return {
done: false,
value: self
};
} };
}
};
return self;
};
/** Type guard: is this yielded instruction a lifted hook? */
const isHook = (u) => typeof u === "object" && u !== null && HookTypeId in u;
//#endregion
//#region src/application/interpreter.ts
/**
* The interpreter — the bridge between React's fiber and Effect's fiber.
*
* React drives a component by calling its function during a render pass.
* `driveRec` runs *inside* that pass: it walks the component's generator
* synchronously, and for every `yield*` it decides who answers.
*
* - a lifted hook → the React hook already ran inline; unwrap its value
* - a service Tag → resolve it synchronously from the runtime's context
* - a sync Effect → run it synchronously, return its value
* - an async Effect → suspend through React's `use`, resuming on the retry
*
* Because the walk is synchronous and deterministic, the user's hook calls keep
* a stable order across renders — they are, and remain, ordinary React hooks.
* Nothing here forks React's reconciler; it cooperates with it.
*/
/**
* Resolve a single yielded Effect against the runtime. Synchronous effects
* (services, pure computation, ref reads) return immediately. An effect that
* cannot finish synchronously surfaces as an `AsyncFiberError`; we route it
* through React Suspense with a promise cached by encounter order, so the
* retry after the promise settles returns the value inline. Any other failure
* is a real error and is thrown to the nearest React error boundary.
*/
const resolveEffect = (effect, deps, state) => {
if (!Effect.isEffect(effect)) throw new TypeError("effract: a component body yielded a value that is neither an Effect nor a hook(...). Wrap React hooks with `hook(...)`, e.g. `yield* hook(useState(0))`.");
const exit = deps.executor.runSyncExit(effect);
if (Exit.isSuccess(exit)) return exit.value;
const squashed = Cause.squash(exit.cause);
if (Cause.isAsyncFiberError(squashed)) {
const index = state.index++;
let slot = deps.cache.get(index);
if (slot === void 0) {
slot = { promise: deps.executor.runPromise(effect) };
deps.cache.set(index, slot);
}
return deps.suspender.use(slot.promise);
}
throw squashed;
};
/**
* Run a React Effect Component body to its rendered result. Creates a fresh
* generator per render (generators are single-use); a Suspense retry simply
* runs this again from the top, replaying hooks in order and hitting the async
* cache for already-started work.
*/
const driveRec = (gen, deps) => {
const state = { index: 0 };
let step = gen.next();
while (!step.done) {
const instruction = step.value;
const result = isHook(instruction) ? instruction.value : resolveEffect(instruction, deps, state);
step = gen.next(result);
}
return step.value;
};
//#endregion
//#region src/infrastructure/react/runtime.tsx
/**
* The runtime provider that `mount` wraps your tree in. It builds an Effect
* `ManagedRuntime` once from a `Layer` and hands it down through React context,
* where every effract component reads it. This is the seam where "server vs
* client" lives: provide a browser layer and the same components run in a SPA;
* provide a server layer and they run under Node, Bun, or a Web Worker — the
* components never change. Use `mount(layer, Root)`; `Runtime` is the low-level
* provider underneath it.
*
* Services are resolved up-front into the runtime's context (the RSC-style
* "resolve near the root" mode), so reading a service inside a component is a
* synchronous context lookup, not an async round-trip.
*/
const RuntimeContext = createContext(null);
const executorFromRuntime = (runtime) => ({
runSyncExit: (effect) => runtime.runSyncExit(effect),
runPromise: (effect) => runtime.runPromise(effect)
});
/**
* Provide an Effect runtime to a React subtree. Prefer `mount(layer, Root)`,
* which wraps the root REC in this provider and checks the tree's services at
* compile time. Reach for `Runtime` directly only to wrap non-REC React trees.
*/
function Runtime({ layer, children }) {
const runtimeRef = useRef(null);
if (runtimeRef.current === null) runtimeRef.current = ManagedRuntime.make(layer);
const runtime = runtimeRef.current;
const value = useMemo(() => ({
executor: executorFromRuntime(runtime),
runtime
}), [runtime]);
useEffect(() => () => void runtime.dispose(), [runtime]);
return /* @__PURE__ */ jsx(RuntimeContext.Provider, {
value,
children
});
}
const useRuntimeContext = () => {
const value = useContext(RuntimeContext);
if (value === null) throw new Error("effract: no runtime found above this component. Mount your root with mount(layer, Root).");
return value;
};
/** Internal: the executor the interpreter runs effects through. */
const useExecutor = () => useRuntimeContext().executor;
/** Escape hatch: the underlying `ManagedRuntime`, for imperative `runPromise`/`runFork`. */
const useEffractRuntime = () => useRuntimeContext().runtime;
//#endregion
//#region src/infrastructure/react/rec.tsx
/**
* React Effect Components (RECs) and the boundary that mounts them.
*
* A REC is the unit of composition in effract. Crucially it is **not** a React
* element type — `<Dashboard />` is a compile error. You compose RECs the way
* you compose Effects: with `yield*`. That is what lets a component's Effect
* requirements (`R`) bubble up the tree to the one place that knows the runtime,
* the `mount` boundary, where they are verified at compile time.
*
* const Dashboard = rec(function* () {
* const stats = yield* Stats; // a service
* const [n, setN] = yield* hook(useState(0)); // a real React hook
* return <main>{yield* StatBadge}{n}</main>; // a child REC, yielded
* });
*
* mount(AppLive, Dashboard); // ← compile error if AppLive lacks a needed service
*
* At runtime a REC still renders as an ordinary React component (own fiber,
* hooks, reconciliation); the yield is only how it is placed and how `R` flows.
*/
/** Brand identifying a React Effect Component. */
const RecTypeId = Symbol.for("@tmonier/effract/Rec");
const useSuspender = () => ({ use });
const useRenderCache = () => {
const ref = useRef(null);
if (ref.current === null) ref.current = /* @__PURE__ */ new Map();
return ref.current;
};
const makeRec = (fc, name) => {
const rendered = (props) => Effect.succeed(createElement(fc, props));
const rec = {
[RecTypeId]: true,
with: rendered,
[Symbol.iterator]() {
let yielded = false;
return { next(sent) {
if (yielded) return {
done: true,
value: sent
};
yielded = true;
return {
done: false,
value: rendered({})
};
} };
}
};
Object.defineProperty(rec, "name", { value: name });
return rec;
};
function rec(body) {
const fc = (props) => {
const executor = useExecutor();
const suspender = useSuspender();
const cache = useRenderCache();
return driveRec(body(props), {
executor,
suspender,
cache
});
};
return makeRec(fc, body.name || "EffractComponent");
}
function view(render) {
const fc = (props) => {
const executor = useExecutor();
const suspender = useSuspender();
const cache = useRenderCache();
return resolveEffect(typeof render === "function" ? render(props) : render, {
executor,
suspender,
cache
}, { index: 0 });
};
return makeRec(fc, "EffractView");
}
/**
* Mount a root REC under an Effect runtime. This is the boundary between
* effract and React: it returns an ordinary React node and, at compile time,
* verifies that `layer` provides every service the REC's tree requires — the
* check lives on the `rec` argument, so there is no cast on the result.
*
* ```tsx
* createRoot(el).render(mount(AppLive, Dashboard));
* ```
*/
function mount(layer, rec) {
const element = Effect.runSync(rec.with({}));
return createElement(Runtime, { layer }, element);
}
//#endregion
//#region src/infrastructure/react/reactivity.tsx
/**
* The signals bridge. Effect's reactive cell is `AtomRef`; this binds it to
* React so a component re-renders precisely when — and only when — an atom it
* actually read changes.
*
* observe($ => $(count) * 2) // a hook: read + auto-subscribe
* <Observe>{$ => <b>{$(count)}</b>}</Observe> // the same, as an element
* const [n, setN] = useAtom(count) // read + write a single atom
*
* `observe` tracks exactly the atoms touched during its selector and subscribes
* to that set, re-tracking on every change so dynamic dependencies stay
* correct. No `Effect.runSync` at the call site, no manual dependency arrays.
*/
/** Create a reactive cell. Sugar for `AtomRef.make`. */
const atom = (initial) => AtomRef.make(initial);
const depsEqual = (a, b) => {
if (a.size !== b.size) return false;
for (const [ref, value] of a) if (!b.has(ref) || !Equal.equals(value, b.get(ref))) return false;
return true;
};
/**
* Run the selector, tracking which atoms it reads. Returns a *stable* reference
* when the tracked atoms and their values are unchanged, so it is safe to call
* from `getSnapshot` without provoking a render loop.
*/
const compute = (state) => {
const nextDeps = /* @__PURE__ */ new Map();
const read = (ref) => {
const value = ref.value;
nextDeps.set(ref, value);
return value;
};
const next = state.selector(read);
if (state.initialized && depsEqual(state.deps, nextDeps)) {
state.deps = nextDeps;
return state.value;
}
state.deps = nextDeps;
state.value = next;
state.initialized = true;
return next;
};
/**
* Subscribe to a derived view over one or more atoms. Re-renders precisely when
* a read atom changes.
*
* ```tsx
* const doubled = observe(($) => $(count) * 2);
* ```
*/
const observe = (selector) => {
const stateRef = useRef(null);
if (stateRef.current === null) stateRef.current = {
selector,
deps: /* @__PURE__ */ new Map(),
value: void 0,
initialized: false
};
stateRef.current.selector = selector;
const subscribe = useCallback((onStoreChange) => {
const state = stateRef.current;
if (state === null) return () => {};
let unsubscribes = [];
const resubscribe = () => {
for (const unsub of unsubscribes) unsub();
unsubscribes = [...state.deps.keys()].map((ref) => ref.subscribe(handleChange));
};
function handleChange() {
compute(state);
resubscribe();
onStoreChange();
}
compute(state);
resubscribe();
return () => {
for (const unsub of unsubscribes) unsub();
};
}, []);
const getSnapshot = useCallback(() => compute(stateRef.current), []);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
};
/** Read a single atom's value, subscribing the component to it. */
const useAtomValue = (ref) => {
const subscribe = useCallback((onStoreChange) => ref.subscribe(onStoreChange), [ref]);
const getSnapshot = useCallback(() => ref.value, [ref]);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
};
/** A stable setter for an atom, supporting both values and updater functions. */
const useAtomSet = (ref) => useCallback((value) => {
if (typeof value === "function") ref.update(value);
else ref.set(value);
}, [ref]);
/** Read and write a single atom — the `useState` shape, backed by Effect. */
const useAtom = (ref) => [useAtomValue(ref), useAtomSet(ref)];
/** The render-prop form of {@link observe}. */
const Observe = ({ children }) => observe(children);
//#endregion
//#region src/index.ts
/**
* effract — write React components as Effect programs.
*
* The same component runs in a SPA, on a Bun/Node server, in a Web Worker, or
* as a React Server Component. "Server vs client" is an Effect runtime detail,
* supplied by `mount(...)` — not an architectural fork.
*
* @packageDocumentation
*/
const VERSION = "0.1.0";
//#endregion
export { HookTypeId, Observe, RecTypeId, Runtime, VERSION, atom, hook, isHook, mount, observe, rec, useAtom, useAtomSet, useAtomValue, useEffractRuntime, view };
/**
* effract — write React components as Effect programs.
*
* The same component runs in a SPA, on a Bun/Node server, in a Web Worker, or
* as a React Server Component. "Server vs client" is an Effect runtime detail,
* supplied by `mount(...)` — not an architectural fork.
*
* @packageDocumentation
*/
export const VERSION = '0.1.0';
// --- the yield protocol ---
export { hook, isHook, HookTypeId } from '#domain/protocol.ts';
export type {
AnyEffect,
Hook,
Yieldable,
RecBody,
RecGenerator,
RequirementsOf,
ErrorsOf,
} from '#domain/protocol.ts';
// --- components ---
export { rec, view, mount, RecTypeId } from '#infrastructure/react/rec.tsx';
export type { REC, MissingServices } from '#infrastructure/react/rec.tsx';
// --- the runtime boundary (mount is canonical; Runtime is the low-level provider) ---
export { Runtime, useEffractRuntime } from '#infrastructure/react/runtime.tsx';
export type { RuntimeProps } from '#infrastructure/react/runtime.tsx';
// --- reactivity ---
export {
observe,
Observe,
atom,
useAtom,
useAtomValue,
useAtomSet,
} from '#infrastructure/react/reactivity.tsx';
export type { Read, ObserveProps } from '#infrastructure/react/reactivity.tsx';