From d7fae05f6cb6a0944a0fd86fab980319d1daddaa Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sun, 19 Jul 2026 15:56:42 -0400 Subject: [PATCH] docs(spec): generic define() primitive, registerRuntime() rename --- .../specs/2026-07-19-runtime-design.md | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/docs/superpowers/specs/2026-07-19-runtime-design.md b/docs/superpowers/specs/2026-07-19-runtime-design.md index e76e8c2..52322c9 100644 --- a/docs/superpowers/specs/2026-07-19-runtime-design.md +++ b/docs/superpowers/specs/2026-07-19-runtime-design.md @@ -14,8 +14,12 @@ cooperatively across them. ## Summary of decisions -- One **global runtime** per process; defined as a module (like moroutines themselves), - registered from main, booted lazily on first dispatch, torn down automatically via unref. +- A generic **`define()`** primitive: values tagged with module identity that cross threads + by reference and resolve per-thread via module evaluation, like task-args; usable as + moroutine args. +- One **global runtime** per process; its definition is a `define()`'d config value, + registered from main via `registerRuntime()`, booted lazily on first dispatch, torn down + automatically via unref. - The **default runtime is just another runtime module**, shipped inside moroutine. - **Main is coordinator only**: it dispatches, consumes results, and brokers peer channels; tasks never run on main. @@ -30,17 +34,43 @@ cooperatively across them. ## 1. Concept & API +### `define()` — module-identified values + +A generic primitive, sibling of `mo()`: tags a value with deterministic module identity so +it can cross threads **by reference** and be resolved on the receiving thread by evaluating +its module — the same resolution model as task-args, but for plain values. + +```ts +// config.ts — side-effect free, like all moroutine modules +import { define } from 'moroutine'; + +export const limits = define(import.meta, { maxBatch: 64, retry: 3 }); +``` + +- Mechanism mirrors `mo()` exactly: module-scope-only creation, `${url}#${index}` IDs from + a per-module counter, registered in the module registry, guarded by the same + freeze-on-first-dispatch check that keeps IDs deterministic. +- Wire form mirrors task-args: where a task-arg travels as `{ __task__, id, args }` and is + resolved by execution, a defined value travels as `{ __define__, id }` and is resolved by + import — `prepareArg` detects it on the sending side; `resolveArg` imports the module and + reads the registry on the receiving side. +- **Supported as moroutine args**: passing a defined value to a task sends the reference, + not the value; the worker resolves it locally. Useful for config objects, lookup tables, + and anything whose identity is "what this module evaluates to." +- Note the semantics: the value is produced by *evaluating the module on each thread*, so + non-shared mutable state inside it is per-thread. Cross-thread state must use shared + memory (as with balancer `initialState()`). + ### Runtime definition -A runtime is defined in a side-effect-free module with deterministic identity, the same -pattern as `mo()`: +A runtime is just a `define()`'d config value: ```ts // app-runtime.ts -import { defineRuntime } from 'moroutine'; +import { define } from 'moroutine'; import { keyAffinity } from './balancer.ts'; -export default defineRuntime(import.meta, { +export default define(import.meta, { size: 8, balance: keyAffinity(), }); @@ -55,22 +85,22 @@ since shared active counts make it correct from every thread. ```ts // main.ts -import { register } from 'moroutine'; +import { registerRuntime } from 'moroutine'; import appRuntime from './app-runtime.ts'; -register(appRuntime); // must precede first dispatch +registerRuntime(appRuntime); // must precede first dispatch ``` - Boot is **lazy**: the first dispatch (bare await or `runtime.run()`) boots whichever runtime is registered — the default if none. -- `register()` after the runtime has booted **throws**. No silent reconfiguration. +- `registerRuntime()` after the runtime has booted **throws**. No silent reconfiguration. - Teardown is **automatic**: workers are `unref()`'d when idle and `ref()`'d while work is in flight (the pattern dedicated workers use today). The process exits naturally when main finishes and no tasks are pending. - **Graceful shutdown is opt-in** via `runtime.shutdown()` (signal → drain in-flight up to a timeout → terminate), for servers that need coordinated drain. Scripts never call it. -- `register()` is a plain call in main; runtime definition modules stay side-effect free - (every thread imports them). +- `registerRuntime()` is a plain call in main; runtime definition modules stay side-effect + free (every thread imports them). ### The ambient runtime handle @@ -115,7 +145,8 @@ direct lazily-created channels. ### Boot handshake -Each worker spawns with `workerData` carrying: the runtime module ID, its own worker index, +Each worker spawns with `workerData` carrying: the runtime definition's `define()` ID, its +own worker index, pool size, the runtime's shared state block (one `SharedArrayBuffer`: per-worker active counts), and the serialized balancer state (see §3). The worker imports the runtime module by ID, reconstructs config and balancer state, and binds the ambient @@ -174,10 +205,10 @@ One uniform `WorkerHandle` interface on every thread: `{ index, exec, activeCoun ## 3. Scheduling & balancers -Balancer code identity comes for free from the runtime module import: every thread evaluates -the same runtime module, hence the same `select` logic. No separate registration. Only -**state** needs a mechanism, because per-thread module evaluation would give each thread its -own closure state. +Balancer code identity comes for free from `define()` resolution: every thread evaluates +the runtime definition's module, hence the same `select` logic. No separate registration. +Only **state** needs a mechanism, because per-thread module evaluation would give each +thread its own closure state. ```ts // Balancer interface (breaking change: initialState + third select arg) @@ -261,14 +292,15 @@ Unchanged: ## 6. Testing -- **Unit:** broker (pair dedup, simultaneous requests, dead-peer error frames); peer table - promise states; handle reconstruction and runtime-mismatch errors; balancer - `initialState()` serialization/reconstruction; control-frame multiplexing alongside task - traffic. +- **Unit:** `define()` (ID determinism, resolution on another thread, as-arg wire form, + module-scope/freeze guards); broker (pair dedup, simultaneous requests, dead-peer error + frames); peer table promise states; handle reconstruction and runtime-mismatch errors; + balancer `initialState()` serialization/reconstruction; control-frame multiplexing + alongside task traffic. - **Integration:** nested dispatch worker→worker and worker→self (shortcut equivalence, including arg-deserialization semantics); pinning from workers; recursive fork-join at pool size 1 completes (liveness invariant); unref-based natural process exit; - `register()` after boot throws; two runtimes' + `registerRuntime()` after boot throws; two runtimes' handles cross-used throws; `workers()`-only programs never boot the runtime. - **Perf gates:** dispatch overhead unchanged in `workers()` mode; same-worker shortcut comparable to a local call; first-contact latency benchmarked; shared-counter dispatch @@ -280,7 +312,8 @@ Unchanged: |---|---|---| | Nested moroutine calls | Dispatch to global pool, policy-routable, same-worker optimized | Full peer model; fork-join is safe given async-parking invariant | | Main thread role | Coordinator only | Keeps event-loop-owning thread responsive; simplest topology | -| Pool creation | Registered from main, lazy boot, unref auto-teardown | Zero ceremony; no silent reconfiguration (`register` after boot throws) | +| Pool creation | Registered from main, lazy boot, unref auto-teardown | Zero ceremony; no silent reconfiguration (`registerRuntime` after boot throws) | +| Runtime definition | Generic `define()` value, not a bespoke wrapper | One resolution model (module identity) shared with task-args; defined values usable as args generally | | Default runtime | Internal runtime module in moroutine | "Just another runtime"; no special-case code path | | Config consistency | Runtime module identity + serialized `initialState()` | Same determinism trick as `mo()`; state solved separately from code | | Worker addressing | Indexed handles, uniform on all threads | Minimal new concepts; index is the portable truth | -- 2.51.2