@absolutejs/runtime 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,88 @@
1
+ # Business Source License 1.1
2
+
3
+ **Licensor:** Alex Kahn
4
+
5
+ **Licensed Work:** @absolutejs/runtime (https://github.com/absolutejs/runtime)
6
+
7
+ **Change Date:** May 29, 2030
8
+
9
+ **Change License:** Apache License, Version 2.0
10
+
11
+ ---
12
+
13
+ ## Terms
14
+
15
+ The Licensor hereby grants you the right to copy, modify, create derivative
16
+ works, redistribute, and make non-production use of the Licensed Work. The
17
+ Licensor may make an Additional Use Grant, permitting limited production use.
18
+
19
+ ### Additional Use Grant
20
+
21
+ You may use the Licensed Work in production, provided your use does not include
22
+ any of the following:
23
+
24
+ 1. **Offering a Competing Service.** You may not offer the Licensed Work, or
25
+ any derivative or substantial portion of it, to third parties as a hosted or
26
+ managed service that competes with a hosted Bun runtime, multi-tenant Bun application platform, or Bun-based platform-as-a-service (including, but not limited to, services like Vercel, Render, Fly.io, Railway, Bun's own hosted offerings, or any similar deployment platform for Bun applications). This includes any
27
+ product whose primary value to its users is the functionality the Licensed
28
+ Work provides.
29
+
30
+ 2. **Resale or Redistribution as a Standalone Product.** You may not sell,
31
+ license, or distribute the Licensed Work, or any derivative or fork of it,
32
+ as a standalone commercial product.
33
+
34
+ 3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
35
+ the Licensed Work must prominently credit AbsoluteJS and include a link to
36
+ the original project repository (https://github.com/absolutejs/runtime).
37
+
38
+ For clarity, the following uses are expressly permitted:
39
+
40
+ - Using the Licensed Work to build and operate your own applications, websites,
41
+ internal tools, or SaaS products (whether commercial or non-commercial), so
42
+ long as the Licensed Work itself is not the primary product you are selling.
43
+ - Using the Licensed Work as a dependency in commercial software you build and
44
+ sell, as long as the software is not itself a competing managed service of
45
+ the kind described in clause 1.
46
+ - Providing consulting, development, or professional services to clients using
47
+ the Licensed Work.
48
+ - Forking and modifying the Licensed Work for your own internal use, provided
49
+ attribution is maintained.
50
+
51
+ ### Change Date and Change License
52
+
53
+ On the Change Date specified above, or on such other date as the Licensor may
54
+ specify by written notice, the Licensed Work will be made available under the
55
+ Change License (Apache License, Version 2.0). Until the Change Date, the terms
56
+ of this Business Source License 1.1 apply.
57
+
58
+ ### Trademark
59
+
60
+ This license does not grant you any rights to use the "AbsoluteJS" or
61
+ "@absolutejs" name, logo, or any related trademarks. Forks and derivative works
62
+ must not be named or branded in a manner that suggests endorsement by or
63
+ affiliation with AbsoluteJS or the Licensor.
64
+
65
+ ### Notices
66
+
67
+ You must not remove or obscure any licensing, copyright, or other notices
68
+ included in the Licensed Work.
69
+
70
+ ### No Warranty
71
+
72
+ THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
73
+ WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
74
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
75
+ EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
76
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
77
+ IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
78
+ LICENSED WORK.
79
+
80
+ ---
81
+
82
+ ## Contact
83
+
84
+ For commercial licensing inquiries or additional permissions, contact:
85
+
86
+ - **Alex Kahn**
87
+ - alexkahndev@gmail.com
88
+ - alexkahndev.github.io
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @absolutejs/runtime
2
+
3
+ Multi-tenant Bun runtime substrate. Wraps `Bun.spawn` so that "run this tenant's `bun run start` inside an idle-killing, metric-emitting child process" is one function call.
4
+
5
+ Built for PaaS providers and platform layers that want to host many small Bun apps under one host process. The library SB-6 surfaces between `@absolutejs/isolated-jsc` + `@absolutejs/sync` and the hosted product downstream.
6
+
7
+ ```ts
8
+ import { createRuntime } from '@absolutejs/runtime';
9
+
10
+ const runtime = createRuntime({
11
+ source: { kind: 'directory', root: '/srv/tenants' },
12
+ idleAfterMs: 5 * 60 * 1000,
13
+ maxConcurrent: 100,
14
+ onMetrics: (event) => prometheus.observe(event),
15
+ onLog: (event) => loki.write(event),
16
+ });
17
+
18
+ // First call: spawns `bun run start` in /srv/tenants/tenant-42,
19
+ // injects PORT, waits for readiness, returns the bound port.
20
+ const tenant = await runtime.ensure('tenant-42');
21
+ await fetch(`http://127.0.0.1:${tenant.port}/`);
22
+
23
+ // Subsequent calls reuse the running process.
24
+ runtime.touch('tenant-42'); // bump idle clock
25
+
26
+ runtime.stats(); // { running, total }
27
+ await runtime.dispose();
28
+ ```
29
+
30
+ ## v0.0.1 surface
31
+
32
+ | API | Purpose |
33
+ |---|---|
34
+ | `createRuntime(options)` | Factory. Returns a `Runtime`. |
35
+ | `runtime.ensure(key)` | Spawn-or-reuse. Single-flight on concurrent calls to the same key. Returns `{ key, port, pid, startedAt, lastTouchedAt }`. |
36
+ | `runtime.touch(key)` | Bump the idle clock for an active tenant. Cheap; call before/after each request. |
37
+ | `runtime.stats()` | `{ running, total }` snapshot. |
38
+ | `runtime.kill(key)` | Force-kill. No-op if not running. |
39
+ | `runtime.dispose()` | Kill all + stop the sweeper. Idempotent. |
40
+
41
+ ### Hibernation strategy (v0.0.1)
42
+
43
+ **Idle-kill at the process layer**, plus the JSC-context hibernation any tenant gets for free via `@absolutejs/isolated-jsc`'s `createHibernatingIsolatePool`. Bun has no process-level snapshot/resume primitive shipped or tracked in an open issue as of 2026-05-29; when one lands we'll add an opt-in `hibernate: 'process-snapshot'` mode and keep idle-kill as the default.
44
+
45
+ The trade-off the default makes explicit: first call after idle pays a full Bun cold spawn (~50–200ms). That's worth it for the free-tier multi-tenant economics; if the wake latency matters for your workload, set `idleAfterMs: 0` and rely on `maxConcurrent`'s LRU eviction instead.
46
+
47
+ ### Observability hooks
48
+
49
+ `onLog`, `onMetrics`, and `onTransition` are pluggable. `onLog` receives newline-split stdout/stderr from every child; `onMetrics` fires on spawn with `durationMs`; `onTransition` fires on every state change (`spawn` / `ready` / `idle-kill` / `lru-evict` / `exit`).
50
+
51
+ ### Pluggable spawn + readiness
52
+
53
+ `spawn` and `readiness` are overrides on `createRuntime`. The default `spawn` runs `['bun', 'run', 'start']` with `PORT` injected; the default `readiness` polls `http://127.0.0.1:${port}/` every 100ms with a 30s deadline. Tests use the spawn override to bypass disk; production use is mostly the defaults.
54
+
55
+ ## Architectural role
56
+
57
+ - **`@absolutejs/isolated-jsc`** — heap-isolated *JS contexts*. One library down: the unit the runtime hibernates is a child process, not a JSC context.
58
+ - **`@absolutejs/sync`** — reactive engine. Independent of the runtime; consumed inside tenant processes when the tenant uses it.
59
+ - **`@absolutejs/runtime`** — *this library*. The process-pool layer.
60
+ - **`@absolutejs/metering`** (planned) — consumes the runtime's metrics + handlerMetrics from sync into a cost-attribution → billing-events pipeline.
61
+ - **`@absolutejs/router`** (planned) — multi-tenant routing in front of one or more runtime instances.
62
+
63
+ ## License
64
+
65
+ CC BY-NC 4.0 — same as the rest of the AbsoluteJS ecosystem.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * `@absolutejs/runtime` — multi-tenant Bun runtime substrate.
3
+ *
4
+ * Wraps Bun's `spawn` so that "run this tenant's `bun run start` inside
5
+ * a hibernating, metric-emitting child process" is one function call.
6
+ * Built for PaaS providers that want to host many small Bun apps under
7
+ * one host process.
8
+ *
9
+ * Architectural role: SB-6's `@absolutejs/runtime` library. Consumers
10
+ * include the hosted `absolutejs.ai` PaaS (eventual) and anyone else
11
+ * who needs the same shape. Stays decoupled from `@absolutejs/sync`
12
+ * and `@absolutejs/isolated-jsc` — those libraries solve different
13
+ * layers of the same stack.
14
+ *
15
+ * v0.0.1 hibernation strategy (per the design doc, STRATEGY-CLOUD.md
16
+ * §9.5): idle-kill at the process layer. Bun has no shipped
17
+ * process-level snapshot/resume primitive as of 2026-05-29, and no
18
+ * open issue tracking one. When that primitive lands we'll add an
19
+ * opt-in `hibernate: 'process-snapshot'` mode and keep idle-kill as
20
+ * the default.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { createRuntime } from '@absolutejs/runtime';
25
+ *
26
+ * const runtime = createRuntime({
27
+ * source: { kind: 'directory', root: '/srv/tenants' },
28
+ * idleAfterMs: 5 * 60 * 1000, // 5 min
29
+ * maxConcurrent: 100,
30
+ * onMetrics: (event) => prometheus.observe(event),
31
+ * onLog: (event) => loki.write(event),
32
+ * });
33
+ *
34
+ * // First call: spawns `bun run start` in /srv/tenants/tenant-42,
35
+ * // injects PORT, waits for readiness, returns the bound port.
36
+ * const tenant = await runtime.ensure('tenant-42');
37
+ * fetch(`http://localhost:${tenant.port}/`);
38
+ *
39
+ * // Subsequent calls reuse the running process.
40
+ * runtime.touch('tenant-42'); // bump idle clock
41
+ *
42
+ * runtime.stats(); // { running, total }
43
+ * await runtime.dispose();
44
+ * ```
45
+ */
46
+ import type { Subprocess } from "bun";
47
+ export type TenantSource = {
48
+ kind: "directory";
49
+ root: string;
50
+ };
51
+ /** Identity of a single tenant process at a point in time. */
52
+ export type Tenant = {
53
+ /** The key the consumer used to address this tenant. */
54
+ key: string;
55
+ /** The port the child process bound to, discovered after readiness. */
56
+ port: number;
57
+ /** OS process id. */
58
+ pid: number;
59
+ /** Wall-clock when the child was spawned. */
60
+ startedAt: number;
61
+ /** Last time the consumer marked this tenant active. */
62
+ lastTouchedAt: number;
63
+ };
64
+ export type RuntimeMetricEvent = {
65
+ type: "spawn";
66
+ key: string;
67
+ pid: number;
68
+ port: number;
69
+ durationMs: number;
70
+ };
71
+ export type RuntimeLogEvent = {
72
+ key: string;
73
+ pid: number;
74
+ stream: "stdout" | "stderr";
75
+ /** A single line of output (newline-terminated lines are split client-side). */
76
+ line: string;
77
+ at: number;
78
+ };
79
+ export type RuntimeTransitionEvent = {
80
+ type: "spawn";
81
+ key: string;
82
+ pid: number;
83
+ port: number;
84
+ } | {
85
+ type: "ready";
86
+ key: string;
87
+ pid: number;
88
+ port: number;
89
+ durationMs: number;
90
+ } | {
91
+ type: "idle-kill";
92
+ key: string;
93
+ pid: number;
94
+ reason: "idle-threshold";
95
+ idleMs: number;
96
+ } | {
97
+ type: "lru-evict";
98
+ key: string;
99
+ pid: number;
100
+ reason: "max-concurrent";
101
+ } | {
102
+ type: "exit";
103
+ key: string;
104
+ pid: number;
105
+ exitCode: number | null;
106
+ };
107
+ export type ReadinessCheck = (args: {
108
+ key: string;
109
+ port: number;
110
+ /** Wall-clock spawn time so the check can compute its own elapsed. */
111
+ startedAt: number;
112
+ }) => Promise<boolean>;
113
+ export type SpawnFn = (args: {
114
+ cwd: string;
115
+ env: Record<string, string>;
116
+ onLogLine: (event: RuntimeLogEvent) => void;
117
+ key: string;
118
+ }) => Promise<Subprocess>;
119
+ /** Options for {@link createRuntime}. */
120
+ export type RuntimeOptions = {
121
+ /** Where to find tenant project directories. */
122
+ source: TenantSource;
123
+ /**
124
+ * Kill the child process after this many ms with no `touch()` call.
125
+ * Default 5 minutes. Set to `0` to disable idle-kill (only LRU and
126
+ * explicit `kill()` shed processes then).
127
+ */
128
+ idleAfterMs?: number;
129
+ /**
130
+ * Max concurrent tenant processes. When a fresh `ensure()` would
131
+ * push past this, the least-recently-touched process is killed
132
+ * first. Default 100.
133
+ */
134
+ maxConcurrent?: number;
135
+ /**
136
+ * Background sweep interval. Default 10_000 ms. The sweep runs only
137
+ * when the runtime is non-empty and is unrefed so the process can
138
+ * exit cleanly.
139
+ */
140
+ sweepIntervalMs?: number;
141
+ /**
142
+ * Override the readiness check. Default: HTTP GET to
143
+ * `http://localhost:${port}/` with a 100ms retry loop, give up after
144
+ * 30s with a `Tenant readiness timed out` error.
145
+ */
146
+ readiness?: ReadinessCheck;
147
+ /**
148
+ * Override how a child process is spawned. Default: `Bun.spawn` with
149
+ * `['bun', 'run', 'start']`, stdio piped through `onLogLine`, env
150
+ * carrying `PORT=${allocatedPort}` and `NODE_ENV=production`. Tests
151
+ * use this to inject a fixture without writing to disk.
152
+ */
153
+ spawn?: SpawnFn;
154
+ /** Operational metrics — spawn/ready durations etc. */
155
+ onMetrics?: (event: RuntimeMetricEvent) => void;
156
+ /** stdout/stderr stream. Bounded internally; backpressure to the host. */
157
+ onLog?: (event: RuntimeLogEvent) => void;
158
+ /** Lifecycle events — spawn/ready/idle-kill/lru-evict/exit. */
159
+ onTransition?: (event: RuntimeTransitionEvent) => void;
160
+ /**
161
+ * Command to run when spawning. Default `['bun', 'run', 'start']`.
162
+ * Tests use this to point at a fixture script.
163
+ */
164
+ command?: readonly string[];
165
+ };
166
+ export type RuntimeStats = {
167
+ running: number;
168
+ total: number;
169
+ };
170
+ export type Runtime = {
171
+ /**
172
+ * Resolve `key` to a running tenant. Spawns if not running, waits
173
+ * for readiness, returns the live {@link Tenant} including the bound
174
+ * `port`. Concurrent calls to the same key share a single-flight
175
+ * spawn — N callers don't create N processes.
176
+ */
177
+ ensure: (key: string) => Promise<Tenant>;
178
+ /**
179
+ * Mark `key` as active right now. Bumps the idle clock; the next
180
+ * sweep won't consider it for idle-kill until `idleAfterMs` again.
181
+ * Cheap; safe to call before/after each request you route to this
182
+ * tenant.
183
+ */
184
+ touch: (key: string) => void;
185
+ /** Synchronous snapshot. */
186
+ stats: () => RuntimeStats;
187
+ /** Force-kill `key`. No-op if not running. */
188
+ kill: (key: string) => Promise<void>;
189
+ /** Dispose every running child + stop the sweep. Idempotent. */
190
+ dispose: () => Promise<void>;
191
+ };
192
+ export declare const createRuntime: (options: RuntimeOptions) => Runtime;
package/dist/index.js ADDED
@@ -0,0 +1,330 @@
1
+ // @bun
2
+ // src/index.ts
3
+ var defaultReadiness = async ({ port, startedAt }) => {
4
+ const deadline = startedAt + 30000;
5
+ while (Date.now() < deadline) {
6
+ try {
7
+ const res = await fetch(`http://127.0.0.1:${port}/`, {
8
+ signal: AbortSignal.timeout(2000)
9
+ });
10
+ return true;
11
+ } catch {
12
+ await new Promise((resolve) => setTimeout(resolve, 100));
13
+ }
14
+ }
15
+ throw new Error("Tenant readiness timed out after 30s");
16
+ };
17
+ var allocateEphemeralPort = () => {
18
+ const server = Bun.listen({
19
+ hostname: "127.0.0.1",
20
+ port: 0,
21
+ socket: {
22
+ data: () => {},
23
+ open: () => {}
24
+ }
25
+ });
26
+ const port = server.port;
27
+ server.stop(true);
28
+ return port;
29
+ };
30
+ var splitLines = (() => {
31
+ const remainders = new Map;
32
+ return (key, chunk) => {
33
+ const prior = remainders.get(key) ?? "";
34
+ const combined = prior + chunk;
35
+ const parts = combined.split(`
36
+ `);
37
+ remainders.set(key, parts.pop() ?? "");
38
+ return parts;
39
+ };
40
+ })();
41
+ var defaultSpawn = async ({
42
+ cwd,
43
+ env,
44
+ onLogLine,
45
+ key
46
+ }) => {
47
+ const child = Bun.spawn({
48
+ cmd: ["bun", "run", "start"],
49
+ cwd,
50
+ env,
51
+ stderr: "pipe",
52
+ stdout: "pipe"
53
+ });
54
+ const readStream = (stream, label) => {
55
+ if (stream === undefined || stream === null)
56
+ return;
57
+ (async () => {
58
+ const reader = stream.getReader();
59
+ const decoder = new TextDecoder;
60
+ const splitKey = `${child.pid}:${label}`;
61
+ try {
62
+ while (true) {
63
+ const { value, done } = await reader.read();
64
+ if (done)
65
+ break;
66
+ const text = decoder.decode(value, { stream: true });
67
+ for (const line of splitLines(splitKey, text)) {
68
+ onLogLine({
69
+ at: Date.now(),
70
+ key,
71
+ line,
72
+ pid: child.pid,
73
+ stream: label
74
+ });
75
+ }
76
+ }
77
+ } catch {}
78
+ })();
79
+ };
80
+ readStream(child.stdout, "stdout");
81
+ readStream(child.stderr, "stderr");
82
+ return child;
83
+ };
84
+ var createRuntime = (options) => {
85
+ const source = options.source;
86
+ const idleAfterMs = options.idleAfterMs ?? 5 * 60 * 1000;
87
+ const maxConcurrent = options.maxConcurrent ?? 100;
88
+ const sweepIntervalMs = options.sweepIntervalMs ?? 1e4;
89
+ const readiness = options.readiness ?? defaultReadiness;
90
+ const spawn = options.spawn ?? defaultSpawn;
91
+ const onMetrics = options.onMetrics;
92
+ const onLog = options.onLog;
93
+ const onTransition = options.onTransition;
94
+ const entries = new Map;
95
+ let sweepTimer;
96
+ let disposed = false;
97
+ const emitMetric = (event) => {
98
+ if (onMetrics === undefined)
99
+ return;
100
+ try {
101
+ onMetrics(event);
102
+ } catch {}
103
+ };
104
+ const emitTransition = (event) => {
105
+ if (onTransition === undefined)
106
+ return;
107
+ try {
108
+ onTransition(event);
109
+ } catch {}
110
+ };
111
+ const emitLog = (event) => {
112
+ if (onLog === undefined)
113
+ return;
114
+ try {
115
+ onLog(event);
116
+ } catch {}
117
+ };
118
+ const tenantCwd = (key) => {
119
+ if (source.kind === "directory") {
120
+ return `${source.root}/${key}`;
121
+ }
122
+ throw new Error(`Unsupported tenant source kind: ${source.kind}`);
123
+ };
124
+ const killChild = async (entry) => {
125
+ const child = entry.child;
126
+ if (child === null)
127
+ return;
128
+ try {
129
+ child.kill();
130
+ } catch {}
131
+ try {
132
+ await child.exited;
133
+ } catch {}
134
+ };
135
+ const removeEntry = async (key, entry) => {
136
+ entries.delete(key);
137
+ await killChild(entry);
138
+ };
139
+ const startSweepIfNeeded = () => {
140
+ if (sweepTimer !== undefined || disposed)
141
+ return;
142
+ sweepTimer = setInterval(() => {
143
+ if (disposed)
144
+ return;
145
+ const now = Date.now();
146
+ for (const [key, entry] of entries) {
147
+ if (entry.tenant === null)
148
+ continue;
149
+ if (idleAfterMs <= 0)
150
+ continue;
151
+ const idleMs = now - entry.tenant.lastTouchedAt;
152
+ if (idleMs >= idleAfterMs) {
153
+ emitTransition({
154
+ idleMs,
155
+ key,
156
+ pid: entry.tenant.pid,
157
+ reason: "idle-threshold",
158
+ type: "idle-kill"
159
+ });
160
+ removeEntry(key, entry).catch(() => {});
161
+ }
162
+ }
163
+ if (entries.size === 0 && sweepTimer !== undefined) {
164
+ clearInterval(sweepTimer);
165
+ sweepTimer = undefined;
166
+ }
167
+ }, sweepIntervalMs);
168
+ if (typeof sweepTimer === "object" && sweepTimer !== null) {
169
+ sweepTimer.unref?.();
170
+ }
171
+ };
172
+ const evictLruIfNeeded = () => {
173
+ if (entries.size < maxConcurrent)
174
+ return;
175
+ let oldestKey;
176
+ let oldestEntry;
177
+ for (const [key, entry] of entries) {
178
+ if (entry.tenant === null)
179
+ continue;
180
+ if (oldestEntry === undefined || oldestEntry.tenant === null || entry.tenant.lastTouchedAt < oldestEntry.tenant.lastTouchedAt) {
181
+ oldestKey = key;
182
+ oldestEntry = entry;
183
+ }
184
+ }
185
+ if (oldestKey !== undefined && oldestEntry !== undefined && oldestEntry.tenant !== null) {
186
+ emitTransition({
187
+ key: oldestKey,
188
+ pid: oldestEntry.tenant.pid,
189
+ reason: "max-concurrent",
190
+ type: "lru-evict"
191
+ });
192
+ removeEntry(oldestKey, oldestEntry).catch(() => {});
193
+ }
194
+ };
195
+ const spawnFresh = async (key) => {
196
+ if (disposed)
197
+ throw new Error("runtime has been disposed");
198
+ evictLruIfNeeded();
199
+ const port = allocateEphemeralPort();
200
+ const startedAt = Date.now();
201
+ const cwd = tenantCwd(key);
202
+ const env = {
203
+ ...process.env,
204
+ NODE_ENV: "production",
205
+ PORT: String(port)
206
+ };
207
+ const child = await spawn({
208
+ cwd,
209
+ env,
210
+ key,
211
+ onLogLine: emitLog
212
+ });
213
+ emitTransition({ key, pid: child.pid, port, type: "spawn" });
214
+ child.exited.then((exitCode) => {
215
+ emitTransition({
216
+ exitCode: exitCode ?? null,
217
+ key,
218
+ pid: child.pid,
219
+ type: "exit"
220
+ });
221
+ const current = entries.get(key);
222
+ if (current !== undefined && current.child === child) {
223
+ entries.delete(key);
224
+ }
225
+ }).catch(() => {});
226
+ try {
227
+ await readiness({ key, port, startedAt });
228
+ } catch (error) {
229
+ try {
230
+ child.kill();
231
+ } catch {}
232
+ entries.delete(key);
233
+ throw error;
234
+ }
235
+ const tenant = {
236
+ key,
237
+ lastTouchedAt: Date.now(),
238
+ pid: child.pid,
239
+ port,
240
+ startedAt
241
+ };
242
+ const entry = entries.get(key);
243
+ if (entry !== undefined) {
244
+ entry.tenant = tenant;
245
+ entry.child = child;
246
+ entry.pending = null;
247
+ }
248
+ const durationMs = Date.now() - startedAt;
249
+ emitMetric({
250
+ durationMs,
251
+ key,
252
+ pid: child.pid,
253
+ port,
254
+ type: "spawn"
255
+ });
256
+ emitTransition({
257
+ durationMs,
258
+ key,
259
+ pid: child.pid,
260
+ port,
261
+ type: "ready"
262
+ });
263
+ startSweepIfNeeded();
264
+ return tenant;
265
+ };
266
+ return {
267
+ async ensure(key) {
268
+ if (disposed)
269
+ throw new Error("runtime has been disposed");
270
+ const existing = entries.get(key);
271
+ if (existing !== undefined) {
272
+ if (existing.tenant !== null) {
273
+ existing.tenant.lastTouchedAt = Date.now();
274
+ return existing.tenant;
275
+ }
276
+ if (existing.pending !== null) {
277
+ return existing.pending;
278
+ }
279
+ }
280
+ const fresh = {
281
+ child: null,
282
+ key,
283
+ pending: null,
284
+ tenant: null
285
+ };
286
+ const promise = spawnFresh(key);
287
+ fresh.pending = promise;
288
+ entries.set(key, fresh);
289
+ return promise;
290
+ },
291
+ touch(key) {
292
+ const entry = entries.get(key);
293
+ if (entry === undefined || entry.tenant === null)
294
+ return;
295
+ entry.tenant.lastTouchedAt = Date.now();
296
+ },
297
+ stats() {
298
+ let running = 0;
299
+ for (const entry of entries.values()) {
300
+ if (entry.tenant !== null)
301
+ running += 1;
302
+ }
303
+ return { running, total: entries.size };
304
+ },
305
+ async kill(key) {
306
+ const entry = entries.get(key);
307
+ if (entry === undefined)
308
+ return;
309
+ await removeEntry(key, entry);
310
+ },
311
+ async dispose() {
312
+ if (disposed)
313
+ return;
314
+ disposed = true;
315
+ if (sweepTimer !== undefined) {
316
+ clearInterval(sweepTimer);
317
+ sweepTimer = undefined;
318
+ }
319
+ const snapshot = [...entries.values()];
320
+ entries.clear();
321
+ await Promise.all(snapshot.map((entry) => killChild(entry)));
322
+ }
323
+ };
324
+ };
325
+ export {
326
+ createRuntime
327
+ };
328
+
329
+ //# debugId=950681D1A6FFFC5D64756E2164756E21
330
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `@absolutejs/runtime` — multi-tenant Bun runtime substrate.\n *\n * Wraps Bun's `spawn` so that \"run this tenant's `bun run start` inside\n * a hibernating, metric-emitting child process\" is one function call.\n * Built for PaaS providers that want to host many small Bun apps under\n * one host process.\n *\n * Architectural role: SB-6's `@absolutejs/runtime` library. Consumers\n * include the hosted `absolutejs.ai` PaaS (eventual) and anyone else\n * who needs the same shape. Stays decoupled from `@absolutejs/sync`\n * and `@absolutejs/isolated-jsc` — those libraries solve different\n * layers of the same stack.\n *\n * v0.0.1 hibernation strategy (per the design doc, STRATEGY-CLOUD.md\n * §9.5): idle-kill at the process layer. Bun has no shipped\n * process-level snapshot/resume primitive as of 2026-05-29, and no\n * open issue tracking one. When that primitive lands we'll add an\n * opt-in `hibernate: 'process-snapshot'` mode and keep idle-kill as\n * the default.\n *\n * @example\n * ```ts\n * import { createRuntime } from '@absolutejs/runtime';\n *\n * const runtime = createRuntime({\n * source: { kind: 'directory', root: '/srv/tenants' },\n * idleAfterMs: 5 * 60 * 1000, // 5 min\n * maxConcurrent: 100,\n * onMetrics: (event) => prometheus.observe(event),\n * onLog: (event) => loki.write(event),\n * });\n *\n * // First call: spawns `bun run start` in /srv/tenants/tenant-42,\n * // injects PORT, waits for readiness, returns the bound port.\n * const tenant = await runtime.ensure('tenant-42');\n * fetch(`http://localhost:${tenant.port}/`);\n *\n * // Subsequent calls reuse the running process.\n * runtime.touch('tenant-42'); // bump idle clock\n *\n * runtime.stats(); // { running, total }\n * await runtime.dispose();\n * ```\n */\n\nimport type { Subprocess } from \"bun\";\n\nexport type TenantSource =\n | { kind: \"directory\"; root: string }\n // Future: { kind: 's3'; bucket: string; prefix: string };\n // Future: { kind: 'git'; remote: string };\n ;\n\n/** Identity of a single tenant process at a point in time. */\nexport type Tenant = {\n /** The key the consumer used to address this tenant. */\n key: string;\n /** The port the child process bound to, discovered after readiness. */\n port: number;\n /** OS process id. */\n pid: number;\n /** Wall-clock when the child was spawned. */\n startedAt: number;\n /** Last time the consumer marked this tenant active. */\n lastTouchedAt: number;\n};\n\nexport type RuntimeMetricEvent = {\n type: \"spawn\";\n key: string;\n pid: number;\n port: number;\n durationMs: number;\n};\n// Future: cpu / memory observation events emitted by the sweeper.\n\nexport type RuntimeLogEvent = {\n key: string;\n pid: number;\n stream: \"stdout\" | \"stderr\";\n /** A single line of output (newline-terminated lines are split client-side). */\n line: string;\n at: number;\n};\n\nexport type RuntimeTransitionEvent =\n | { type: \"spawn\"; key: string; pid: number; port: number }\n | { type: \"ready\"; key: string; pid: number; port: number; durationMs: number }\n | {\n type: \"idle-kill\";\n key: string;\n pid: number;\n reason: \"idle-threshold\";\n idleMs: number;\n }\n | { type: \"lru-evict\"; key: string; pid: number; reason: \"max-concurrent\" }\n | { type: \"exit\"; key: string; pid: number; exitCode: number | null };\n\nexport type ReadinessCheck = (args: {\n key: string;\n port: number;\n /** Wall-clock spawn time so the check can compute its own elapsed. */\n startedAt: number;\n}) => Promise<boolean>;\n\nexport type SpawnFn = (args: {\n cwd: string;\n env: Record<string, string>;\n onLogLine: (event: RuntimeLogEvent) => void;\n key: string;\n}) => Promise<Subprocess>;\n\n/** Options for {@link createRuntime}. */\nexport type RuntimeOptions = {\n /** Where to find tenant project directories. */\n source: TenantSource;\n /**\n * Kill the child process after this many ms with no `touch()` call.\n * Default 5 minutes. Set to `0` to disable idle-kill (only LRU and\n * explicit `kill()` shed processes then).\n */\n idleAfterMs?: number;\n /**\n * Max concurrent tenant processes. When a fresh `ensure()` would\n * push past this, the least-recently-touched process is killed\n * first. Default 100.\n */\n maxConcurrent?: number;\n /**\n * Background sweep interval. Default 10_000 ms. The sweep runs only\n * when the runtime is non-empty and is unrefed so the process can\n * exit cleanly.\n */\n sweepIntervalMs?: number;\n /**\n * Override the readiness check. Default: HTTP GET to\n * `http://localhost:${port}/` with a 100ms retry loop, give up after\n * 30s with a `Tenant readiness timed out` error.\n */\n readiness?: ReadinessCheck;\n /**\n * Override how a child process is spawned. Default: `Bun.spawn` with\n * `['bun', 'run', 'start']`, stdio piped through `onLogLine`, env\n * carrying `PORT=${allocatedPort}` and `NODE_ENV=production`. Tests\n * use this to inject a fixture without writing to disk.\n */\n spawn?: SpawnFn;\n /** Operational metrics — spawn/ready durations etc. */\n onMetrics?: (event: RuntimeMetricEvent) => void;\n /** stdout/stderr stream. Bounded internally; backpressure to the host. */\n onLog?: (event: RuntimeLogEvent) => void;\n /** Lifecycle events — spawn/ready/idle-kill/lru-evict/exit. */\n onTransition?: (event: RuntimeTransitionEvent) => void;\n /**\n * Command to run when spawning. Default `['bun', 'run', 'start']`.\n * Tests use this to point at a fixture script.\n */\n command?: readonly string[];\n};\n\nexport type RuntimeStats = {\n running: number;\n total: number;\n};\n\nexport type Runtime = {\n /**\n * Resolve `key` to a running tenant. Spawns if not running, waits\n * for readiness, returns the live {@link Tenant} including the bound\n * `port`. Concurrent calls to the same key share a single-flight\n * spawn — N callers don't create N processes.\n */\n ensure: (key: string) => Promise<Tenant>;\n /**\n * Mark `key` as active right now. Bumps the idle clock; the next\n * sweep won't consider it for idle-kill until `idleAfterMs` again.\n * Cheap; safe to call before/after each request you route to this\n * tenant.\n */\n touch: (key: string) => void;\n /** Synchronous snapshot. */\n stats: () => RuntimeStats;\n /** Force-kill `key`. No-op if not running. */\n kill: (key: string) => Promise<void>;\n /** Dispose every running child + stop the sweep. Idempotent. */\n dispose: () => Promise<void>;\n};\n\n/* ─── internals ──────────────────────────────────────────────────────── */\n\ntype Entry = {\n key: string;\n /** Set while the spawn is in-flight; concurrent ensure() callers await it. */\n pending: Promise<Tenant> | null;\n tenant: Tenant | null;\n child: Subprocess | null;\n};\n\nconst defaultReadiness: ReadinessCheck = async ({ port, startedAt }) => {\n const deadline = startedAt + 30_000;\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`http://127.0.0.1:${port}/`, {\n signal: AbortSignal.timeout(2_000),\n });\n // Any response — even 404 — means the server bound the port.\n void res;\n return true;\n } catch {\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n throw new Error(\"Tenant readiness timed out after 30s\");\n};\n\n/**\n * Ask the OS for a currently-free TCP port by binding 0 + closing.\n * Race window: another process can grab the port between close and\n * the child's bind. Acceptable for v0.0.1; production deployments\n * should use a coordinated port allocator (or have the child bind 0\n * and report back via stdout, which is a v0.0.2 follow-up).\n */\nconst allocateEphemeralPort = (): number => {\n const server = Bun.listen({\n hostname: \"127.0.0.1\",\n port: 0,\n socket: {\n data: () => {},\n open: () => {},\n },\n });\n const port = server.port;\n server.stop(true);\n return port;\n};\n\nconst splitLines = (() => {\n // Per-stream remainder, keyed by child pid + stream label.\n const remainders = new Map<string, string>();\n return (key: string, chunk: string): string[] => {\n const prior = remainders.get(key) ?? \"\";\n const combined = prior + chunk;\n const parts = combined.split(\"\\n\");\n remainders.set(key, parts.pop() ?? \"\");\n return parts;\n };\n})();\n\nconst defaultSpawn: SpawnFn = async ({\n cwd,\n env,\n onLogLine,\n key,\n}) => {\n const child = Bun.spawn({\n cmd: [\"bun\", \"run\", \"start\"],\n cwd,\n env,\n stderr: \"pipe\",\n stdout: \"pipe\",\n });\n\n // Stream stdout/stderr into onLogLine as newline-terminated lines.\n const readStream = (\n stream: ReadableStream<Uint8Array> | undefined | null,\n label: \"stdout\" | \"stderr\",\n ): void => {\n if (stream === undefined || stream === null) return;\n void (async () => {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n const splitKey = `${child.pid}:${label}`;\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n const text = decoder.decode(value, { stream: true });\n for (const line of splitLines(splitKey, text)) {\n onLogLine({\n at: Date.now(),\n key,\n line,\n pid: child.pid,\n stream: label,\n });\n }\n }\n } catch {\n // stream errored on child exit; nothing to surface\n }\n })();\n };\n readStream(child.stdout as ReadableStream<Uint8Array> | undefined, \"stdout\");\n readStream(child.stderr as ReadableStream<Uint8Array> | undefined, \"stderr\");\n\n return child;\n};\n\nexport const createRuntime = (options: RuntimeOptions): Runtime => {\n const source = options.source;\n const idleAfterMs = options.idleAfterMs ?? 5 * 60 * 1000;\n const maxConcurrent = options.maxConcurrent ?? 100;\n const sweepIntervalMs = options.sweepIntervalMs ?? 10_000;\n const readiness = options.readiness ?? defaultReadiness;\n const spawn = options.spawn ?? defaultSpawn;\n const onMetrics = options.onMetrics;\n const onLog = options.onLog;\n const onTransition = options.onTransition;\n\n const entries = new Map<string, Entry>();\n let sweepTimer: ReturnType<typeof setInterval> | undefined;\n let disposed = false;\n\n const emitMetric = (event: RuntimeMetricEvent): void => {\n if (onMetrics === undefined) return;\n try {\n onMetrics(event);\n } catch {\n /* observational only */\n }\n };\n const emitTransition = (event: RuntimeTransitionEvent): void => {\n if (onTransition === undefined) return;\n try {\n onTransition(event);\n } catch {\n /* observational only */\n }\n };\n const emitLog = (event: RuntimeLogEvent): void => {\n if (onLog === undefined) return;\n try {\n onLog(event);\n } catch {\n /* observational only */\n }\n };\n\n const tenantCwd = (key: string): string => {\n if (source.kind === \"directory\") {\n return `${source.root}/${key}`;\n }\n throw new Error(\n `Unsupported tenant source kind: ${(source as { kind: string }).kind}`,\n );\n };\n\n const killChild = async (entry: Entry): Promise<void> => {\n const child = entry.child;\n if (child === null) return;\n try {\n child.kill();\n } catch {\n /* already dead */\n }\n try {\n await child.exited;\n } catch {\n /* ignore */\n }\n };\n\n const removeEntry = async (key: string, entry: Entry): Promise<void> => {\n entries.delete(key);\n await killChild(entry);\n };\n\n const startSweepIfNeeded = (): void => {\n if (sweepTimer !== undefined || disposed) return;\n sweepTimer = setInterval(() => {\n if (disposed) return;\n const now = Date.now();\n for (const [key, entry] of entries) {\n if (entry.tenant === null) continue;\n if (idleAfterMs <= 0) continue;\n const idleMs = now - entry.tenant.lastTouchedAt;\n if (idleMs >= idleAfterMs) {\n emitTransition({\n idleMs,\n key,\n pid: entry.tenant.pid,\n reason: \"idle-threshold\",\n type: \"idle-kill\",\n });\n void removeEntry(key, entry).catch(() => {});\n }\n }\n if (entries.size === 0 && sweepTimer !== undefined) {\n clearInterval(sweepTimer);\n sweepTimer = undefined;\n }\n }, sweepIntervalMs);\n if (typeof sweepTimer === \"object\" && sweepTimer !== null) {\n (sweepTimer as { unref?: () => void }).unref?.();\n }\n };\n\n const evictLruIfNeeded = (): void => {\n if (entries.size < maxConcurrent) return;\n let oldestKey: string | undefined;\n let oldestEntry: Entry | undefined;\n for (const [key, entry] of entries) {\n if (entry.tenant === null) continue; // mid-spawn; don't evict\n if (\n oldestEntry === undefined ||\n oldestEntry.tenant === null ||\n entry.tenant.lastTouchedAt < oldestEntry.tenant.lastTouchedAt\n ) {\n oldestKey = key;\n oldestEntry = entry;\n }\n }\n if (oldestKey !== undefined && oldestEntry !== undefined && oldestEntry.tenant !== null) {\n emitTransition({\n key: oldestKey,\n pid: oldestEntry.tenant.pid,\n reason: \"max-concurrent\",\n type: \"lru-evict\",\n });\n void removeEntry(oldestKey, oldestEntry).catch(() => {});\n }\n };\n\n const spawnFresh = async (key: string): Promise<Tenant> => {\n if (disposed) throw new Error(\"runtime has been disposed\");\n evictLruIfNeeded();\n\n const port = allocateEphemeralPort();\n const startedAt = Date.now();\n const cwd = tenantCwd(key);\n const env: Record<string, string> = {\n ...(process.env as Record<string, string>),\n NODE_ENV: \"production\",\n PORT: String(port),\n };\n\n const child = await spawn({\n cwd,\n env,\n key,\n onLogLine: emitLog,\n });\n\n emitTransition({ key, pid: child.pid, port, type: \"spawn\" });\n\n // Reap the entry when the process exits — whether we killed it or\n // it died on its own. Single source of truth for \"is this tenant\n // running\": the entry's `tenant` field.\n void child.exited\n .then((exitCode) => {\n emitTransition({\n exitCode: exitCode ?? null,\n key,\n pid: child.pid,\n type: \"exit\",\n });\n // Only delete if THIS entry is still the live one. A consumer\n // who killed + immediately re-ensured may have replaced it.\n const current = entries.get(key);\n if (current !== undefined && current.child === child) {\n entries.delete(key);\n }\n })\n .catch(() => {});\n\n try {\n await readiness({ key, port, startedAt });\n } catch (error) {\n try {\n child.kill();\n } catch {\n /* ignore */\n }\n entries.delete(key);\n throw error;\n }\n\n const tenant: Tenant = {\n key,\n lastTouchedAt: Date.now(),\n pid: child.pid,\n port,\n startedAt,\n };\n const entry = entries.get(key);\n if (entry !== undefined) {\n entry.tenant = tenant;\n entry.child = child;\n entry.pending = null;\n }\n const durationMs = Date.now() - startedAt;\n emitMetric({\n durationMs,\n key,\n pid: child.pid,\n port,\n type: \"spawn\",\n });\n emitTransition({\n durationMs,\n key,\n pid: child.pid,\n port,\n type: \"ready\",\n });\n startSweepIfNeeded();\n return tenant;\n };\n\n return {\n async ensure(key) {\n if (disposed) throw new Error(\"runtime has been disposed\");\n const existing = entries.get(key);\n if (existing !== undefined) {\n if (existing.tenant !== null) {\n existing.tenant.lastTouchedAt = Date.now();\n return existing.tenant;\n }\n if (existing.pending !== null) {\n return existing.pending;\n }\n }\n const fresh: Entry = {\n child: null,\n key,\n pending: null,\n tenant: null,\n };\n const promise = spawnFresh(key);\n fresh.pending = promise;\n entries.set(key, fresh);\n return promise;\n },\n\n touch(key) {\n const entry = entries.get(key);\n if (entry === undefined || entry.tenant === null) return;\n entry.tenant.lastTouchedAt = Date.now();\n },\n\n stats() {\n let running = 0;\n for (const entry of entries.values()) {\n if (entry.tenant !== null) running += 1;\n }\n return { running, total: entries.size };\n },\n\n async kill(key) {\n const entry = entries.get(key);\n if (entry === undefined) return;\n await removeEntry(key, entry);\n },\n\n async dispose() {\n if (disposed) return;\n disposed = true;\n if (sweepTimer !== undefined) {\n clearInterval(sweepTimer);\n sweepTimer = undefined;\n }\n const snapshot = [...entries.values()];\n entries.clear();\n await Promise.all(snapshot.map((entry) => killChild(entry)));\n },\n };\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "950681D1A6FFFC5D64756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@absolutejs/runtime",
3
+ "version": "0.0.1",
4
+ "description": "Multi-tenant Bun runtime substrate for PaaS providers — spawn isolated child Bun processes per tenant, idle-kill, metric emit. The library SB-6 surfaces between isolated-jsc + sync and the hosted product downstream.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/absolutejs/runtime.git"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "type": "module",
13
+ "license": "BSL-1.1",
14
+ "author": "Alex Kahn",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "absolutejs",
20
+ "bun",
21
+ "paas",
22
+ "multi-tenant",
23
+ "runtime",
24
+ "hibernation",
25
+ "process-pool"
26
+ ],
27
+ "scripts": {
28
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
29
+ "test": "bun test tests/",
30
+ "typecheck": "tsc --noEmit",
31
+ "format": "prettier --write \"./**/*.{ts,json,md}\"",
32
+ "check:package": "bun run typecheck && bun run build && bun run test",
33
+ "release": "bun run format && bun run check:package && bun publish"
34
+ },
35
+ "peerDependencies": {
36
+ "bun-types": ">= 1.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/bun": "1.3.14",
40
+ "prettier": "3.5.3",
41
+ "typescript": "5.8.3"
42
+ },
43
+ "exports": {
44
+ ".": {
45
+ "types": "./dist/index.d.ts",
46
+ "import": "./dist/index.js",
47
+ "default": "./dist/index.js"
48
+ }
49
+ },
50
+ "files": ["dist", "README.md"]
51
+ }