diff --git a/.changeset/runtime-foundations.md b/.changeset/runtime-foundations.md index 423b798..f76582c 100644 --- a/.changeset/runtime-foundations.md +++ b/.changeset/runtime-foundations.md @@ -19,5 +19,6 @@ Runtime foundations: `runtime.shutdown()`. Task-arg caches are now per runtime worker (previously per dedicated worker); streaming pipelines get balancer placement instead of one thread per stage; an undispatched streaming task used as a task arg or `channel()` - source is a bare dispatch and runs on the global runtime. + source is a bare dispatch and runs on the global runtime. Bare dispatch from inside a + worker thread currently throws (worker-side runtime participation is planned). - **Breaking:** dispatching a task pinned to a handle from a different pool throws. diff --git a/README.md b/README.md index 1600545..52409e7 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ const same = await add(3, 4); // bare await — same runtime dispatches (booting if needed), `runtime.workers` exposes uniform worker handles for pinning with `assign()`, and `runtime.signal` fires when shutdown begins. `registerRuntime()` after the runtime has booted throws — no silent reconfiguration. +Bare dispatch and `runtime.run()` currently work from the main thread only — a bare +`await task` inside worker code throws until worker-side runtime participation lands. Long-running apps (servers) can drain gracefully: diff --git a/docs/superpowers/specs/2026-07-19-runtime-design.md b/docs/superpowers/specs/2026-07-19-runtime-design.md index 1664517..d80bfae 100644 --- a/docs/superpowers/specs/2026-07-19-runtime-design.md +++ b/docs/superpowers/specs/2026-07-19-runtime-design.md @@ -121,9 +121,9 @@ export const processItem = mo(import.meta, async (item: Item) => { ``` `runtime` is a lazy view of "the global runtime this thread belongs to": on main, the -registered (or default) runtime; on a worker, bound by the boot handshake. It is a `Runner` -(same shape `workers()` returns): `runtime.run(task | task[])`, `runtime.workers`, -`runtime.signal`. +registered (or default) runtime; on a worker, bound by the boot handshake. It exposes +`runtime.run(task | tasks)`, `runtime.workers`, `runtime.signal`, and `runtime.shutdown()` +— a handle with a run method, not itself callable. ### Bare await @@ -223,14 +223,15 @@ interface Balancer { ``` ```ts -export function keyAffinity(): Balancer<{ cursor: Uint32Atomic }> { +export function keyAffinity(): Balancer { + const fallback = roundRobin(); return { - initialState: () => ({ cursor: uint32atomic() }), - select(workers, task, { cursor }) { + initialState: () => fallback.initialState(), + select(workers, task, fallbackState) { let key: string | undefined; if (isTask(increment, task)) key = task.args[0]; else if (isTask(read, task)) key = task.args[0]; - if (key === undefined) return workers[cursor.add(1) % workers.length]; + if (key === undefined) return fallback.select(workers, task, fallbackState); return workers[hash(key) % workers.length]; }, }; diff --git a/src/runtime.ts b/src/runtime.ts index 0710e9b..494d7d5 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -22,15 +22,15 @@ let shutdownPromise: Promise | null = null; * behavior change, so this throws instead. */ export function registerRuntime(definition: RuntimeDefinition): void { + if (shutdownStarted) { + throw new Error('Cannot registerRuntime(): the global runtime has been shut down.'); + } if (pool !== null) { throw new Error( 'Cannot registerRuntime(): the global runtime has already booted. ' + 'Register before the first dispatch (bare `await task` or runtime.run()).', ); } - if (shutdownStarted) { - throw new Error('Cannot registerRuntime(): the global runtime has been shut down.'); - } if (registered !== null) { throw new Error('Cannot registerRuntime(): a runtime definition is already registered.'); }