((r) => {
release = r;
});
const Async = rec(function* () {
const v = yield* Effect.promise(() => gate);
return val:{v};
});
const Page = rec(function* () {
return loading}>{yield* Async};
});
const root = createRoot(container);
await act(async () => root.render(mount(Layer.empty, Page)));
expect(container.textContent).toContain('loading');
await act(async () => {
release(7);
await gate;
});
expect(container.textContent).toContain('val:7');
await act(async () => root.unmount());
});
it('runs the SAME tree under two runtimes — server vs client is the mount', () => {
const Total = rec(function* () {
const stats = yield* Stats;
return ;
});
const Page = rec(function* () {
return {yield* Total}
;
});
expect(renderToStaticMarkup(mount(statsLayer(100), Page))).toContain('100');
expect(renderToStaticMarkup(mount(statsLayer(1), Page))).toContain('1');
});
});
describe('explicit .memo() placements (ADR 0007)', () => {
it('WITHOUT .memo(), a parent re-render with identical-value props re-runs the child (plain React)', async () => {
const tick = atom('tick', 0);
let childRuns = 0;
const Child = rec(function* (props: { n: number }) {
childRuns += 1;
return {props.n};
});
const Parent = rec(function* () {
const t = yield* tick; // the parent re-renders on every write
return (
{yield* Child.with({ n: 1 })} {/* a fresh `{ n: 1 }` literal each render */}
);
});
const root = createRoot(container);
await act(async () => root.render(mount(Layer.empty, Parent)));
const runsAfterMount = childRuns;
await act(async () => tick.update((n) => n + 1));
expect(container.querySelector('output')?.textContent).toBe('1'); // the parent re-rendered
expect(childRuns).toBeGreaterThan(runsAfterMount); // no memo — the child re-ran with it
await act(async () => root.unmount());
});
it('skips the child body when a .memo() child receives value-equal primitive props', async () => {
const tick = atom('tick', 0);
let childRuns = 0;
const Child = rec(function* (props: { n: number }) {
childRuns += 1;
return {props.n};
});
const MemoChild = Child.memo(); // once, at declaration — a stable component type
const Parent = rec(function* () {
const t = yield* tick;
return (
{yield* MemoChild.with({ n: 1 })} {/* a fresh `{ n: 1 }` literal each render */}
);
});
const root = createRoot(container);
await act(async () => root.render(mount(Layer.empty, Parent)));
const runsAfterMount = childRuns;
await act(async () => tick.update((n) => n + 1));
expect(container.querySelector('output')?.textContent).toBe('1'); // the parent DID re-render
expect(childRuns).toBe(runsAfterMount); // …but value-equal props skipped the child
await act(async () => root.unmount());
});
it('re-runs a .memo() child when a prop changes value', async () => {
const tick = atom('tick', 0);
let childRuns = 0;
const Child = rec(function* (props: { n: number }) {
childRuns += 1;
return {props.n};
});
const MemoChild = Child.memo();
const Parent = rec(function* () {
const t = yield* tick;
return {yield* MemoChild.with({ n: t })}
;
});
const root = createRoot(container);
await act(async () => root.render(mount(Layer.empty, Parent)));
const runsAfterMount = childRuns;
await act(async () => tick.update((n) => n + 1));
expect(childRuns).toBeGreaterThan(runsAfterMount);
expect(container.querySelector('i')?.textContent).toBe('1');
await act(async () => root.unmount());
});
it('still re-renders a .memo() child from its OWN atom subscription', async () => {
const tick = atom('tick', 0);
const inner = atom('inner', 0);
let childRuns = 0;
const Child = rec(function* (props: { n: number }) {
childRuns += 1;
const v = yield* inner; // the child's own subscription
return (
{props.n}:{v}
);
});
const MemoChild = Child.memo();
const Parent = rec(function* () {
const t = yield* tick;
return (
{yield* MemoChild.with({ n: 1 })}
);
});
const root = createRoot(container);
await act(async () => root.render(mount(Layer.empty, Parent)));
const runsAfterMount = childRuns;
// Parent-only write, value-equal props → the memo skips the child…
await act(async () => tick.update((n) => n + 1));
expect(childRuns).toBe(runsAfterMount);
// …but the child's own atom still re-renders it straight through the memo.
await act(async () => inner.update((n) => n + 1));
expect(childRuns).toBeGreaterThan(runsAfterMount);
expect(container.querySelector('em')?.textContent).toBe('1:1');
await act(async () => root.unmount());
});
it('skips the child body for structurally-equal fresh Equal-implementing prop values', async () => {
// A value class carrying its own equality — the shape `.memo()`'s gate
// admits for structured data (a plain object or array would not compile).
class Vec implements Equal.Equal {
constructor(
readonly x: number,
readonly y: number,
) {}
[Equal.symbol](that: Equal.Equal): boolean {
return that instanceof Vec && this.x === that.x && this.y === that.y;
}
[Hash.symbol](): number {
return Hash.string(`${this.x},${this.y}`);
}
}
const tick = atom('tick', 0);
let childRuns = 0;
const Child = rec(function* (props: { v: Vec }) {
childRuns += 1;
return (
{props.v.x},{props.v.y}
);
});
const MemoChild = Child.memo();
const Parent = rec(function* () {
const t = yield* tick;
return (
{yield* MemoChild.with({ v: new Vec(1, 2) })} {/* a fresh instance each render */}
);
});
const root = createRoot(container);
await act(async () => root.render(mount(Layer.empty, Parent)));
const runsAfterMount = childRuns;
await act(async () => tick.update((n) => n + 1));
expect(container.querySelector('output')?.textContent).toBe('1'); // the parent DID re-render
expect(childRuns).toBe(runsAfterMount); // …but the Equal trait skipped the child
await act(async () => root.unmount());
});
});
// --- type-level guarantees (checked by tsgo, not run) ---
{
const Needs = rec(function* () {
const s = yield* Stats;
return {s.total};
});
const Root = rec(function* () {
return {yield* Needs};
});
// ✓ AppLive provides Stats:
void mount(statsLayer(1), Root);
// ✗ empty layer is missing Stats — mount returns a non-ReactNode error type:
// @ts-expect-error effract: runtime is missing Stats
const _bad: ReactNode = mount(Layer.empty, Root);
void _bad;
// ✗ a REC is not a JSX element type:
// @ts-expect-error RECs cannot be used as JSX
const _jsx = ;
void _jsx;
// A volatile atom read is a mount-checked obligation (the render channel).
// The cast stands in for a born-volatile source (`atom.fromStream`, which
// returns `Atom`); the atom is never dereferenced — the
// generator below never runs.
const price = null as unknown as ReadableAtom;
const Ticker = rec(function* () {
const p = yield* price;
return {p};
});
// ✗ an undischarged volatile subscription refuses to mount:
// @ts-expect-error effract: volatile subscription not handled
const _volatile: ReactNode = mount(Layer.empty, Ticker);
void _volatile;
// ✓ .hot() is the deliberate acknowledgment — V discharged:
void mount(Layer.empty, Ticker.hot());
// ✓ RendersOn extracts the exact label union a REC can re-render on:
const _lock: [RendersOn] extends ['price']
? 'price' extends RendersOn
? true
: never
: never = true;
void _lock;
}