# R-Machine — API Reference > Uniformity Under Change > A stable namespace is the contract; the implementation behind it is free to change. R-Machine is a TypeScript resource layer for React and Next.js unifying state, DI, and i18n behind one consumer primitive over a stable string namespace. **Terms:** resource = a TS factory exposed under a namespace; gear = non-localized resource (inner=server-only, base=shared service, outer=stateful, vertex=per-component); shell = localized resource; plug = consumer read handle; port = injected external value; kit = cross-resource injection; atlas = namespace→resource map; blueprint = a resource's cached resolution. https://rmachine.dev https://github.com/codecarvings/r-machine/ Warning: R-Machine is still in active development - API may change before stable release. --- ## Conceptual model: the namespace as a stable contract R-Machine is easier to reason about through one model than through a list of features. A codebase is a dynamic entity: it evolves sprint after sprint, refactor after refactor, generation after generation. A useful question when evaluating an architecture is not only *"can it do X?"* but *"how many files must change when X evolves?"* — production files, test files, mocks, fixtures, imports. R-Machine answers that question the way a DBMS does: | DBMS concept | R-Machine equivalent | |---|---| | Table name (`customers`) | Resource namespace (`outer/cart`, `shell/checkout`) | | Schema (column types) | TypeScript interface | | Query (`SELECT * FROM customers`) | `Plug` / `usePlug` | | Storage engine, indexes | Implementation body (gear or shell) | A database table has a stable name that consumers depend on. The storage engine can be replaced and indexes can change without forcing any consumer to update: the table name is the contract. R-Machine applies the same principle to application code. The resource namespace is the stable contract; the implementation behind it is the volatile layer. Consumers — including tests, mocks, and fixtures — depend on the namespace, not on where a value lives or how it is shaped, so a change to the implementation does not propagate to them. --- ## Getting started R-Machine ships an **LLM-agent skill** (via the `rforge` CLI) that scaffolds the project and adds resources, so an AI coding agent applies the conventions in this document correctly. Scaffold an app, install the skill, then drive it with natural-language prompts: ```bash pnpm create next-app@latest # or an existing Next.js / React (Vite) app pnpm dlx rforge@latest skill # installs the skill into .claude/skills + .agents/skills ``` Then prompt your agent (Claude Code or another), e.g.: - "Install R-Machine in this project" — creates `resource-atlas.ts`, `setup.ts`, the framework strategy, and the toolset exports. - "Transform the hardcoded texts of @src/app/[locale]/page.tsx into localized contents" — extracts the strings into a `shell` and rewrites the component to read them through a plug. The same flow scaffolds gears and shells. --- ## 1. Public API surface (complete) **Composers** (resource declaration) `InnerGear`, `BaseGear`, `OuterGear`, `Shell`, `localized` **Toolset dep builders** `res.perLocale` — declares a `Shell` as a locale-loader dep inside `withDeps`; `.pickAll` / `.pick` batch-resolve loaders (§4.1) **Plug** (consumer primitive) `Plug`, `ClientPlug`, `ServerPlug`, `DirectPlug` (container-free, runs anywhere) **Testing** `mockPlug`, `verifyResourceAtlas`, `createEventCollector` (from `@r-machine/testing`) **Diagnostics** `enableRMachineDevMode` (from `r-machine`) — console-traces the runtime event bus (see §15.2) `getResolveContext` (from `r-machine`) — reads the resolution attribution attached to a resolution-failure error (see §10.6) **Cursor primitives** (reactive members inside an `OuterGear`) `_.action`, `_.getter`, `_.cell`, `_.relay`, `_.cmd` **Lifecycle** `[Symbol.dispose]` convention (see §9) **Setup** `RMachine.create`, `defineLayout`, `ResourceAtlas`, `PathAtlas` **Framework strategies** `ReactStandardStrategy` (web React) `NextAppPathStrategy`, `NextAppFlatStrategy`, `NextAppOriginStrategy` (Next.js App Router, three routing models) `createNextDevImport` (Next.js dev-mode loader for HMR + `verifyResourceAtlas` activation — see §11.8) **Strategy-emitted toolsets** React: `ReactRMachine`, `VertexFrame`, `Plug` Next.js client: `NextClientRMachine`, `VertexFrame`, `ClientPlug` Next.js server (canonical, with proxy): `NextServerRMachine`, `bindLocale`, `setLocale`, `generateLocaleStaticParams`, `rMachineProxy`, `ServerPlug` Next.js server (no-proxy, path strategy only): `NextServerRMachine`, `bindLocale`, `setLocale`, `generateLocaleStaticParams`, `routeHandlers`, `ServerPlug` **Path declaration** `declarePathAtlas` (from `@r-machine/next`) — see §3.5 **Resource (`ResMatrix`)** — value produced by `.define(...)` `r.plug`, `r.clone(...)` — see §4.4 and §5 (instantiation is engine-internal; tests instantiate via `ctrl.createRes()`, §14) **Strategy helpers** — `strategy.getHelpers()` returns `{ localeHelper }` on every strategy, plus `hrefHelper` on all Next.js strategies. See §11.3. **Type-level utilities** `RMachineLocale`, `BrandedResource` (re-exported as `RShape`) --- ## 2. Layout families Six family values are accepted in `defineLayout`. Anything else is a compile error. | Family | Composer | Stateful? | Can depend on | Consumed by | Locale-aware? | Eligible for kit? | |---|---|---|---|---|---|---| | `gear:inner` | `InnerGear` | no | `InnerGear`, `BaseGear`, `gearKit`; any Shell via `res.perLocale` (§4.1) | `InnerGear`, `BaseGear`, and `ServerPlug` (Next.js RSC only — never `Plug` or `ClientPlug`) | no | yes (`gearKit`) | | `gear:base` | `BaseGear` | no | `BaseGear`, `gearKit`; any Shell via `res.perLocale` (§4.1) | `InnerGear`, `BaseGear`, `OuterGear`, `Shell` (only if listed in `bridgeGears`) | no | yes (`gearKit`, `serverKit`/`clientKit`) | | `gear:outer` | `OuterGear` | optional | `BaseGear`, `OuterGear`, `gearKit`; any Shell via `res.perLocale` (§4.1) | other `OuterGear` and consumers via `Plug` / `ClientPlug` (not `ServerPlug`) | no | optional | | `gear:outer(vertex)` | `OuterGear` | optional | same as `gear:outer` | only consumers via `Plug` / `ClientPlug`; **cannot be a dep of any resource**; instance scoped per call (or shared via ``) | no | optional | | `shell` | `Shell` | no | `Shell`, `shell(mono)`, `BaseGear` (only if in `bridgeGears`), `shellKit`; any Shell in any locale via `res.perLocale` (§4.1) | other `Shell` and consumers | yes | yes (`shellKit`) | | `shell(mono)` | `Shell` | no | same as `shell` | other `Shell` and consumers | yes | yes (`shellKit`) | Dep-graph asymmetry: no *plain* dep path connects `gear:inner` to `gear:outer`, `gear:outer(vertex)`, or `Shell` — enforced at the `withDeps(...)` call site by the compiler. The one sanctioned bridge to a `Shell` is `res.perLocale(...)` (§4.1), which admits it as a locale **loader** `(locale) => Promise`, not a resolved surface. --- ## 3. Setup A project has two declarative configuration files by convention — `resource-atlas.ts` and `setup.ts` — plus per-folder `loader.ts` files that wire module loading (see §3.3, "Module loading"). ### 3.1. `defineLayout` Maps folder prefixes to families. Prefix matching is **longest-match wins**. Layout keys must end in `/`; missing trailing slash is a compile error. ```ts const folders = defineLayout({ "inner/": "gear:inner", "base/": "gear:base", "outer/": "gear:outer", "vertex/": "gear:outer(vertex)", "shell/": "shell", "shell/lib/": "shell(mono)", }); ``` A file at `inner/inventory.ts` resolves to family `gear:inner`, namespace `inner/inventory`. A file at `vertex/cart.ts` resolves to `gear:outer(vertex)`, namespace `vertex/cart`. ### 3.2. `ResourceAtlas` A named class built from `defineLayout`'s return plus a type-level `ResourceMap`: ```ts type ResourceMap = { "inner/inventory": Inner_Inventory; "outer/cart": Outer_Cart; "vertex/search": Vertex_Search; "shell/product": Shell_Product; }; export class ResourceAtlas extends folders() {} ``` Keys whose namespace doesn't match any layout prefix are filtered out of the type-narrowed atlas. The error surfaces as a compile-time `RMachineTypeError` at the call site of `ResourceAtlas.getTokenBuilder()`, listing the offending keys — e.g. `RMachineTypeError<"Invalid namespaces declared in atlas shape (dropped by layout filter): *** shell_wrong/common ***">`. A TypeScript limitation prevents flagging the mistake at the atlas declaration itself. `ResourceAtlas.getTokenBuilder()` returns a factory minting typed runtime-opaque handles carrying their namespace at the type level. Tokens and string literals are interchangeable everywhere a dep handle is accepted: ```ts const token = ResourceAtlas.getTokenBuilder(); export const tasks = token("outer/tasks"); // OuterGear.withDeps(tasks).define(...) ≡ OuterGear.withDeps("outer/tasks").define(...) ``` #### Internal namespaces (`#` prefix) Prefix an atlas key with `#` to mark its namespace as **internal**. An internal namespace is visible only inside the resource network — usable as a `withDeps(...)` target by other gears/shells, and referenceable from `gearKit` / `shellKit` — but it is **filtered out of every consumer-facing surface** (`Plug`, `ClientPlug`, `ServerPlug`, `DirectPlug`). ```ts type ResourceMap = { "base/config": Base_Config; // public — reachable via Plug / ClientPlug / ServerPlug "#base/jwt": Base_Jwt; // internal — only reachable as a gear→gear dep "outer/cart": Outer_Cart; }; ``` Typical use: utility resources that should never appear in UI code — JWT/crypto helpers, server-only adapters, internal caches. Consuming an internal namespace from a component is a compile error (the key isn't in the plug's accepted-namespace union). Rules: - The marker must be the **first character** (`"#base/jwt"`); suffix or mid-string `#` is not recognized. - Layout classification is unchanged — `#base/jwt` still resolves to `gear:base` via the `base/` prefix (the leading `#` is stripped before prefix matching). - Factory-side `gearKit` / `shellKit` may reference internal namespaces; the consumer `kit` / `clientKit` / `serverKit` may not (compile error). - The `#` is a **type-level marker only** — it never appears in filesystem paths or module loading. `#base/jwt` lives at `pub/base/jwt.ts`; the loader receives the unmarked path. - Use the same string everywhere a handle is needed: `withDeps("#base/jwt")`, `token("#base/jwt")`, `bridgeGears: ["#base/jwt"]`. ### 3.3. `RMachine.create` ```ts RMachine.create({ ResourceAtlas, // required locales: ["en", "it"] as const, // const tuple narrows L type defaultLocale: "en", // must be one of `locales` bridgeGears: ["base/config"], // optional: base namespaces visible to shells gearKit: { log: "base/logger" }, // optional: injected as $.kit.* into every gear factory shellKit: { fmt: "shell/lib/fmt" }, // optional: injected as $.kit.* into every shell factory experimental: { outerGear: "on" }, // type-level conditional gate for OuterGear in toolset }); ``` - `bridgeGears` is type-narrowed to `gear:base` namespaces only (an outer/inner namespace is a type error); `gearKit` / `shellKit` are injected as `$.kit.{name}` into every factory of that family. - `experimental.outerGear`: opt-in flag, `"on"`-or-absent (no `"off"` value — `{ outerGear: "off" }` is a type error). **Omitted is the default**: `OuterGear` is removed from the toolset at the type level. The `experimental` namespace gates features whose API may still evolve incompatibly; the flag is retired once the feature stabilizes. #### Module loading (`ResourceAtlas.loader`) How a namespace becomes a module is configured on the atlas, **not** in `RMachine.create`. Register a loader for one or more layout prefixes — or `"*"` as a catch-all fallback (a prefix-specific loader always wins over `"*"`). The fn receives the **full** resource path (e.g. `"base/config"`, locale-suffixed for shells): ```ts ResourceAtlas.loader.register(["base/", "shell/", "shell/lib/", "outer/", "vertex/"], (path) => devImport ? devImport(`./${path}`) : import(`./${path}`) ); ``` Loaders are split across the two resource folders (layout in §16) so server-only code never reaches the client bundle. Each folder owns a `loader.ts` whose `import()` glob is rooted there (so its bundler context is non-empty even before any resource exists — green-field builds don't break): - `pub/loader.ts` registers the client-safe prefixes (`base/`, `outer/`, `vertex/`, `shell/`, `shell/lib/`); imported for its side effect from `setup.ts`. - `prv/loader.ts` is fenced with `import "server-only"` and registers `["inner/"]`; imported from `server-toolset.ts`. Because the server-only glob lives only in this fenced module, `inner/` chunks never enter the client bundle. - A project with **no** server-only resources needs only `pub/` and can register `["*"]` as a single catch-all (React SPA, standalone, Next apps without `inner/`). ### 3.4. Toolset ```ts export const { InnerGear, BaseGear, OuterGear, Shell, DirectPlug, localized, res } = rMachine.createToolset(); export type Locale = RMachineLocale; export type { BrandedResource as RShape } from "r-machine"; ``` ### 3.5. `PathAtlas` (Next.js only) Declares the application's localized URL paths in one place. Each canonical path key is the route as it appears in the `app/` folder; per-locale translations are siblings of nested sub-paths. Lives in its own file (`r-machine/path-atlas.ts`) and is passed to the Next.js strategy at setup time. ```ts // r-machine/path-atlas.ts import { declarePathAtlas } from "@r-machine/next"; import type { Locale } from "./setup"; export class PathAtlas extends declarePathAtlas().as({ "/example-static": { it: "/esempio-statico", // translation for locale "it" "/page-1": { it: "/pagina-1", }, "/page-2": { en: "/page-2-in-english", // explicit canonical override per locale it: "/pagina-2", }, }, "/example-dynamic": { it: "/esempio-dinamico", "/[slug]": {}, // dynamic segment: empty value, no translations }, }) {} ``` Rules: - **Locale entries** are sibling keys named after a declared locale (`"it": "/..."`); the value is that locale's `/`-prefixed translation. - **Sub-path entries** are sibling keys starting with `/` (`"/page-1": {...}`); they nest recursively. - **Dynamic segments** (`"/[slug]"`, `"/[...rest]"`, `"/[[...rest]]"`) take an empty `{}` — no translations, no children. - The default locale needs no translation entry (canonical key used as-is unless overridden — see `/page-2`'s `en:`). - Validation runs at strategy-construction time; mismatches between `PathAtlas` keys and the actual `app/` folder are the developer's responsibility. Pass the class (not an instance) to the strategy via the `PathAtlas` option. It is optional — omitted, URLs are the literal `app/` folder paths in every locale. --- ## 4. Composers All composers expose a chain. Every step except `define` is optional, and `define` is always last. | Composer | Chain | |---|---| | `InnerGear` | `withDeps → withPorts → define` | | `BaseGear` | `withDeps → withPorts → define` | | `OuterGear` | `withDeps → withPorts → withState → define` | | `Shell` | `withDeps → withPorts → define` | `Shell` does not support `withState`. ### 4.1. `withDeps(...)` Two forms — list and map — yielding tuple or named-key access in the factory: ```ts InnerGear.withDeps("inner/clock", "base/config").define((plugin) => { // list form const [clock, config, $] = plugin; /* ... */ }); InnerGear.withDeps({ clock: "inner/clock", config: "base/config" }).define((plugin) => { // map form const { clock, config, $ } = plugin; /* ... */ }); ``` The factory takes a single first argument — `plugin` — uniform across all four composers (`InnerGear`, `BaseGear`, `OuterGear`, `Shell`). Destructure it on the first line of the body: - **No deps** (or `withDeps()` with no arguments) — map form (default): `const { $ } = plugin;`. - **Map form** (`withDeps({ name: "ns" })`) — `const { name, $ } = plugin;`, with `$` alongside the named deps. - **List form** (`withDeps("ns1", "ns2")`) — `const [dep1, dep2, $] = plugin;`, with `$` always as the last tuple element. **`res.perLocale("shell/x")` — a `Shell` as a locale-loader dep.** Wrapping a shell namespace with `res.perLocale(...)` (from the toolset, §3.4) resolves it not to a surface but to a **loader** `(locale) => Promise`, so a locale-agnostic gear — or a `Shell` reusing another locale — reads localized content at runtime; `locale` is typed to the configured locale union. Usable in `withDeps` of any gear family and `Shell`, in list or map form: ```ts BaseGear.withDeps({ hi: res.perLocale("shell/greeting") }).define((plugin) => { const { hi } = plugin; // hi: (locale: Locale) => Promise return { greet: (locale: Locale) => hi(locale) }; }); ``` Mock it with a function (§14.2). Two resolution-time batch helpers on `res.perLocale` (they close over the configured locales) fold multiple loaders in one call: `res.perLocale.pickAll(loader | map)` resolves **every** locale, locale-major (`Record` / `Record`); `res.perLocale.pick(locale, map | tuple)` resolves a batch at **one** locale, shape preserved. ### 4.2. `withPorts({...})` Declares external values (server actions, SDK clients, fetch wrappers, locale-aware data sources) used inside the factory. Accessed as `$.ports.{name}`. ```ts import { createPost } from "../lib/actions"; OuterGear .withDeps("base/config") .withPorts({ createPost }) .withState({ pending: false }) .define((plugin, _) => { const [config, $] = plugin; return { submit: async (title: string, body: string) => { await $.ports.createPost(title, body); }, }; }); ``` Ports are inputs to the gear/shell, not part of the consumer Surface. Available on all four composers (`InnerGear`, `BaseGear`, `OuterGear`, `Shell`). ### 4.3. `withState(initial)` Available on `OuterGear` only. Provides `$.state` and `$.defaultState` to the factory. ### 4.4. `define(factory)` Final step. The user factory receives the `plugin` context as its first argument (carrying `$`, deps, and kit) and the cursor `_` as its second for `OuterGear`, and returns the resource shape — see §6 (cursor primitives) and §8 (factory `$` context). `define(...)` itself does **not** return the resource shape. It returns a `ResMatrix` — the canonical value exported as `r` by every resource module: | Property | Type | Purpose | |---|---|---| | `r.plug` | typed plug | The plug handle for this resource, used as the target of `mockPlug(...)` (§10.3, §14). Carries deps/ports/locale narrowing at the type level. | | `r.clone(fn?)` | `() => ResMatrix` / `(fn: (res: R, plugin, [cursor]) => T) => ResMatrix` | Returns a fresh, independent `ResMatrix` reusing the same factory and chain, with an optional transform `fn` that overrides fields of `R` (locked to the same shape — see §5). Pair with `withPorts(...)` / `withState(...)` for ports / state overrides. | The shape is uniform across `InnerGear`, `BaseGear`, `OuterGear`, and `Shell` (including `shell(mono)`). The convention everywhere in this document is `export const r = ..define(...)`; `r` is always a `ResMatrix`. ### 4.5. `OuterGear` shorthand forms Stateless: `OuterGear.define(() => ({ greet: (name: string) => `hello ${name}` }))`. Stateful — array shortcut. R-Machine synthesises a default identity getter and (read-write only) a canonical action `(partial) => state`: read-write returns `[getterName, actionName]`, readonly returns `[getterName]`: ```ts OuterGear.withState({ count: 0 }).define(() => ["counter", "setCounter"]); // { counter; setCounter } OuterGear.withState({ count: 0 }).define(() => ["counter"]); // { counter } — readonly ``` Stateful — full custom: see §6 cursor primitives. ### 4.6. Vertex (`gear:outer(vertex)`) Same `OuterGear` composer. There is no `VertexGear` export. What makes a resource a vertex is its **layout entry**, not the call shape — an ordinary `OuterGear` declared under a `gear:outer(vertex)` prefix: ```ts // pub/vertex/shopping-cart.ts (export type Vertex_ShoppingCart = RShape) export const r = OuterGear.withState({ items: [] as string[] }).define((plugin, _) => { const { $ } = plugin; return { state: _.getter(), add: _.action((item: string) => ({ items: [...$.state.items, item] })), count: _.getter(() => $.state.items.length), }; }); ``` Each `Plug("vertex/...").useR()` call creates a fresh instance with lifecycle bound to that component. **Vertex gears cannot be a dependency of any other resource** — reachable only through `Plug` / `ClientPlug` from a component. ### 4.7. `Shell` — multi-locale The canonical file exports `r` (and `export type Shell_X = RShape`). R-Machine derives the shape from it. It can be a **plain object** or a **factory** — use a factory when the canonical needs `$.locale`, `$.kit`, `$.ports`, deps, or async: ```ts // pub/shell/common/en.ts — plain object export const r = { greeting: "Hello", farewell: "Goodbye" }; // pub/shell/common/en.ts — factory (locale-aware) export const r = Shell.define((plugin) => ({ greeting: `Hello (${plugin.$.locale})` })); // pub/shell/landing/en.ts — factory with ports (async load) export const r = Shell.withPorts({ fetchHeroCopy }).define(async ({ $ }) => { const data = await $.ports.fetchHeroCopy($.locale); return { hero: data.hero, sub: data.sub }; }); // pub/shell/common/en.ts — with deps export const r = Shell.withDeps("base/config").define((plugin) => { const [config, $] = plugin; return { hero: `Welcome — API: ${config.apiBase}`, copy: $.kit.fmt.number(1000) }; }); ``` **Variant files** use `localized(namespace, value)`, which performs **exact-keyed validation** against the canonical type (extra keys → `never`; missing keys → `Property '...' is missing`): ```ts // pub/shell/common/it.ts export const r = localized("shell/common", { greeting: "Ciao", farewell: "Arrivederci" }); ``` For variants needing a factory (async load, locale-aware computation), wrap `localized` inside `Shell.define`: ```ts export const r = Shell.define(async () => { await loadHeavyData(); return localized("shell/common", { greeting: "Ciao", farewell: "Arrivederci" }); }); ``` ### 4.8. `shell(mono)` A **single-file locale-aware** resource: it has access to `$.locale` like any shell, but no per-locale variant files. Use for formatters and locale-aware helpers without translation. The mono nature comes from the layout entry (`"shell/lib/": "shell(mono)"`). ```ts // pub/shell/lib/fmt.ts import { type RShape, Shell } from "@/r-machine/setup"; export const r = Shell.define((plugin) => { const { $ } = plugin; return { number: (n: number) => new Intl.NumberFormat($.locale).format(n), date: (d: Date) => new Intl.DateTimeFormat($.locale).format(d), }; }); export type Shell_Lib_Fmt = RShape; ``` #### Locale-aware formatting via the platform `Intl.*` primitives Locale-aware formatting uses the `Intl.*` family — `NumberFormat`, `DateTimeFormat`, `PluralRules`, `RelativeTimeFormat`, `ListFormat`, `Collator`, `Segmenter`, `DisplayNames`. These are already locale-aware natively, zero-bundle (built into every modern runtime — browser, Node, Deno, Bun, edge), and minimal in surface, so R-Machine does not wrap them. R-Machine provides the *wiring* — `$.locale` automatically passed, `shellKit` for cross-shell injection, `mockPlug` for test-time substitution. The primitives stay the platform's; the integration is R-Machine's. #### Pluralization in a few lines A pluralization helper is a few lines of TypeScript on top of `Intl.PluralRules`. Two variants, depending on how many CLDR plural categories the project's locales need. For locales with binary plural rules (English, Italian, German, French, Spanish, ...): ```ts // pub/shell/lib/fmt.ts import { type RShape, Shell } from "@/r-machine/setup"; type PluralForms = { one?: string; other: string }; export const r = Shell.define((plugin) => { const { $ } = plugin; const pluralRules = new Intl.PluralRules($.locale); return { number: (n: number) => new Intl.NumberFormat($.locale).format(n), date: (d: Date) => new Intl.DateTimeFormat($.locale).format(d), plural: (count: number, forms: PluralForms) => { const cat = pluralRules.select(count); const tpl = (cat === "one" ? forms.one : undefined) ?? forms.other; return tpl.replace(/#/g, String(count)); }, }; }); export type Shell_Lib_Fmt = RShape; ``` For locales with multi-category plural rules (Russian, Polish, Arabic, ...), widen the input type and look up by category — same `Shell.define` wrapper as above: ```ts type PluralForms = Partial> & { other: string }; // inside the returned object: plural: (count: number, forms: PluralForms) => { const tpl = forms[pluralRules.select(count)] ?? forms.other; return tpl.replace(/#/g, String(count)); }, ``` Consumer-side, the call sites are typed and explicit: ```ts // en/it fmt.plural(count, { one: "# item", other: "# items" }) // ru fmt.plural(count, { one: "# элемент", few: "# элемента", many: "# элементов", other: "# элемента", }) ``` The signature `plural(count: number, forms: PluralForms)` is type-checked end-to-end: `other` is required (omitting it is a compile error), unknown keys are rejected, `count` is enforced to be a number. A consumer that forgets a required form, or that passes the wrong type, surfaces the mistake at compile time. ### 4.9. Resources are TypeScript factories R-Machine imposes a structural boundary at the resource edge — locale scoping for shells, kind classification for gears, dep-graph asymmetry between families — and **nothing else** on what the factory returns. A resource is a typed TypeScript factory; inside the boundary the full expressive power of the language is available. R-Machine introduces no DSL for content, no template syntax for strings, no wrapper layer over external libraries: there is nothing to learn beyond TypeScript itself. Two consequences follow. #### Bring your own formatter / parser / renderer If a project needs a locale-aware primitive R-Machine doesn't ship — ICU `MessageFormat`, Fluent, a Markdown renderer, an in-house format — there is no integration story. The library is imported inside a `shell` or `shell(mono)` factory; `$.locale` is the wiring. A `shell(mono)` exposing ICU `MessageFormat` via `intl-messageformat`: ```ts // pub/shell/lib/icu.ts — import the library, $.locale is the wiring import { IntlMessageFormat, type PrimitiveType } from "intl-messageformat"; export const r = Shell.withPorts({ IntlMessageFormat }).define(({ $ }) => { const cache = new Map(); const compile = (msg: string) => cache.get(msg) ?? (cache.set(msg, new $.ports.IntlMessageFormat(msg, $.locale)), cache.get(msg)!); return { format: (msg: string, values?: Record) => compile(msg).format(values) as string, }; }); ``` The same shape applies to any locale-aware library — date formatters (`date-fns`, `luxon`), transliterators, Markdown/MDX renderers, locale-aware collators. R-Machine's role is the boundary (`$.locale` auto-passed, kit injection, `mockPlug` for tests), not the format. This generalizes §4.8: there is no architectural commitment to any particular i18n library because there is no integration to commit to. #### Return anything TypeScript can express A resource's shape is whatever the factory returns. For a shell, that is not restricted to strings: ```tsx // pub/shell/features/intl_demo/en.tsx export const r = Shell.define(({ fmt }) => ({ sectionTitle: "Locale-Aware Formatting", // plain string caption: (d: Date) => `Today's date: ${fmt.date.long(d)}`, // fn → string description: (amount: number) => ( // fn → JSX fragment <>The value {fmt.currency(amount)} is {fmt.number(amount)}. ), Items: ({ count }: { count: number }) => ( // React component You have {fmt.plural(count, { one: "# item", other: "# items" })}. ), })); ``` The shape flows end-to-end to the consumer: `Plug("shell/").useR()` returns exactly what the factory returned, type-narrowed. The same freedom applies to gears — an `OuterGear` may surface state cursors next to JSX components next to instances of an external class, whatever the call site needs. **Implication.** For any question of the form *"does R-Machine support X?"* — where X is a content type, formatting style, or rendering pattern — the answer is *"X is whatever the factory returns, because a resource is a TypeScript factory."* The API surface is intentionally narrow: it provides the wiring (locale, kit, ports, plug), the boundary classification (kinds, dep rules), and the test substitution primitive (`mockPlug`) — nothing more. #### On architectural lock-in Adopting R-Machine couples a codebase to its consumption API — consumers read through `Plug` / `ClientPlug` / `ServerPlug`, resources are declared with its composers. That coupling is real, the kind any architecture with a central consumption primitive incurs. What it does *not* reach is the substance: a resource is a plain TypeScript factory returning ordinary values, so its logic and content stay portable TypeScript. The R-Machine-specific part is the wiring around it — atlas, composer chain, plug calls — narrow and mechanical. Consumers depend on a string namespace and a typed shape, both framework-agnostic; migrating would rewrite the wiring, not the business logic. --- ## 5. Cloning resources `clone(fn?)` is the mechanism for **declaring a second resource that reuses the logic of an existing one under a different atlas namespace**. It lives at module / atlas level, not inside a single module. The convention everywhere in R-Machine is **one module = one `export const r`**. `clone` does not break it — it is used in a *separate* resource module that imports the source `r`, calls `clone(...)` on it, and re-exports the result as its own `r`. That second module is then registered in `ResourceAtlas` under its own namespace key, just like any other resource. The matrix returned by every `.define(...)` exposes a small **fluent builder** mirroring the composer side: ``` composer.withPorts(...).withState(...).define(fn) ← create from scratch matrix.withPorts(...).withState(...).clone(fn?) ← derive from existing ``` `withPorts` / `withState` produce intermediate builders whose only terminal is `clone(fn?)`. The optional `fn` is a transform that the system runs **after** the original factory and **before** post-processing, so it receives the resource already resolved (locale, deps, state) and can override only the fields it wants — the rest pass through unchanged. The transform never widens the result shape: extra keys outside `R` are pinned to `never` at the type level (the matrix's `clone` is generic over the inferred return type, and `NoExcess` blocks excess properties at the call site). ```ts // pub/outer/cart.ts ← source (export type Outer_Cart = RShape) export const r = OuterGear .withPorts({ checkout: prodCheckout }) .withState({ items: [] as Item[] }) .define((plugin, _) => { /* ... */ }); // pub/outer/cart-secondary.ts ← derived resource, own module + own atlas key import { r as base } from "./cart"; export const r = base.clone(); // identical logic, new identity ``` Both entries (`"outer/cart"` and `"outer/cart-secondary"`, both typed `Outer_Cart`) share the same factory and chain, but each carries its **own plug, its own resolved instance, and its own state** (for stateful `OuterGear`). They are independent at runtime. ### 5.1. `clone` vs `mockPlug` `clone` is an **atlas-level** production construct (a second `r` in a second module, new namespace, new plug/instance, lives in the deployed atlas); `mockPlug` (§14) is a **consumer-level** test/scoped override that reuses the original resource's identity and swaps what the consumer sees. So `clone` is never a side-export next to `export const r = ...` — for that you want `mockPlug` (tests) or a separate resource module (production). ### 5.2. Builder methods by composer | Composer | Available on the matrix | |---|---| | `InnerGear`, `BaseGear` | `clone(fn?)`, `withPorts(p).clone(fn?)` | | `OuterGear` (stateless) | `clone(fn?)`, `withPorts(p).clone(fn?)` | | `OuterGear` (stateful, declared with `withState`) | `clone(fn?)`, `withPorts(p).clone(fn?)`, `withState(s).clone(fn?)`, `withPorts(p).withState(s).clone(fn?)` (commutative) | | `Shell` (incl. `shell(mono)`) | `clone(fn?)`, `withPorts(p).clone(fn?)` | `withPorts(p)` shallow-merges `p` onto the existing port map: only the keys you list are replaced. `withState(s)` accepts `DeepPartial` and deep-merges onto the original `defaultState`: only the leaves you provide are replaced, everything else is preserved. The `fn` transform is locked to the resource's `R` shape — it can override any subset of `R`'s keys but cannot add new ones (the matrix exposes a future, differently-named method for cases that genuinely need to widen the resource). ### 5.3. Use cases #### 5.3.1. Same logic, multiple atlas slots — `clone()` no-arg Use when N independent instances of the same gear must coexist under distinct namespaces. Examples: a product-comparison page rendering three cards side by side, multiple carts in a multi-tenant UI, or the same gear surfaced both globally and inside a `gear:outer(vertex)` (§12). ```ts // pub/outer/product-card.ts ← source (export type Outer_ProductCard = RShape) export const r = OuterGear.withState({ productId: "", qty: 1 }).define((plugin, _) => { /* … */ }); // pub/outer/product-card-a.ts, -b.ts, -c.ts ← one no-arg clone per slot import { r as base } from "./product-card"; export const r = base.clone(); ``` Register each slot under its own key in `resource-atlas.ts` (`"outer/product-card-a": Outer_ProductCard`, …). Each slot has its own state cursor — mutating slot A does not touch B or C. The **global + vertex** variant follows the same shape: keep the logic in one module, then declare a no-arg clone in a `vertex/...` module so the same gear can be instantiated locally per vertex frame (§12) without sharing state with the global one. #### 5.3.2. Variant with different external bindings — `withPorts(...).clone()` Use when the same logic must run against a different external boundary — a draft writer instead of the published one, a stub fetcher instead of the CMS. ```ts // pub/outer/post-form.ts ← source (export type Outer_PostForm = RShape) export const r = OuterGear .withPorts({ createPost: prodCreatePost }) .withState({ pending: false }) .define((plugin, _) => { /* ... */ }); // pub/outer/post-form-draft.ts ← variant, own atlas key "outer/post-form-draft" import { r as base } from "./post-form"; export const r = base.withPorts({ createPost: draftCreatePost }).clone(); ``` The same shape applies to `Shell` variants (e.g. a `shell/landing-static` derived from `shell/landing` with a stub `fetchHeroCopy`). #### 5.3.3. Variant with different starting state — `withState(...).clone()` Available on stateful `OuterGear` only. Combine with `withPorts` when both need to change — the chain is commutative: ```ts // pub/outer/post-form-debug.ts import { r as base } from "./post-form"; export const r = base .withPorts({ createPost: loggedCreatePost }) .withState({ pending: true }) .clone(); ``` #### 5.3.4. Sibling locale variant — `clone(fn)` for regional overrides The common case is a second locale that is almost identical to the first, with a handful of phrases that differ (US vs UK spelling, dialectal swaps, regulator-mandated wording). The factory runs in the **clone's** locale context, so anything that already depends on `$.locale` is correct in `res` — `fn` only has to override the values that genuinely differ between regions. ```ts // pub/shell/checkout/en-US.tsx — source variant: Shell.define(({ $ }) => ({ cta: "Add to cart", colorLabel: "Color", … })) // pub/shell/checkout/en-GB.tsx — sibling locale, mostly identical import { r as enUS } from "./en-US"; export const r = enUS.clone((res, { $ }) => ({ ...res, colorLabel: "Colour", // UK spelling zipPlaceholder: `Postcode for ${$.locale}`, // UK terminology })); ``` Both files share a single atlas key (`"shell/checkout": Shell_Checkout`), matching the multi-locale `Shell` convention (§4.7): one namespace, one file per locale. The clone-with-`fn` form lets en-GB inherit en-US's structure without copying the whole literal, and shape drift is blocked — `{ ...res, prova: 21 }` errors because `prova` is not a key of `Shell_Checkout`. The same fits gear variants where a few fields of `R` differ between deployments. ### 5.4. Semantics Each clone has its own plug and (stateful `OuterGear`) its own state cursor — resolving/mutating a clone never touches the source, and sibling clones are independent. `clone` of a `clone` accumulates transforms left-to-right (`fn_n(...fn_1(originalFactory(...)))`). The original factory is awaited first, then `fn(res, plugin[, cursor])` runs against the fully-resolved `R` shape; `fn` may be sync or async (consuming `res` never needs `await`). The `fn` return is shape-locked to `R` via `NoExcess` — `{ ...res, extra: 1 }` is a compile error; override existing keys, never add new ones. For stateful gears R-Machine rebuilds the state machinery so `$.state` / `$.defaultState` / cursors stay coherent with the merged value. `clone` is factory-time only — the cloned `r` must be wired into `ResourceAtlas` to participate at runtime. --- ## 6. Cursor primitives Inside an `OuterGear` factory, `_` (second argument) is the only way to declare reactive members. Each primitive produces a branded value. The matrix of which primitives are available follows the composer chain (e.g. `_.action`, `_.relay`, `_.cmd` require `withState(...)`). ### 6.1. `_.action(reducer?)` — `Action` Synchronous state-mutating reducer. Returns `DeepPartial`; runtime merges into current state. ```ts const inc = _.action(() => ({ n: $.state.n + 1 })); const set = _.action((n: number) => ({ n })); const fill = _.action(); // canonical (partial) => S ``` The canonical form (`_.action()`) doubles as the way to seed initial state from inside the factory, when it must be derived from values only available there rather than the static value passed to `withState(...)`. Typical motivations: init computed from `$.kit` / `$.ports` / awaited async data; SSR hydration from a server snapshot fetched via a port (§11.9); persistence rehydrated from `localStorage` / `IndexedDB` / a cookie. ```ts export const r = OuterGear.withPorts({ loadInitial }).withState({ items: [] as string[] }) .define(async ({ $ }, _) => { _.action()({ items: await $.ports.loadInitial() }); // seed before returning return { state: _.getter(), add: _.action((i: string) => ({ items: [...$.state.items, i] })) }; }); ``` ### 6.2. `_.getter(...)` / `_.cell(...)` — `Getter` `_.getter` has two forms; `_.cell` is a third, dedicated form: ```ts const state = _.getter(); // identity for state const sum = _.getter(() => $.state.a + $.state.b); // ad-hoc derived (recomputes each read) const heavy = _.cell(() => bigComputation()); // its own cell: memoized + tracked ``` `_.cell` is short for **getterCell**: it backs the value with its own cell in the reactive graph. Two things follow — (1) it **memoizes** its body, and (2) it is its **own dependency**. It returns a `Getter`, so it is read-only. The cell form is also the unit of **fine-grained reactivity** on the consumer side: a component reading only it re-renders solely when the cell's output changes by `Object.is` — see §10.4. ### 6.3. `_.relay({ select, onChange, equals? })` Side-effecting subscription declared inside an `OuterGear`. `select` derives a value from state; `onChange` runs when that value changes and may dispatch one or more `Cmd`. Brand-tagged and **stripped from the consumer Surface** (it is wiring, not a value) — keep it on the resource under a `$`-prefixed key (§7) so you can still reach it during tests. ```ts const $myRelay = _.relay({ select: () => $.state.count, // () => T onChange: (curr, prev) => { if (curr > 10) return _.cmd(reset); }, // → void | Cmd | Cmd[] | Promise<…> equals?: "identity" | "shallow" | ((curr: T, prev: T) => boolean), }); ``` **Dependency tracking.** `select` takes no arguments; every state/getter/memo read during a run is recorded as a relay **dependency**, re-captured each run. When any tracked dependency changes the relay is marked dirty and re-evaluated. **When it fires.** On registration `select` runs once to capture dependencies and seed `prev` — **`onChange` does not fire** (a relay reacts to *changes*, not existence). On each subsequent change `select` re-runs; if `!equals(next, prev)`, `onChange(next, prev)` fires and `prev` advances. **What `onChange` may return:** - `void` / any non-`Cmd` value → nothing dispatched. - a single `Cmd`, or a `Cmd[]` → dispatched in array order (non-`Cmd` entries ignored), e.g. `return [_.cmd(notify, "limit reached"), _.cmd(reset)]` runs `notify` then `reset`. - a `Promise` → handled **out-of-band**: it does not block the current update, is awaited on a microtask, and resolved commands dispatch in a *fresh* transaction (so they observe post-update state). A rejection is swallowed and reported as a `relay:onChangeError` event. **`equals?`** — compares `select`'s current vs previous to decide whether `onChange` fires (`true` = equivalent, skip). Built-in name or custom comparator, default `"identity"`: - `"identity"` — `Object.is` (default; a `select` rebuilding an equivalent object each pass fires every time). - `"shallow"` — first-level key/element equality via `Object.is` (nested by reference). Use when `select` returns a freshly-built object/array (e.g. `() => ({ count: $.state.count, label: $.state.label })`) whose contents, not identity, matter. - `(curr, prev) => boolean` — custom predicate. **Errors are isolated.** A relay failure never throws into the triggering action; it surfaces as a `relay:onChangeError` event (§15.2). `select` throws → `onChange` is skipped and the relay stalls until a later `select` succeeds; `onChange` throws → `prev` still advances (next change fires normally), no commands dispatched; a dispatched `Cmd`'s action throws → the remaining commands still run. **Disposal.** When the owning `OuterGear` instance is disposed (§9), each relay is torn down (subscriptions removed, deregistered). There is **no final `onChange`** — disposal is silent. ### 6.4. `_.cmd(action, ...args)` — `Cmd` A reified, type-checked action call: `_.cmd(action, ...args)`, where `action` is a reference returned by `_.action(...)` and `...args` are that action's parameters, checked against `Parameters`. It is the value a relay's `onChange` returns to request a state change. ```ts const reset = _.action(() => ({ count: 0 })); const notify = _.action((msg: string) => ({ lastMessage: msg })); // inside onChange: return [_.cmd(notify, "limit reached"), _.cmd(reset)]; ``` A `Cmd` is an **inert descriptor** — it pairs an action with its bound arguments and does nothing on its own. Creating one has no side effect, and the arguments are captured at creation time (a snapshot — they are not re-read at dispatch, so there is no staleness). The relay runtime collects the commands an `onChange` returns and dispatches them during the update's command phase (see §6.5), calling `action(...args)` in order. Returning a command instead of calling the action directly inside `onChange` is what keeps mutations *out* of the select/notify phase: every relay in the batch observes a consistent state before any command-driven mutation begins. ### 6.5. Execution model: the flush `_.action`, `_.relay`, and `_.cmd` interact through a single **flush** that runs at the end of the outermost action's transaction and loops over three phases until no relay or state cell remains dirty: 1. **Relays** — every dirty relay's `onChange` fires, in deterministic order; the commands they return are **collected, not yet dispatched**, so all relays in the batch observe the same state. 2. **Commands** — the collected `Cmd`s are dispatched in order (`action(...args)`), each as a nested transaction; their mutations feed back into the same dirty queues. 3. **Notifications** — subscribed consumers (React components, §10.4) are notified, deduplicated per cell. Command-driven mutations from phase 2 can re-dirty relays, restarting the loop. Two consequences worth designing around: - **Ordering.** By default relays fire in registration order. A fully configured `RMachine` installs a dependency-graph ordering: relays fire by distance from the mutation's source namespace, then by atlas-declared priority, then registration order — deterministic across runs. - **Loop protection.** A relay may fire at most **3 times per flush**. A relay whose `onChange` dispatches a command that mutates the very dependency its `select` reads will re-fire each loop; on the 4th it emits a `relay:loopDetected` event (§15.2) and throws `RelayLoopError` (carrying the relay name and fire count), aborting the flush. A further hard cap stops any flush after 100 iterations. Guard such relays with `equals` or a condition in `onChange` so the selected value eventually stabilizes. --- ## 7. `$`-prefix convention Members of the returned resource whose key starts with `$` are stripped from the public Surface (and IDE tooltips) but present on the resource for testing — e.g. `$watch: $myRelay` or `$internalAction: _.action(...)` are hidden from the Surface yet reachable via `mockPlug`. --- ## 8. Factory `$` context The `$` plugin context received by every factory has fields conditional on what the composer chain declared: | Field | Present when | What it is | |---|---|---| | `$.kit` | the kit map for this family is non-empty | the resolved kit | | `$.locale` | the resource is a `Shell` or `shell(mono)` | active locale, narrowed to the project's locale union | | `$.state` | the gear was declared with `withState(...)` | current state value, narrowed to `S` | | `$.defaultState` | the gear was declared with `withState(...)` | the value passed to `withState(...)` (or to the `state` override on a clone — see §5) | | `$.ports` | the resource was declared with `withPorts({...})` | the record of ported external values (with any clone-time overrides applied — see §5) | Fields not declared are absent from the type (destructuring `{ state }` in a stateless gear is a type error, not runtime `undefined`). This is the **factory** `$`, distinct from the **consumer** `$` returned by `plug.useR()` (different members and presence rules — see §10.5). --- ## 9. Lifecycle: `[Symbol.dispose]` convention When a factory acquires resources whose lifetime exceeds a single call (intervals, subscriptions, listeners, connections, watchers), it returns an object that carries a `[Symbol.dispose]` teardown: ```ts export const r = OuterGear.withState(0).define(({ $ }, _) => { const update = _.action(() => $.state + 1); const handle = setInterval(() => { update(); }, 1000); return { value: _.getter(), [Symbol.dispose]: () => clearInterval(handle) }; }); ``` - `[Symbol.dispose]` is the standard TC39 well-known symbol for synchronous disposal — no r-machine helper needed (an optional `dispose(value)` wrapper exists, §9.1). It is invisible to `Plug(...).useR()` (all symbol keys are filtered from the surface). - Teardown runs exactly once per instance whose factory completed successfully (a factory that throws before returning runs none — see §10.6 on leaks). The runtime guarantees idempotency even across a manual call plus r-machine's slot disposal. - Triggers per family: vertex gears on consuming-component unmount; global outer/base/inner gears and shells on app shutdown / hot reload. Available in every family (incl. vertex and `shell(mono)`). **Async dispose is not supported.** If a factory returns an object with `[Symbol.asyncDispose]`, r-machine throws `RMachineUsageError(ERR_ASYNC_DISPOSE_NOT_SUPPORTED)` at resolve time. Use `[Symbol.dispose]` with a synchronous closure; if cleanup is inherently async, fire-and-forget the async work from inside the sync teardown. ### 9.1. Manual disposal in tests The resource from `ctrl.createRes()` retains `Symbol.dispose` on its `TestSurface` and is compatible with the TC39 explicit resource management proposal. You rarely dispose it by hand: **the controller auto-disposes every instance it created on reset**, so `using ctrl` alone tears them down. When you do want explicit control, three equivalent teardowns work — `value[Symbol.dispose]()`, the `dispose(value)` wrapper (from `r-machine`), or scope-bound `using value = await ctrl.createRes()` (TS 5.2+, lib `esnext.disposable`). --- ## 10. Plug variants ### 10.1. Call shapes Single-resource returns a one-element tuple; list form is positional, map form named. All shapes work on `Plug`, `ClientPlug`, `ServerPlug`: ```ts const [tasks] = Plug("outer/tasks").useR(); // single const [tasks, common, config] = Plug("outer/tasks", "shell/common", "base/config").useR(); // list const { tasks, common, config } = Plug({ tasks: "outer/tasks", common: "shell/common", config: "base/config" }).useR(); // map ``` ### 10.2. Variants and runtime contexts | Variant | Runtime | Sync/Async | Can consume | |---|---|---|---| | `Plug` | React standard strategy (client-side) | sync (suspends if unresolved) | `shell`, `shell(mono)`, `gear:base`, `gear:outer`, `gear:outer(vertex)` | | `ClientPlug` | Next.js Client Component | sync (suspends if unresolved) | same as `Plug` | | `ServerPlug` | Next.js RSC | returns `Promise` (await it) | `shell`, `shell(mono)`, `gear:base`, `gear:inner` | | `DirectPlug` | **anywhere** (core toolset — React, Next, worker, cron, email render) | returns `Promise` (await it); locale passed to `useR(locale)` | `shell`, `shell(mono)`, `gear:base` | `ServerPlug` cannot consume `gear:outer` or `gear:outer(vertex)`. `Plug`/`ClientPlug` cannot consume `gear:inner`. **`DirectPlug` — the container-free base case.** Every other variant is *resolution + a container that holds the current locale* (React context for `Plug`/`ClientPlug`, the Next request scope/headers for `ServerPlug`). `DirectPlug` has no container: you pass the locale to `useR(locale)` yourself, resolution is async, and it carries no `setLocale`/`getPath`/`params`. Its deps are restricted to **shells + base gears** — exactly the resources whose resolution is a pure function of locale (no `gear:inner`/`gear:outer`/`gear:outer(vertex)`, which need a container). Because it depends on nothing framework-specific it comes from the **core toolset** (`rMachine.createToolset()`) and runs **anywhere**. Configure ambient shared resources via `directKit: { … }` on `RMachine.create(...)` (shell + base-gear namespaces only); they surface as `$.kit`. ```tsx // Same import in React and Next — it's on the core toolset. const plug = DirectPlug("shell/email/welcome"); export async function WelcomeEmail({ locale, name }) { const [s, $] = await plug.useR(locale); // async; $.locale echoes `locale`, $.kit = directKit return {s.greeting(name)}; } WelcomeEmail.plug = plug; // attached to the consumer for testing purposes with mockPlug ``` On the client, call `useR` in an async handler/effect — never directly in render (it is async, unlike the sync/Suspense `ClientPlug`). Prefer `ServerPlug` inside RSCs (it also auto-binds the request locale and adds `getPath`/`params`); reach for `DirectPlug` when there is **no** request/React context to bind to. ### 10.3. Surface type ```ts const [tasks] = Plug("outer/tasks").useR(); // type: Surface<{ count: number; add: (s: string) => ... }, "outer/tasks", "gear:outer"> ``` The branded view: `$`-keys stripped, `Getter` lifted to `V`, `Action` preserved as `F`, `Relay` removed entirely. A surface mismatch reads as `expected Surface<…, "outer/tasks", "gear:outer">, got Surface<…, "outer/cart", "gear:outer">`. Every defined resource carries an `r.plug` property, used by `mockPlug` (§14). ### 10.4. Reactive tracking (consumer re-render semantics) `OuterGear` state is reactive, and the wiring to React re-renders is **automatic and read-driven** — no dependency arrays, no selector functions, no consumer-side `useMemo`. The call shape (`Plug(...).useR()`) hides this, so it is documented here because it governs exactly when a component re-renders. **The contract.** When a component consumes an `OuterGear` through `Plug` / `ClientPlug`, every reactive value read *during render* is recorded; after commit it subscribes to exactly those reads and re-renders **only when a value it actually read last render changes** — never on an unrelated mutation or a value it stopped reading. So a `CartBadge` rendering `{cart.count}` re-renders when `count` changes but not when other cart state mutates. The subscription set is rebuilt on **every** render, always reflecting the current render's reads (never a stale declaration). #### Tracking granularity Two levels, selected by how the value is declared in the gear (§6): | Read | Dependency unit | Re-render trigger | |---|---|---| | Raw `$.state` — via `_.getter()` identity or a plain `_.getter(() => $.state.…)` | the **whole gear-instance state** | any action that changes *any* leaf of that instance's state | | A cell — `_.cell(() => …)` (§6.2) | the **cell itself** | only when the cell recomputes to a value that is **not** `Object.is`-equal to its previous output | Raw-state reads are deliberately coarse: reading *any* part of `$.state` subscribes the consumer to that instance's entire state object (no per-property proxy). For **fine-grained** "re-render only when *this* derived value changes" behavior, expose the value as a **cell** (`_.cell`): a cell is its own dependency, so when an unrelated action runs it is marked dirty, recomputes lazily on next read, and — if its output is `Object.is`-unchanged — notifies no one. ```tsx // pub/outer/cart.ts — within define((plugin, _) => { const { $ } = plugin; return {...} }) state: _.getter(), // whole-state dep count: _.cell(() => $.state.items.length), // fine-grained subtotal: _.cell(() => sum($.state.items)), // fine-grained addItem: _.action((i: Item) => ({ items: [...$.state.items, i] })), setCoupon: _.action((coupon: string) => ({ coupon })), ``` A component reading only `cart.count` re-renders when `items.length` changes but **not** when `setCoupon` runs (the `count` cell recomputes to the same number → no notification); one reading `cart.state` re-renders on either action. **No-op actions are free.** State merges use structural sharing: if a reducer yields a state with no changed leaf, the merged value is reference-identical to the previous one and **no subscriber is notified** — not even whole-state readers. **Reads inside actions are not tracked.** Reads in an `_.action(...)` reducer body run in a **silent zone** and never register as consumer dependencies; only reads during a component's render establish subscriptions. **Scope.** Reactive tracking is **client-side** — a property of `Plug` (React) and `ClientPlug` (Next.js). `ServerPlug` (RSC) has none (renders once per request, `await`s its resources, cannot consume `gear:outer` anyway — §10.2). Vertex gears (§12) carry the identical contract **per instance**: each `` instance (or frame-less `useR()` call) tracks its own consumers independently. **React Compiler is not needed for R-Machine code** — the reactivity is already read-driven and the expensive memoization lives in the gear layer (`_.cell`, relays), invisible to the compiler. For a mixed codebase with the compiler enabled globally, set `reactCompiler: "on"` on the strategy config (React standard + all Next App strategies; default `"off"`), accepting its per-re-render wrapping overhead. ### 10.5. Consumer `$` context `plug.useR()` returns the resolved resource(s) followed by a `$` consumer context — the trailing tuple element (list form) or the `$` key (map form). This `$` is **distinct** from the factory `$` of §8: it is the call-site context, with different members and different presence rules. Its fields are conditional on the plug variant and on what the strategy declared: | Field | Present when | What it is | |---|---|---| | `$.locale` | **always**, on every consumer plug (`Plug`, `ClientPlug`, `ServerPlug`, `DirectPlug`) | the active locale as a **canonical** `Locale` — always one of the codes declared in `setup.ts`, narrowed to the project's locale union. On `DirectPlug` it is a read-only echo of the locale you passed to `useR(locale)` | | `$.setLocale` | strategy plugs only (`Plug`, `ClientPlug`, `ServerPlug`) — **not** `DirectPlug` (it has no bound container to switch) | `(newLocale: Locale) => Promise` — switches the active locale and persists it (see §11.4) | | `$.kit` | the consumer kit map for this runtime is non-empty (`kit` / `clientKit` / `serverKit` / `directKit`) | the resolved consumer kit (see §11.4) | | `$.getPath` | **Next.js** plugs (`ClientPlug` / `ServerPlug`) with a `PathAtlas` declared | `BoundPathComposer` — type-checked path composer (see §11.5) | | `$.params` | **`ServerPlug` only**, resolved via the params overload `useR(params)` / `useUnboundR(params)` | the awaited route `params` record (locale key + dynamic segments); see §11.5 | Fields not present for a given plug are absent from the type — destructuring them is a compile error, not `undefined` at runtime (same contract as the factory `$`). Notes on variants: React `Plug` has `$.locale`, `$.setLocale`, `$.kit` (when declared) but **no** `$.getPath` / `$.params`; `ClientPlug` adds `$.getPath` but **never** `$.params` (client components get no route params); `ServerPlug` adds `$.getPath`, plus `$.params` only when resolved through the params overload. `DirectPlug` has **only** `$.locale` (a readonly echo of the locale passed to `useR`) and `$.kit` (when `directKit` is declared) — **no** `$.setLocale` (nothing to mutate — there is no bound container), `$.getPath`, or `$.params`. For the active locale prefer canonical `$.locale` over the raw `$.params.locale` (see §11.5). ### 10.6. Error handling: when a factory throws Resolution fails when a factory throws synchronously, an `async` factory's promise rejects, or a dependency fails. The consumer observes **the original error the factory threw** — unwrapped, keeping its identity/prototype, so `instanceof` and framework signals (`notFound()` / `redirect()`) still work. (R-Machine's own *structural* failures — invalid module shape, missing/circular dependency — surface as `RMachineResolveError`, code `ERR_RESOLVE_FAILED`.) The failure also emits a `blueprint:resolveError` / `res:resolveError` event (§15.2). Per variant: on **`Plug` / `ClientPlug`** (React) a *pending* resolution **suspends** to the nearest Suspense boundary (`` installs a default at the app root; override via `Suspense`/`Suspense={null}`), and a *failed* one **re-throws during render** to the nearest **React Error Boundary** — R-Machine ships none, so wrap subtrees in your own (e.g. `react-error-boundary`). On **`ServerPlug`** (RSC), `await plug.useR(params)` **rejects** with the same error: use `try/catch` or let it bubble to `error.tsx` (`loading.tsx` covers pending; `notFound()` propagates to `not-found.tsx`). Other contracts: - **Attribution.** The unwrapped error carries a **non-enumerable** resolution context (invisible to logging / `JSON.stringify`), read with `getResolveContext(error)` (from `r-machine`) → `{ namespace, locale, chain }`, pinning the **deepest** failing namespace. Returns `undefined` for anything not from a resolution failure. - **Retry.** A failed resolution is **not cached** — the slot is evicted, so an Error Boundary reset, Suspense retry, or next request re-runs the factory. - **Partial side effects.** Teardown (§9) runs only for a factory that returned successfully; one that acquires a side effect and *then* throws **leaks** it — `try/catch` and release before re-throwing. - **Testing.** Point an overridden port at a rejecting stub with `mockPlug` (§14) to exercise the consumer's error path. --- ## 11. Framework strategies ### 11.1. `ReactStandardStrategy` (web React) ```ts import { ReactStandardStrategy } from "@r-machine/react"; export const strategy = ReactStandardStrategy.create(rMachine, { kit: { fmt: "shell/lib/fmt" }, localeDetector: () => rMachine.localeHelper.matchLocales(navigator.languages), localeStore: { get: () => localStorage.getItem("locale") ?? undefined, set: (n) => localStorage.setItem("locale", n) }, }); export const { localeHelper } = strategy.getHelpers(); export const { ReactRMachine, Plug, VertexFrame } = await strategy.createToolset(); ``` If the layout declares any `gear:inner`, calling `ReactStandardStrategy.create` raises an `RMachineTypeError` listing the offending namespaces. `strategy.getHelpers()` exposes strategy-level utilities outside the plug consumer context (§11.3). Bootstrap the app by wrapping the tree in `}>…`. ### 11.2. Next.js strategies Three routing models, same shape: - `NextAppPathStrategy` — locale in URL path (`/en/…`, `/it/…`) - `NextAppFlatStrategy` — cookie/header, locale absent from the URL - `NextAppOriginStrategy` — subdomain or origin per locale All three are created with `strategy.create(rMachine, { … })` and accept a common base of options (`clientKit` / `serverKit`, `PathAtlas`, `cookie`, `implicitDefaultLocale`, `autoLocaleBinding`, `basePath`, `localeLabel`) plus a few strategy-specific ones — full reference below. `NextAppFlatStrategy` requires a `cookie` declaration (locale lives nowhere else); `NextAppOriginStrategy` requires `localeOriginMap: { en: "example.com", it: "example.it" }`; `PathAtlas` is optional in all three. #### Configuration options (reference) Every option is optional unless marked **required**; omitting one uses the listed default. **Common to all three Next.js strategies:** | Option | Type / values | Default | Effect | |---|---|---|---| | `clientKit` / `serverKit` | `{ name: namespace }` | `{}` | Consumer kit injected as `$.kit.{name}`, split per runtime (§11.4). | | `PathAtlas` | `PathAtlas` class | empty | Localized URL paths (§3.5 / §11.5). | | `localeKey` | `string` | `"locale"` | Name of the `[locale]` folder/segment and its `$.params` key. | | `autoLocaleBinding` | `"on" \| "off"` | `"off"` | `"on"`: server plugs resolve locale from a request header (zero-arg `useR()`), at the cost of dynamic rendering. Requires the proxy. See note below. | | `basePath` | `string` | `""` | Prefix prepended to every composed URL; match your Next.js `basePath`. | | `reactCompiler` | `"on" \| "off"` | `"off"` | React Compiler coexistence (§10.4); not needed for R-Machine code, leave `"off"` unless the compiler is global in a mixed codebase. | **`NextAppPathStrategy`** (locale in the URL path) adds: | Option | Type / values | Default | Effect | |---|---|---|---| | `cookie` | `"on" \| "off" \| CookieDeclaration` | `"off"` | Persist locale in a cookie (default `rm-locale`, 30-day); required when `implicitDefaultLocale` is on. | | `localeLabel` | `"strict" \| "lowercase"` | `"lowercase"` | URL case for `"it-IT"`: `lowercase`→`/it-it/`, `strict`→`/it-IT/`. Matched case-insensitively; `$.locale` is always canonical, `$.params.locale` is this raw form. | | `autoDetectLocale` | `"on" \| "off" \| { pathMatcher }` | `"on"` | Proxy reads locale from the URL segment and binds it (`{ pathMatcher }` restricts paths). Requires the proxy. | | `implicitDefaultLocale` | `"on" \| "off" \| { pathMatcher }` | `"off"` | `"on"`: default-locale URLs omit the prefix (`/page` not `/en/page`). Requires `cookie` + the proxy. | **Flat / Origin / React additions:** | Strategy | Option | Default | Effect | |---|---|---|---| | `NextAppFlatStrategy` | `cookie: CookieDeclaration` (**required**) | `{ name: "rm-locale", maxAge: 30d, path: "/" }` | The only place the locale lives (no `"off"`). | | `NextAppFlatStrategy` | `pathMatcher: RegExp \| null` | excludes `/_next`, `/_vercel`, `/api`, files | Request paths the locale middleware handles; `null` = all. | | `NextAppOriginStrategy` | `localeOriginMap: { [locale]: string \| string[] }` (**required**) | — | Maps each locale to its origin(s) for absolute URLs via `hrefHelper.getUrl` (§11.3); array → first used. | | `NextAppOriginStrategy` | `pathMatcher` | same as Flat | Same as Flat. | | `ReactStandardStrategy` | `kit: { name: namespace }` | `{}` | Consumer kit (§11.4) — single map, one runtime. | | `ReactStandardStrategy` | `localeDetector: () => Locale \| Promise` | none | Picks the initial locale (e.g. `matchLocales(navigator.languages)`); falls back to `defaultLocale`. | | `ReactStandardStrategy` | `localeStore: { get; set }` (sync/async) | none | Reads the persisted locale on boot, persists on change; `set` is what `$.setLocale` invokes. | | `ReactStandardStrategy` | `reactCompiler` | `"off"` | Same as the common `reactCompiler` (§10.4). | Constraints (Next.js): `implicitDefaultLocale` requires `cookie` + the proxy (§11.7); `autoDetectLocale` requires the proxy; `localeOriginMap` is required by `NextAppOriginStrategy`, `cookie` by `NextAppFlatStrategy`. ##### `autoLocaleBinding` — what it changes in your components This is the one option that visibly changes consumer code. It controls **how a Server Component obtains the request locale** — not what `useR()` returns. With the default `"off"`, every page/layout establishes the locale itself via the params overload (or once with `bindLocale`); with `"on"`, the locale is read from a proxy-set request header so **any** `ServerPlug` resolves with **no argument** and you never call `bindLocale`: ```tsx // "off" (default): bind explicitly | // "on": auto from request header async function Page({ params }) { // async function Page() { const [page] = await plug.useR(params); // const [page] = await plug.useR(); } // } ``` The trade-off: reading the header opts the route into **dynamic rendering** — Next.js can no longer SSG those routes (response computed per request). That cost is why it is opt-in; the default keeps routes statically optimizable at the price of explicit per-route binding. (`useR(params)` / `bindLocale` still work with `"on"`, just optional.) Toolset is split into client and server: ```ts // r-machine/client-toolset.ts "use client"; export const { NextClientRMachine, ClientPlug, VertexFrame, } = await strategy.createClientToolset(); // r-machine/server-toolset.ts export const { rMachineProxy, NextServerRMachine, generateLocaleStaticParams, bindLocale, setLocale, ServerPlug, } = await strategy.createServerToolset(NextClientRMachine); ``` The split kit (`clientKit` vs `serverKit`) is enforced at the type level — a server-only namespace listed in `clientKit` does not compile. ### 11.3. Strategy helpers (`strategy.getHelpers()`) Every strategy exposes `getHelpers()`, returning strategy-level utilities that need the locale config or path atlas but live **outside** the plug consumer context — `export const { localeHelper, hrefHelper } = strategy.getHelpers()` (alongside the strategy export in `setup.ts`, same for all three Next.js strategies). Shape per strategy: | Strategy | Returned helpers | |---|---| | `ReactStandardStrategy` | `{ localeHelper }` | | `NextAppPathStrategy` | `{ localeHelper, hrefHelper }` — `hrefHelper.getPath(locale, path, params?)` | | `NextAppFlatStrategy` | `{ localeHelper, hrefHelper }` — `hrefHelper.getPath(path, params?)` (no `locale` arg — flat strategy keeps locale in the cookie) | | `NextAppOriginStrategy` | `{ localeHelper, hrefHelper }` — `hrefHelper.getPath(locale, path, params?)` **and** `hrefHelper.getUrl(locale, path, params?)` | #### `localeHelper` — `LocaleHelper` Available on **every** strategy. ```ts localeHelper.locales // readonly LocaleList localeHelper.defaultLocale // L localeHelper.matchLocales(requested, algorithm?) // L — best match against the strategy's locales localeHelper.matchLocalesForAcceptLanguageHeader(header, …) // L — parses an Accept-Language header, then matches localeHelper.validateLocale(locale) // RMachineConfigError | null ``` The exposed instance is the same one held by `rMachine.localeHelper` (used to configure the strategy itself); re-exporting it via `getHelpers()` keeps the surface unified — consumers import everything from `setup.ts`. #### `hrefHelper` — Next.js only Same path-composition primitive as consumer-side `$.getPath` (§11.5), but with the locale supplied explicitly (or omitted, for flat). Use it where there is no request locale bound — non-localized layouts, root `generateMetadata`, static link generation at module scope. ```tsx // e.g. in a non-localized layout (no request locale): pass the locale explicitly const [common] = await plug.useR(localeHelper.defaultLocale); const homeUrl = hrefHelper.getPath(localeHelper.defaultLocale, "/"); ``` Shape varies by strategy: **Path** / **Origin** take locale as `getPath`'s first arg (the URL encodes it per call); **Flat** takes no locale arg (same path for every locale, cookie carries it); **Origin** additionally exposes `getUrl(locale, …)` returning a fully-qualified URL from `localeOriginMap`. Type narrowing is identical to `$.getPath` (§11.5). ### 11.4. Consumer-side kit (`kit` / `clientKit` / `serverKit`) Strategy options accept a kit declaration that is injected as `$.kit.{name}` into every plug consumer's `$` context. It is the consumer-side analogue of `gearKit` / `shellKit` (which inject into resource factories — see §3.3 and §8): same map shape (`{ name: namespace }`), same type-narrowed access, but resolved at the call site of `plug.useR()`. | Strategy | Option(s) | Plug | Sync/async | |---|---|---|---| | `ReactStandardStrategy` | `kit` | `Plug` | sync | | `NextAppPathStrategy` / `NextAppFlatStrategy` / `NextAppOriginStrategy` | `clientKit`, `serverKit` (independent) | `ClientPlug`, `ServerPlug` | sync / async | Next.js splits the kit into independent `clientKit` / `serverKit` (two runtimes) — `ReactStandardStrategy.create(rMachine, { kit: { fmt: "shell/lib/fmt" } })` vs `NextAppPathStrategy.create(rMachine, { clientKit: {…}, serverKit: {…} })`. Listing the same namespace in both is common for shells usable in either runtime; a server-only namespace (e.g. `gear:inner`) in `clientKit` is a compile error. Access is identical across strategies — destructure `$` from `plug.useR()` and read `$.kit.{name}` (only `ServerPlug.useR()` is async): ```tsx const [box, $] = plug.useR(); const time = $.kit.fmt.time(new Date()); // React/Client (sync) const [page, $] = await plug.useR(); const time = $.kit.fmt.time(new Date()); // ServerPlug (async) ``` The kit entry is a fully-resolved Surface of the underlying resource — same shape a peer factory receives via its own `$.kit` (locale-aware for shells, branded `Surface<…>` for gears). ### 11.5. Path composition (`PathAtlas` consumption) When `PathAtlas` is declared, the consumer context exposes `$.getPath` — a type-checked composer building locale-aware URLs from canonical path keys, identical on `ClientPlug` and `ServerPlug` (the latter async). Destructure `$` from `plug.useR()` (trailing tuple element / `$` key) and call it: ```tsx const [nav, $] = plug.useR(); // ClientPlug, sync {nav.home} {nav.exampleStatic.page1.label} const [example, $] = await plug.useR(params); // ServerPlug, async (params binds locale) {item.title} ``` Call shapes — `$.getPath("/example-static/page-1")` (static) and `$.getPath("/example-dynamic/[slug]", { slug: "abc" })` (dynamic): - First argument is the **canonical** path key (as in `PathAtlas` / the `app/` folder), narrowed to the union of declared paths — wrong keys / missing `/` / non-existent segments are compile errors. - A dynamic path **requires** a params object keyed by each `[name]` / `[...name]` / `[[...name]]` segment with the right type; static paths take no second argument. - Returns the localized URL for the active request locale with the strategy's prefix scheme applied (path prefix / none / origin), `basePath` prepended, `implicitDefaultLocale` honored. Query strings are not handled — append them yourself. `plug.useR(params)` binds the request locale from the route `params`, then resolves; binding happens on the first call per request and later `useR()` calls reuse it. (With `autoLocaleBinding: "on"` — §11.2 — you skip params binding and call `useR()` zero-arg everywhere, trading static rendering for convenience.) #### Reading route params: `$.params` The params-binding overloads (`plug.useR(params)` / `plug.useUnboundR(params)`) also surface the resolved route params on `$.params` — the awaited route `params` object exactly as Next.js produced it, so you read every segment without `await`-ing `params` again. It is the same `P` Next.js infers from `PageProps` / `LayoutProps` (`{ [localeKey]: string; …dynamicSegments }`), present **only** on a `ServerPlug` resolved through the params overload — not on zero-arg `useR()`, the explicit-locale overload, or any React `Plug` / `ClientPlug` (destructuring it where absent is a compile error). For the active locale prefer canonical `$.locale` (§10.5); `$.params.locale` is the raw URL segment (string), uncanonicalized. ```tsx const [example, $] = await plug.useR(params); // PageProps<"/[locale]/example-dynamic/[slug]"> $.locale; // canonical active locale (e.g. "it-IT") $.params.slug; // raw dynamic segment — string ``` #### Standalone binding: `bindLocale(...)` `bindLocale` binds the request locale **without** consuming a plug — for a page/layout that has no plug to call. After it runs, every subsequent zero-arg `plug.useR()` in the same request (including nested children) picks up the bound locale. Two overloads: ```ts bindLocale

>(params: Promise

): Promise

; // from route params bindLocale(locale: AnyLocale): Locale; // from explicit locale // e.g. in a layout: const { locale } = await bindLocale(params); → ``` The `params` overload returns the same `params` promise (locale canonicalized), awaitable inline. Invoke it **once per request** at the top of the page/layout (a second conflicting call throws `ERR_LOCALE_BIND_CONFLICT`). If the page itself needs a plug, prefer `plug.useR(params)` — it is exactly `bindLocale(params)` + `plug.useR()` in one call. #### Switching locale: `setLocale(...)` `setLocale(newLocale: Locale): Promise` is the standalone server primitive for **changing** the active locale and persisting it across requests (exported from every Next.js server toolset). The value is validated (invalid → `ERR_UNKNOWN_LOCALE`) and persisted via the strategy's mechanism — cookie for path/flat, origin redirect for origin. **Server-only**: call it from a Server Action or Route Handler `GET` (`await setLocale(next)`), never from a Server Component render path (cookie writes need an action/handler context). #### Consumer-side locale switching: `$.setLocale(...)` Every plug consumer also receives `$.setLocale(newLocale: Locale): Promise` — the in-consumer counterpart, for when the change starts inside a component (`