/** * 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 Data from 'effect/Data'; import * as Effect from 'effect/Effect'; import * as Layer from 'effect/Layer'; import { query } from '#domain/protocol.ts'; import { atom } from '#infrastructure/reactivity-core.ts'; import { rec } from '#infrastructure/rec-core.tsx'; import { mount } from '#infrastructure/server/mount.ts'; class Stats extends Context.Service()('test/Stats') {} class NotFound extends Data.TaggedError('NotFound')<{ readonly id: number }> {} 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 under { scope: "shared" }', 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, { scope: 'shared' }); expect(await Root()).toBe('total:1'); expect(await Root()).toBe('total:1'); expect(builds).toBe(1); }); it('isolates per-request state by default — concurrent renders cannot bleed (#28)', async () => { // A service holding per-user state in an atom. Under the default isolated // scope each render gets its own runtime → its own service instance → its own // atom, so two concurrent renders that each set a different user and read it // back after an await never observe each other's write. class UserState extends Context.Service()('test/UserState', { make: Effect.sync(() => { const current = atom('current', 'anon'); return { current, setUser: (u: string) => current.set(u) }; }), }) { static readonly layer = Layer.effect(UserState, UserState.make); } const pageFor = (user: string) => rec(function* () { const state = yield* UserState; state.setUser(user); // Await, giving a concurrent render a chance to interleave its own write. yield* Effect.promise(() => new Promise((r) => setTimeout(r, 10))); return `user:${state.current.value}`; }); // Same layer for both — isolation must come from the per-render runtime, not // from distinct layers. const ada = mount(UserState.layer, pageFor('ada')); const grace = mount(UserState.layer, pageFor('grace')); const [a, b] = await Promise.all([ada(), grace()]); expect(a).toBe('user:ada'); expect(b).toBe('user:grace'); // Shared scope, by contrast, would let the second write win for both — the // footgun the default avoids. const sharedA = mount(UserState.layer, pageFor('ada'), { scope: 'shared' }); const sharedB = mount(UserState.layer, pageFor('grace'), { scope: 'shared' }); const [sa, sb] = await Promise.all([sharedA(), sharedB()]); expect(sa).toBe(sb); // both observe the last write — shared, on purpose }); it('renders a typed fallback via .catch — same API as the client', async () => { const Badge = rec(function* () { yield* Effect.fail(new NotFound({ id: 1 })); return 'never'; }).catch({ NotFound: (e) => `empty:${e.id}` }); const Root = mount(Layer.empty, Badge); expect(await Root()).toBe('empty:1'); }); it('mounts a query-bearing REC with no loading obligation — the server resolves inline', async () => { // On the client this REC carries an undischarged `S`; the server `mount` // accepts it (there is no pending state server-side) and awaits the query. const Badge = rec(function* () { const total = yield* query(Effect.succeed(7)); return `total:${total}`; }); const Root = mount(Layer.empty, Badge); expect(await Root()).toBe('total:7'); }); });