diff --git a/src/runner.ts b/src/runner.ts index 07fbfbf..3dc600f 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -17,7 +17,8 @@ export type Task = { /** Resolves the dispatch return type: `AsyncIterable` for streaming tasks, `Promise` otherwise. */ export type RunResult = T extends AsyncIterable ? AsyncIterable : Promise; -type TaskResults[]> = { +/** Maps a tuple of tasks to a tuple of their result types. @internal */ +export type TaskResults[]> = { [K in keyof T]: T[K] extends Task ? R : never; }; diff --git a/src/runtime.ts b/src/runtime.ts index 1a5c771..1589c0d 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -2,16 +2,15 @@ import { workers } from './worker-pool.ts'; import { getDefineId } from './define.ts'; import { defaultRuntime } from './runtime-definition.ts'; import type { RuntimeDefinition } from './runtime-definition.ts'; -import type { Task, RunResult, Runner, WorkerHandle } from './runner.ts'; +import type { Task, TaskResults, RunResult, Runner, WorkerHandle } from './runner.ts'; import type { ChannelOptions } from './channel.ts'; -type TaskResults[]> = { - [K in keyof T]: T[K] extends Task ? R : never; -}; - +// Process-wide singleton state, one runtime per process lifetime — no reset +// hook by design; fresh-state tests use child processes. let registered: RuntimeDefinition | null = null; let pool: Runner | null = null; let shutdownStarted = false; +let shutdownPromise: Promise | null = null; /** * Registers a custom runtime definition as the process's global runtime. @@ -45,7 +44,7 @@ export function registerRuntime(definition: RuntimeDefinition): void { /** Boots (if needed) and returns the global runtime pool. @internal */ export function getRuntime(): Runner { if (shutdownStarted) { - throw new Error('The global runtime has been shut down'); + throw new Error('The global runtime has been shut down.'); } if (pool === null) { const def = registered ?? defaultRuntime; @@ -67,11 +66,11 @@ export function getRuntime(): Runner { /** Returns the booted pool or throws — for surface that must not boot. */ function bootedRuntime(): Runner { - if (pool === null) { - throw new Error('The global runtime is not booted (it boots on first dispatch)'); - } if (shutdownStarted) { - throw new Error('The global runtime has been shut down'); + throw new Error('The global runtime has been shut down.'); + } + if (pool === null) { + throw new Error('The global runtime is not booted (it boots on first dispatch).'); } return pool; } @@ -110,12 +109,10 @@ export const runtime = { * re-booted (one runtime per process lifetime). */ async shutdown(): Promise { - if (pool === null) { - shutdownStarted = true; // never booted: nothing to drain, lock the door - return; - } shutdownStarted = true; - await pool[Symbol.asyncDispose](); + if (pool === null) return; // never booted: nothing to drain, lock the door + shutdownPromise ??= pool[Symbol.asyncDispose](); + await shutdownPromise; }, }; diff --git a/test/runtime.test.ts b/test/runtime.test.ts index 733b227..db1d4a0 100644 --- a/test/runtime.test.ts +++ b/test/runtime.test.ts @@ -10,12 +10,19 @@ import { double, add } from './fixtures/math.ts'; // (runtime-lifecycle tests). describe('global runtime', () => { it('property access does not boot the runtime', () => { - // runtime.workers before boot must throw rather than boot as a side effect + // runtime.workers/signal before boot must throw rather than boot as a side effect assert.throws(() => runtime.workers, { message: /not booted/ }); + assert.throws(() => runtime.signal, { message: /not booted/ }); + }); + + it('registerRuntime() rejects a non-define()-branded definition', () => { + assert.throws(() => registerRuntime({ size: 1 }), { message: /define\(\)-branded/ }); }); it('registerRuntime() before boot succeeds and wins over the default', async () => { registerRuntime(tinyRuntime); + // registering a second definition throws, even before boot + assert.throws(() => registerRuntime(tinyRuntime), { message: /already registered/ }); const result = await runtime.run(double(21)); assert.equal(result, 42); assert.equal(runtime.workers.length, 1); // tinyRuntime.size, not availableParallelism