/** * `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
{yield* StatsBadge}
; // resolved on the server * }); * export default mount(AppLive, Page); // same call as on the client * ``` */ import type { ReactNode } from 'react'; import * as Layer from 'effect/Layer'; import * as ManagedRuntime from 'effect/ManagedRuntime'; import { driveServerRec } from '#application/server-driver.ts'; import { QueryCache, queryCacheNoop } from '#infrastructure/query-cache.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; /** * Shared runtimes, one per layer identity — used only under `{ scope: 'shared' }`. * The default is a *fresh* runtime per render (see below), so this is opt-in. */ const sharedRuntimes = new WeakMap(); /** * Merge the built-in `QueryCache` into the user's layer (provideMerge, so a * service may *require* it in its `make`). There is no query store on the RSC * path — a server render resolves each query inline exactly once — so the * implementation is the honest no-op: invalidate/refetch match nothing. */ const withQueryCache = ( layer: Layer.Layer, ): Layer.Layer => Layer.provideMerge(layer, Layer.succeed(QueryCache, queryCacheNoop)); const sharedRuntimeFor = (layer: Layer.Layer): ServerRuntime => { const existing = sharedRuntimes.get(layer); if (existing !== undefined) { return existing; } const runtime = ManagedRuntime.make(withQueryCache(layer)); sharedRuntimes.set(layer, runtime); return runtime; }; /** Options for the server `mount`. */ export interface ServerMountOptions { /** * How the render's Effect runtime is scoped. * * - `'isolated'` (**default**) — a *fresh* runtime is built for each render and * disposed after it, so any per-user state a service holds in an `atom` cannot * bleed across concurrent requests. This is the safe default for a server that * serves many users. (It does **not** isolate a *module-level* `atom` — one * declared outside a service `make` is a single process-global cell shared by * every request; keep per-request state *inside* a service's `make`.) * - `'shared'` — one runtime per layer identity, reused across every render for * the process (the pre-0.6 behaviour). Correct only for a genuinely global, * stateless layer; it avoids rebuilding the layer per request. If your layer * is expensive but has per-user state, split it: a `shared` base composed with * a per-request layer, or memoise the costly parts with `Layer.memoize`. */ readonly scope?: 'isolated' | 'shared'; } /** * 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)` * / ``). 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. * * Unlike the client `mount`, this imposes no loading obligation: a query resolves * inline before the HTML is sent, so there is no pending state to fall back for. * A tree's `S` is therefore accepted and ignored here — loading is a client * concern, handled where the tree actually renders interactively. The volatility * obligation `V` is accepted and ignored for the same reason: a server render * happens exactly once, so there are no re-renders for a hot subscription to * cause — scheduling, like loading, is checked where the tree renders * interactively. * * **Per-request isolation is the default.** Each render builds and disposes its * own runtime, so two concurrent requests cannot observe each other's service * state. Pass `{ scope: 'shared' }` to reuse one process-global runtime for a * stateless layer. See {@link ServerMountOptions}. */ export function mount( layer: Layer.Layer, rec: REC, RE, R, S, V, L> & // `QueryCache` is built in: the server `mount` provides it (as a no-op — no // store on the RSC path), so a tree that reads it never counts it as missing. ([Effective] extends [ROut | QueryCache] ? unknown : MissingServices, ROut | QueryCache>>), options?: ServerMountOptions, ): () => Promise { const scope = options?.scope ?? 'isolated'; // 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 the runtime accepts is the one Effect-boundary step — here, // once, not at any call site. const drive = (runtime: ServerRuntime): Promise => driveServerRec( rec.body({}), (effect) => runtime.runPromise(effect as Parameters[0]), rec.catchHandlers, rec.defectHandler, ); const Root = async (): Promise => { if (scope === 'shared') { return drive(sharedRuntimeFor(layer)); } // Isolated (default): a fresh runtime per render, disposed after it resolves — // the server render is resolve-up-front, so every yield has settled by the time // `drive` returns, and disposing then releases this request's scopes cleanly. const runtime = ManagedRuntime.make(withQueryCache(layer)); try { return await drive(runtime); } finally { await runtime.dispose(); } }; Object.defineProperty(Root, 'name', { value: rec.displayName }); return Root; }