@absolutejs/runtime 0.0.1 → 0.1.0
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/README.md +21 -6
- package/dist/index.d.ts +80 -8
- package/dist/index.js +156 -19
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,18 +27,29 @@ runtime.stats(); // { running, total }
|
|
|
27
27
|
await runtime.dispose();
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Surface (0.1.0)
|
|
31
31
|
|
|
32
32
|
| API | Purpose |
|
|
33
33
|
|---|---|
|
|
34
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 }`. |
|
|
35
|
+
| `runtime.ensure(key)` | Spawn-or-reuse. Single-flight on concurrent calls to the same key. Throws fast if `key` is in a back-off window. Returns `{ key, port, pid, startedAt, lastTouchedAt }`. |
|
|
36
36
|
| `runtime.touch(key)` | Bump the idle clock for an active tenant. Cheap; call before/after each request. |
|
|
37
|
-
| `runtime.stats()` | `{ running, total }` snapshot. |
|
|
37
|
+
| `runtime.stats()` | `{ running, total, draining, backoff }` snapshot. |
|
|
38
38
|
| `runtime.kill(key)` | Force-kill. No-op if not running. |
|
|
39
|
+
| `runtime.restart(key)` | Kill + spawn fresh in one call. For deploys that swap to a new release. |
|
|
40
|
+
| `runtime.clearBackoff(key)` | Forget consecutive-failure state. |
|
|
41
|
+
| `runtime.drain()` | Refuse new `ensure()` spawns; existing tenants keep running. For graceful shard shutdown. |
|
|
39
42
|
| `runtime.dispose()` | Kill all + stop the sweeper. Idempotent. |
|
|
40
43
|
|
|
41
|
-
###
|
|
44
|
+
### Back-off on spawn failures
|
|
45
|
+
|
|
46
|
+
A spawn that fails (spawn fn threw, or readiness timed out) records a per-key `{ attempt, retryAt, lastError }` and the next `ensure(key)` throws fast until `retryAt`. After `maxFailures` (default 10) consecutive failures, the key stays refused until `clearBackoff(key)`. Defaults: `baseMs=1000`, `maxMs=60_000`, `maxFailures=10`. Override via the `backoff` option. Without this, one broken tenant thrashes the host with rapid spawn retries.
|
|
47
|
+
|
|
48
|
+
### Observation (Linux-only)
|
|
49
|
+
|
|
50
|
+
When `observeIntervalMs > 0` (default `30_000`), the sweeper periodically reads `/proc/<pid>/stat` (utime + stime) and `/proc/<pid>/status` (VmRSS) per running tenant and emits `{ type: 'observation', key, pid, cpuMs, rssBytes, at }` via `onMetrics`. This is the per-tenant data `@absolutejs/metering` consumes to attribute idle hibernation cost. Silently skips on non-Linux.
|
|
51
|
+
|
|
52
|
+
### Hibernation strategy
|
|
42
53
|
|
|
43
54
|
**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
55
|
|
|
@@ -46,7 +57,11 @@ The trade-off the default makes explicit: first call after idle pays a full Bun
|
|
|
46
57
|
|
|
47
58
|
### Observability hooks
|
|
48
59
|
|
|
49
|
-
`onLog`, `onMetrics`, and `onTransition` are pluggable. `onLog` receives newline-split stdout/stderr from every child; `onMetrics` fires on spawn with `
|
|
60
|
+
`onLog`, `onMetrics`, and `onTransition` are pluggable. `onLog` receives newline-split stdout/stderr from every child; `onMetrics` fires on spawn (`{ type: 'spawn', durationMs }`) and periodically with observations on Linux (`{ type: 'observation', cpuMs, rssBytes }`); `onTransition` fires on every state change: `spawn`, `ready`, `idle-kill`, `lru-evict`, `exit` (with a structured `reason`), `backoff`, `drain`.
|
|
61
|
+
|
|
62
|
+
### Exit reasons
|
|
63
|
+
|
|
64
|
+
The `exit` transition's `reason` field is one of: `crashed`, `exited-clean`, `idle-killed`, `lru-evicted`, `killed`, `readiness-timeout`, `disposed`, `restarted`. The meter / control plane uses this to decide whether to charge, retry, or alert.
|
|
50
65
|
|
|
51
66
|
### Pluggable spawn + readiness
|
|
52
67
|
|
|
@@ -62,4 +77,4 @@ The trade-off the default makes explicit: first call after idle pays a full Bun
|
|
|
62
77
|
|
|
63
78
|
## License
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
BSL 1.1 with a named carveout for the hosted multi-tenant Bun runtime / PaaS substrate category (Convex, Liveblocks, Vercel, Render, Fly, Cloudflare Workers). See [LICENSE](./LICENSE). Change Date: 4 years from first release; Change License: Apache 2.0.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
* and `@absolutejs/isolated-jsc` — those libraries solve different
|
|
13
13
|
* layers of the same stack.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* open issue tracking one. When that primitive lands we'll add an
|
|
15
|
+
* Hibernation strategy (per STRATEGY-CLOUD.md §9.5): idle-kill at the
|
|
16
|
+
* process layer. Bun has no shipped process-level snapshot/resume
|
|
17
|
+
* primitive as of 2026-05-29. When that primitive lands we'll add an
|
|
19
18
|
* opt-in `hibernate: 'process-snapshot'` mode and keep idle-kill as
|
|
20
19
|
* the default.
|
|
21
20
|
*
|
|
@@ -29,6 +28,7 @@
|
|
|
29
28
|
* maxConcurrent: 100,
|
|
30
29
|
* onMetrics: (event) => prometheus.observe(event),
|
|
31
30
|
* onLog: (event) => loki.write(event),
|
|
31
|
+
* observeIntervalMs: 30_000,
|
|
32
32
|
* });
|
|
33
33
|
*
|
|
34
34
|
* // First call: spawns `bun run start` in /srv/tenants/tenant-42,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* // Subsequent calls reuse the running process.
|
|
40
40
|
* runtime.touch('tenant-42'); // bump idle clock
|
|
41
41
|
*
|
|
42
|
-
* runtime.stats(); // { running, total }
|
|
42
|
+
* runtime.stats(); // { running, total, draining }
|
|
43
43
|
* await runtime.dispose();
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
@@ -67,6 +67,16 @@ export type RuntimeMetricEvent = {
|
|
|
67
67
|
pid: number;
|
|
68
68
|
port: number;
|
|
69
69
|
durationMs: number;
|
|
70
|
+
} | {
|
|
71
|
+
/** Periodic observation emitted by the sweeper (Linux-only; see `observeIntervalMs`). */
|
|
72
|
+
type: "observation";
|
|
73
|
+
key: string;
|
|
74
|
+
pid: number;
|
|
75
|
+
/** Cumulative CPU ms used by the child since spawn, derived from `/proc/<pid>/stat`. */
|
|
76
|
+
cpuMs: number;
|
|
77
|
+
/** Resident set size in bytes, derived from `/proc/<pid>/status` VmRSS. */
|
|
78
|
+
rssBytes: number;
|
|
79
|
+
at: number;
|
|
70
80
|
};
|
|
71
81
|
export type RuntimeLogEvent = {
|
|
72
82
|
key: string;
|
|
@@ -76,6 +86,19 @@ export type RuntimeLogEvent = {
|
|
|
76
86
|
line: string;
|
|
77
87
|
at: number;
|
|
78
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Why a tenant process ended. Used by `RuntimeTransitionEvent` of type
|
|
91
|
+
* `'exit'` to give the consumer enough info to charge or restart correctly:
|
|
92
|
+
* - `crashed` — the process exited on its own with a non-zero code
|
|
93
|
+
* - `exited-clean` — the process exited 0 (probably a graceful self-stop)
|
|
94
|
+
* - `idle-killed` — the sweeper killed it after `idleAfterMs` with no `touch()`
|
|
95
|
+
* - `lru-evicted` — `ensure()` for a new tenant evicted this one
|
|
96
|
+
* - `killed` — explicit `runtime.kill(key)` call
|
|
97
|
+
* - `readiness-timeout` — readiness check failed; we killed during spawn
|
|
98
|
+
* - `disposed` — `runtime.dispose()` killed it
|
|
99
|
+
* - `restarted` — `runtime.restart(key)` killed it on purpose
|
|
100
|
+
*/
|
|
101
|
+
export type ExitReason = "crashed" | "exited-clean" | "idle-killed" | "lru-evicted" | "killed" | "readiness-timeout" | "disposed" | "restarted";
|
|
79
102
|
export type RuntimeTransitionEvent = {
|
|
80
103
|
type: "spawn";
|
|
81
104
|
key: string;
|
|
@@ -103,6 +126,16 @@ export type RuntimeTransitionEvent = {
|
|
|
103
126
|
key: string;
|
|
104
127
|
pid: number;
|
|
105
128
|
exitCode: number | null;
|
|
129
|
+
reason: ExitReason;
|
|
130
|
+
} | {
|
|
131
|
+
/** A spawn was deferred because the key is in the back-off window after a failure. */
|
|
132
|
+
type: "backoff";
|
|
133
|
+
key: string;
|
|
134
|
+
attempt: number;
|
|
135
|
+
retryAfterMs: number;
|
|
136
|
+
} | {
|
|
137
|
+
type: "drain";
|
|
138
|
+
reason: "drain-requested";
|
|
106
139
|
};
|
|
107
140
|
export type ReadinessCheck = (args: {
|
|
108
141
|
key: string;
|
|
@@ -116,6 +149,14 @@ export type SpawnFn = (args: {
|
|
|
116
149
|
onLogLine: (event: RuntimeLogEvent) => void;
|
|
117
150
|
key: string;
|
|
118
151
|
}) => Promise<Subprocess>;
|
|
152
|
+
export type SpawnBackoff = {
|
|
153
|
+
/** First retry waits this long. Default 1000 ms. */
|
|
154
|
+
baseMs?: number;
|
|
155
|
+
/** Maximum back-off (the cap on the doubled wait). Default 60_000 ms. */
|
|
156
|
+
maxMs?: number;
|
|
157
|
+
/** After this many consecutive failures, `ensure()` throws immediately for this key until reset. Default 10. */
|
|
158
|
+
maxFailures?: number;
|
|
159
|
+
};
|
|
119
160
|
/** Options for {@link createRuntime}. */
|
|
120
161
|
export type RuntimeOptions = {
|
|
121
162
|
/** Where to find tenant project directories. */
|
|
@@ -138,9 +179,16 @@ export type RuntimeOptions = {
|
|
|
138
179
|
* exit cleanly.
|
|
139
180
|
*/
|
|
140
181
|
sweepIntervalMs?: number;
|
|
182
|
+
/**
|
|
183
|
+
* How often the sweeper observes CPU + RSS per running tenant. Default
|
|
184
|
+
* 30_000 ms. Set to `0` to disable; observation only works on Linux
|
|
185
|
+
* (`/proc/<pid>` derived) — the sweeper silently skips on other OSes.
|
|
186
|
+
* Output goes to `onMetrics` as `{ type: 'observation', ... }`.
|
|
187
|
+
*/
|
|
188
|
+
observeIntervalMs?: number;
|
|
141
189
|
/**
|
|
142
190
|
* Override the readiness check. Default: HTTP GET to
|
|
143
|
-
* `http://
|
|
191
|
+
* `http://127.0.0.1:${port}/` with a 100ms retry loop, give up after
|
|
144
192
|
* 30s with a `Tenant readiness timed out` error.
|
|
145
193
|
*/
|
|
146
194
|
readiness?: ReadinessCheck;
|
|
@@ -151,11 +199,13 @@ export type RuntimeOptions = {
|
|
|
151
199
|
* use this to inject a fixture without writing to disk.
|
|
152
200
|
*/
|
|
153
201
|
spawn?: SpawnFn;
|
|
154
|
-
/**
|
|
202
|
+
/** Exponential-backoff policy for consecutive spawn failures. */
|
|
203
|
+
backoff?: SpawnBackoff;
|
|
204
|
+
/** Operational metrics — spawn/ready durations + periodic observations. */
|
|
155
205
|
onMetrics?: (event: RuntimeMetricEvent) => void;
|
|
156
206
|
/** stdout/stderr stream. Bounded internally; backpressure to the host. */
|
|
157
207
|
onLog?: (event: RuntimeLogEvent) => void;
|
|
158
|
-
/** Lifecycle events — spawn/ready/idle-kill/lru-evict/exit. */
|
|
208
|
+
/** Lifecycle events — spawn/ready/idle-kill/lru-evict/exit/backoff/drain. */
|
|
159
209
|
onTransition?: (event: RuntimeTransitionEvent) => void;
|
|
160
210
|
/**
|
|
161
211
|
* Command to run when spawning. Default `['bun', 'run', 'start']`.
|
|
@@ -166,6 +216,10 @@ export type RuntimeOptions = {
|
|
|
166
216
|
export type RuntimeStats = {
|
|
167
217
|
running: number;
|
|
168
218
|
total: number;
|
|
219
|
+
/** True when the runtime is draining — refusing new ensure() calls. */
|
|
220
|
+
draining: boolean;
|
|
221
|
+
/** Number of keys currently in the back-off window. */
|
|
222
|
+
backoff: number;
|
|
169
223
|
};
|
|
170
224
|
export type Runtime = {
|
|
171
225
|
/**
|
|
@@ -173,6 +227,9 @@ export type Runtime = {
|
|
|
173
227
|
* for readiness, returns the live {@link Tenant} including the bound
|
|
174
228
|
* `port`. Concurrent calls to the same key share a single-flight
|
|
175
229
|
* spawn — N callers don't create N processes.
|
|
230
|
+
*
|
|
231
|
+
* If `key` is in the back-off window after a recent failure, throws
|
|
232
|
+
* immediately (without spawning). Use `clearBackoff(key)` to retry early.
|
|
176
233
|
*/
|
|
177
234
|
ensure: (key: string) => Promise<Tenant>;
|
|
178
235
|
/**
|
|
@@ -186,6 +243,21 @@ export type Runtime = {
|
|
|
186
243
|
stats: () => RuntimeStats;
|
|
187
244
|
/** Force-kill `key`. No-op if not running. */
|
|
188
245
|
kill: (key: string) => Promise<void>;
|
|
246
|
+
/**
|
|
247
|
+
* Kill `key` and respawn it. Used by deploys to swap to a new release
|
|
248
|
+
* after the `current` symlink has been updated. Concurrent restart
|
|
249
|
+
* calls for the same key share a single-flight respawn.
|
|
250
|
+
*/
|
|
251
|
+
restart: (key: string) => Promise<Tenant>;
|
|
252
|
+
/** Forget any consecutive-failure state for `key`. Next `ensure()` retries immediately. */
|
|
253
|
+
clearBackoff: (key: string) => void;
|
|
254
|
+
/**
|
|
255
|
+
* Begin draining: refuse new `ensure()` calls (they throw immediately).
|
|
256
|
+
* In-flight spawns and existing tenants are untouched — wait for
|
|
257
|
+
* `stats().running` to reach 0, or call `dispose()` for hard shutdown.
|
|
258
|
+
* Useful for graceful shard shutdown before a host reboot.
|
|
259
|
+
*/
|
|
260
|
+
drain: () => void;
|
|
189
261
|
/** Dispose every running child + stop the sweep. Idempotent. */
|
|
190
262
|
dispose: () => Promise<void>;
|
|
191
263
|
};
|
package/dist/index.js
CHANGED
|
@@ -38,14 +38,38 @@ var splitLines = (() => {
|
|
|
38
38
|
return parts;
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
|
-
var
|
|
41
|
+
var isLinux = typeof process !== "undefined" && process.platform === "linux";
|
|
42
|
+
var readProcStats = async (pid) => {
|
|
43
|
+
if (!isLinux)
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
const statText = await Bun.file(`/proc/${pid}/stat`).text();
|
|
47
|
+
const statusText = await Bun.file(`/proc/${pid}/status`).text();
|
|
48
|
+
const closeParen = statText.lastIndexOf(")");
|
|
49
|
+
if (closeParen === -1)
|
|
50
|
+
return null;
|
|
51
|
+
const after = statText.slice(closeParen + 2).split(" ");
|
|
52
|
+
const utime = Number(after[11]);
|
|
53
|
+
const stime = Number(after[12]);
|
|
54
|
+
if (!Number.isFinite(utime) || !Number.isFinite(stime))
|
|
55
|
+
return null;
|
|
56
|
+
const ticksPerSec = globalThis.Bun?.clockTicksPerSecond ?? 100;
|
|
57
|
+
const cpuMs = (utime + stime) / ticksPerSec * 1000;
|
|
58
|
+
const match = statusText.match(/^VmRSS:\s+(\d+)\s+kB/m);
|
|
59
|
+
const rssBytes = match && match[1] ? Number(match[1]) * 1024 : 0;
|
|
60
|
+
return { cpuMs, rssBytes };
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var defaultSpawn = (command) => async ({
|
|
42
66
|
cwd,
|
|
43
67
|
env,
|
|
44
68
|
onLogLine,
|
|
45
69
|
key
|
|
46
70
|
}) => {
|
|
47
71
|
const child = Bun.spawn({
|
|
48
|
-
cmd: [
|
|
72
|
+
cmd: [...command],
|
|
49
73
|
cwd,
|
|
50
74
|
env,
|
|
51
75
|
stderr: "pipe",
|
|
@@ -86,14 +110,25 @@ var createRuntime = (options) => {
|
|
|
86
110
|
const idleAfterMs = options.idleAfterMs ?? 5 * 60 * 1000;
|
|
87
111
|
const maxConcurrent = options.maxConcurrent ?? 100;
|
|
88
112
|
const sweepIntervalMs = options.sweepIntervalMs ?? 1e4;
|
|
113
|
+
const observeIntervalMs = options.observeIntervalMs ?? 30000;
|
|
89
114
|
const readiness = options.readiness ?? defaultReadiness;
|
|
90
|
-
const
|
|
115
|
+
const command = options.command ?? ["bun", "run", "start"];
|
|
116
|
+
const spawn = options.spawn ?? defaultSpawn(command);
|
|
91
117
|
const onMetrics = options.onMetrics;
|
|
92
118
|
const onLog = options.onLog;
|
|
93
119
|
const onTransition = options.onTransition;
|
|
120
|
+
const backoffOptions = {
|
|
121
|
+
baseMs: options.backoff?.baseMs ?? 1000,
|
|
122
|
+
maxFailures: options.backoff?.maxFailures ?? 10,
|
|
123
|
+
maxMs: options.backoff?.maxMs ?? 60000
|
|
124
|
+
};
|
|
94
125
|
const entries = new Map;
|
|
126
|
+
const backoffs = new Map;
|
|
127
|
+
const exitReasons = new Map;
|
|
95
128
|
let sweepTimer;
|
|
129
|
+
let lastObserveAt = 0;
|
|
96
130
|
let disposed = false;
|
|
131
|
+
let draining = false;
|
|
97
132
|
const emitMetric = (event) => {
|
|
98
133
|
if (onMetrics === undefined)
|
|
99
134
|
return;
|
|
@@ -121,10 +156,12 @@ var createRuntime = (options) => {
|
|
|
121
156
|
}
|
|
122
157
|
throw new Error(`Unsupported tenant source kind: ${source.kind}`);
|
|
123
158
|
};
|
|
124
|
-
const
|
|
159
|
+
const killChildWithReason = async (entry, reason) => {
|
|
125
160
|
const child = entry.child;
|
|
126
161
|
if (child === null)
|
|
127
162
|
return;
|
|
163
|
+
entry.pendingExitReason = reason;
|
|
164
|
+
exitReasons.set(child.pid, reason);
|
|
128
165
|
try {
|
|
129
166
|
child.kill();
|
|
130
167
|
} catch {}
|
|
@@ -132,9 +169,39 @@ var createRuntime = (options) => {
|
|
|
132
169
|
await child.exited;
|
|
133
170
|
} catch {}
|
|
134
171
|
};
|
|
135
|
-
const removeEntry = async (key, entry) => {
|
|
172
|
+
const removeEntry = async (key, entry, reason) => {
|
|
136
173
|
entries.delete(key);
|
|
137
|
-
await
|
|
174
|
+
await killChildWithReason(entry, reason);
|
|
175
|
+
};
|
|
176
|
+
const recordBackoff = (key, error) => {
|
|
177
|
+
const prev = backoffs.get(key);
|
|
178
|
+
const attempt = (prev?.attempt ?? 0) + 1;
|
|
179
|
+
const wait = Math.min(backoffOptions.maxMs, backoffOptions.baseMs * 2 ** (attempt - 1));
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
backoffs.set(key, { attempt, lastError: message, retryAt: Date.now() + wait });
|
|
182
|
+
};
|
|
183
|
+
const observeRunning = async () => {
|
|
184
|
+
if (!isLinux || observeIntervalMs <= 0 || onMetrics === undefined)
|
|
185
|
+
return;
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
if (now - lastObserveAt < observeIntervalMs)
|
|
188
|
+
return;
|
|
189
|
+
lastObserveAt = now;
|
|
190
|
+
for (const [key, entry] of entries) {
|
|
191
|
+
if (entry.tenant === null)
|
|
192
|
+
continue;
|
|
193
|
+
const stats = await readProcStats(entry.tenant.pid);
|
|
194
|
+
if (stats === null)
|
|
195
|
+
continue;
|
|
196
|
+
emitMetric({
|
|
197
|
+
at: now,
|
|
198
|
+
cpuMs: stats.cpuMs,
|
|
199
|
+
key,
|
|
200
|
+
pid: entry.tenant.pid,
|
|
201
|
+
rssBytes: stats.rssBytes,
|
|
202
|
+
type: "observation"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
138
205
|
};
|
|
139
206
|
const startSweepIfNeeded = () => {
|
|
140
207
|
if (sweepTimer !== undefined || disposed)
|
|
@@ -157,9 +224,10 @@ var createRuntime = (options) => {
|
|
|
157
224
|
reason: "idle-threshold",
|
|
158
225
|
type: "idle-kill"
|
|
159
226
|
});
|
|
160
|
-
removeEntry(key, entry).catch(() => {});
|
|
227
|
+
removeEntry(key, entry, "idle-killed").catch(() => {});
|
|
161
228
|
}
|
|
162
229
|
}
|
|
230
|
+
observeRunning().catch(() => {});
|
|
163
231
|
if (entries.size === 0 && sweepTimer !== undefined) {
|
|
164
232
|
clearInterval(sweepTimer);
|
|
165
233
|
sweepTimer = undefined;
|
|
@@ -189,12 +257,14 @@ var createRuntime = (options) => {
|
|
|
189
257
|
reason: "max-concurrent",
|
|
190
258
|
type: "lru-evict"
|
|
191
259
|
});
|
|
192
|
-
removeEntry(oldestKey, oldestEntry).catch(() => {});
|
|
260
|
+
removeEntry(oldestKey, oldestEntry, "lru-evicted").catch(() => {});
|
|
193
261
|
}
|
|
194
262
|
};
|
|
195
263
|
const spawnFresh = async (key) => {
|
|
196
264
|
if (disposed)
|
|
197
265
|
throw new Error("runtime has been disposed");
|
|
266
|
+
if (draining)
|
|
267
|
+
throw new Error("runtime is draining; ensure() refused");
|
|
198
268
|
evictLruIfNeeded();
|
|
199
269
|
const port = allocateEphemeralPort();
|
|
200
270
|
const startedAt = Date.now();
|
|
@@ -204,18 +274,29 @@ var createRuntime = (options) => {
|
|
|
204
274
|
NODE_ENV: "production",
|
|
205
275
|
PORT: String(port)
|
|
206
276
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
277
|
+
let child;
|
|
278
|
+
try {
|
|
279
|
+
child = await spawn({
|
|
280
|
+
cwd,
|
|
281
|
+
env,
|
|
282
|
+
key,
|
|
283
|
+
onLogLine: emitLog
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
entries.delete(key);
|
|
287
|
+
recordBackoff(key, error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
213
290
|
emitTransition({ key, pid: child.pid, port, type: "spawn" });
|
|
214
291
|
child.exited.then((exitCode) => {
|
|
292
|
+
const stashed = exitReasons.get(child.pid);
|
|
293
|
+
const reason = stashed ?? (exitCode === 0 ? "exited-clean" : "crashed");
|
|
294
|
+
exitReasons.delete(child.pid);
|
|
215
295
|
emitTransition({
|
|
216
296
|
exitCode: exitCode ?? null,
|
|
217
297
|
key,
|
|
218
298
|
pid: child.pid,
|
|
299
|
+
reason,
|
|
219
300
|
type: "exit"
|
|
220
301
|
});
|
|
221
302
|
const current = entries.get(key);
|
|
@@ -226,10 +307,15 @@ var createRuntime = (options) => {
|
|
|
226
307
|
try {
|
|
227
308
|
await readiness({ key, port, startedAt });
|
|
228
309
|
} catch (error) {
|
|
310
|
+
const entry2 = entries.get(key);
|
|
311
|
+
if (entry2 !== undefined) {
|
|
312
|
+
entry2.pendingExitReason = "readiness-timeout";
|
|
313
|
+
}
|
|
229
314
|
try {
|
|
230
315
|
child.kill();
|
|
231
316
|
} catch {}
|
|
232
317
|
entries.delete(key);
|
|
318
|
+
recordBackoff(key, error);
|
|
233
319
|
throw error;
|
|
234
320
|
}
|
|
235
321
|
const tenant = {
|
|
@@ -245,6 +331,7 @@ var createRuntime = (options) => {
|
|
|
245
331
|
entry.child = child;
|
|
246
332
|
entry.pending = null;
|
|
247
333
|
}
|
|
334
|
+
backoffs.delete(key);
|
|
248
335
|
const durationMs = Date.now() - startedAt;
|
|
249
336
|
emitMetric({
|
|
250
337
|
durationMs,
|
|
@@ -263,6 +350,24 @@ var createRuntime = (options) => {
|
|
|
263
350
|
startSweepIfNeeded();
|
|
264
351
|
return tenant;
|
|
265
352
|
};
|
|
353
|
+
const checkBackoff = (key) => {
|
|
354
|
+
const state = backoffs.get(key);
|
|
355
|
+
if (state === undefined)
|
|
356
|
+
return;
|
|
357
|
+
if (state.attempt >= backoffOptions.maxFailures) {
|
|
358
|
+
throw new Error(`Tenant "${key}" exceeded ${backoffOptions.maxFailures} consecutive spawn failures; clearBackoff() to retry. Last error: ${state.lastError}`);
|
|
359
|
+
}
|
|
360
|
+
const remaining = state.retryAt - Date.now();
|
|
361
|
+
if (remaining > 0) {
|
|
362
|
+
emitTransition({
|
|
363
|
+
attempt: state.attempt,
|
|
364
|
+
key,
|
|
365
|
+
retryAfterMs: remaining,
|
|
366
|
+
type: "backoff"
|
|
367
|
+
});
|
|
368
|
+
throw new Error(`Tenant "${key}" is backing off after ${state.attempt} failure(s); retry in ${remaining}ms. Last error: ${state.lastError}`);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
266
371
|
return {
|
|
267
372
|
async ensure(key) {
|
|
268
373
|
if (disposed)
|
|
@@ -277,10 +382,14 @@ var createRuntime = (options) => {
|
|
|
277
382
|
return existing.pending;
|
|
278
383
|
}
|
|
279
384
|
}
|
|
385
|
+
if (draining)
|
|
386
|
+
throw new Error("runtime is draining; ensure() refused");
|
|
387
|
+
checkBackoff(key);
|
|
280
388
|
const fresh = {
|
|
281
389
|
child: null,
|
|
282
390
|
key,
|
|
283
391
|
pending: null,
|
|
392
|
+
pendingExitReason: null,
|
|
284
393
|
tenant: null
|
|
285
394
|
};
|
|
286
395
|
const promise = spawnFresh(key);
|
|
@@ -300,13 +409,41 @@ var createRuntime = (options) => {
|
|
|
300
409
|
if (entry.tenant !== null)
|
|
301
410
|
running += 1;
|
|
302
411
|
}
|
|
303
|
-
return { running, total: entries.size };
|
|
412
|
+
return { backoff: backoffs.size, draining, running, total: entries.size };
|
|
304
413
|
},
|
|
305
414
|
async kill(key) {
|
|
306
415
|
const entry = entries.get(key);
|
|
307
416
|
if (entry === undefined)
|
|
308
417
|
return;
|
|
309
|
-
await removeEntry(key, entry);
|
|
418
|
+
await removeEntry(key, entry, "killed");
|
|
419
|
+
},
|
|
420
|
+
async restart(key) {
|
|
421
|
+
if (disposed)
|
|
422
|
+
throw new Error("runtime has been disposed");
|
|
423
|
+
const entry = entries.get(key);
|
|
424
|
+
if (entry !== undefined) {
|
|
425
|
+
await removeEntry(key, entry, "restarted");
|
|
426
|
+
}
|
|
427
|
+
const fresh = {
|
|
428
|
+
child: null,
|
|
429
|
+
key,
|
|
430
|
+
pending: null,
|
|
431
|
+
pendingExitReason: null,
|
|
432
|
+
tenant: null
|
|
433
|
+
};
|
|
434
|
+
const promise = spawnFresh(key);
|
|
435
|
+
fresh.pending = promise;
|
|
436
|
+
entries.set(key, fresh);
|
|
437
|
+
return promise;
|
|
438
|
+
},
|
|
439
|
+
clearBackoff(key) {
|
|
440
|
+
backoffs.delete(key);
|
|
441
|
+
},
|
|
442
|
+
drain() {
|
|
443
|
+
if (draining)
|
|
444
|
+
return;
|
|
445
|
+
draining = true;
|
|
446
|
+
emitTransition({ reason: "drain-requested", type: "drain" });
|
|
310
447
|
},
|
|
311
448
|
async dispose() {
|
|
312
449
|
if (disposed)
|
|
@@ -316,9 +453,9 @@ var createRuntime = (options) => {
|
|
|
316
453
|
clearInterval(sweepTimer);
|
|
317
454
|
sweepTimer = undefined;
|
|
318
455
|
}
|
|
319
|
-
const snapshot = [...entries.
|
|
456
|
+
const snapshot = [...entries.entries()];
|
|
320
457
|
entries.clear();
|
|
321
|
-
await Promise.all(snapshot.map((entry) =>
|
|
458
|
+
await Promise.all(snapshot.map(([_key, entry]) => killChildWithReason(entry, "disposed")));
|
|
322
459
|
}
|
|
323
460
|
};
|
|
324
461
|
};
|
|
@@ -326,5 +463,5 @@ export {
|
|
|
326
463
|
createRuntime
|
|
327
464
|
};
|
|
328
465
|
|
|
329
|
-
//# debugId=
|
|
466
|
+
//# debugId=6E6CE2813AF8623B64756E2164756E21
|
|
330
467
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
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"
|
|
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 * Hibernation strategy (per STRATEGY-CLOUD.md §9.5): idle-kill at the\n * process layer. Bun has no shipped process-level snapshot/resume\n * primitive as of 2026-05-29. 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 * observeIntervalMs: 30_000,\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, draining }\n * await runtime.dispose();\n * ```\n */\n\nimport type { Subprocess } from \"bun\";\n\nexport type TenantSource =\n | { kind: \"directory\"; root: string };\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 | {\n type: \"spawn\";\n key: string;\n pid: number;\n port: number;\n durationMs: number;\n }\n | {\n /** Periodic observation emitted by the sweeper (Linux-only; see `observeIntervalMs`). */\n type: \"observation\";\n key: string;\n pid: number;\n /** Cumulative CPU ms used by the child since spawn, derived from `/proc/<pid>/stat`. */\n cpuMs: number;\n /** Resident set size in bytes, derived from `/proc/<pid>/status` VmRSS. */\n rssBytes: number;\n at: number;\n };\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\n/**\n * Why a tenant process ended. Used by `RuntimeTransitionEvent` of type\n * `'exit'` to give the consumer enough info to charge or restart correctly:\n * - `crashed` — the process exited on its own with a non-zero code\n * - `exited-clean` — the process exited 0 (probably a graceful self-stop)\n * - `idle-killed` — the sweeper killed it after `idleAfterMs` with no `touch()`\n * - `lru-evicted` — `ensure()` for a new tenant evicted this one\n * - `killed` — explicit `runtime.kill(key)` call\n * - `readiness-timeout` — readiness check failed; we killed during spawn\n * - `disposed` — `runtime.dispose()` killed it\n * - `restarted` — `runtime.restart(key)` killed it on purpose\n */\nexport type ExitReason =\n | \"crashed\"\n | \"exited-clean\"\n | \"idle-killed\"\n | \"lru-evicted\"\n | \"killed\"\n | \"readiness-timeout\"\n | \"disposed\"\n | \"restarted\";\n\nexport type RuntimeTransitionEvent =\n | { type: \"spawn\"; key: string; pid: number; port: number }\n | {\n type: \"ready\";\n key: string;\n pid: number;\n port: number;\n durationMs: number;\n }\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 | {\n type: \"exit\";\n key: string;\n pid: number;\n exitCode: number | null;\n reason: ExitReason;\n }\n | {\n /** A spawn was deferred because the key is in the back-off window after a failure. */\n type: \"backoff\";\n key: string;\n attempt: number;\n retryAfterMs: number;\n }\n | { type: \"drain\"; reason: \"drain-requested\" };\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\nexport type SpawnBackoff = {\n /** First retry waits this long. Default 1000 ms. */\n baseMs?: number;\n /** Maximum back-off (the cap on the doubled wait). Default 60_000 ms. */\n maxMs?: number;\n /** After this many consecutive failures, `ensure()` throws immediately for this key until reset. Default 10. */\n maxFailures?: number;\n};\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 * How often the sweeper observes CPU + RSS per running tenant. Default\n * 30_000 ms. Set to `0` to disable; observation only works on Linux\n * (`/proc/<pid>` derived) — the sweeper silently skips on other OSes.\n * Output goes to `onMetrics` as `{ type: 'observation', ... }`.\n */\n observeIntervalMs?: number;\n /**\n * Override the readiness check. Default: HTTP GET to\n * `http://127.0.0.1:${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 /** Exponential-backoff policy for consecutive spawn failures. */\n backoff?: SpawnBackoff;\n /** Operational metrics — spawn/ready durations + periodic observations. */\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/backoff/drain. */\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 /** True when the runtime is draining — refusing new ensure() calls. */\n draining: boolean;\n /** Number of keys currently in the back-off window. */\n backoff: 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 * If `key` is in the back-off window after a recent failure, throws\n * immediately (without spawning). Use `clearBackoff(key)` to retry early.\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 /**\n * Kill `key` and respawn it. Used by deploys to swap to a new release\n * after the `current` symlink has been updated. Concurrent restart\n * calls for the same key share a single-flight respawn.\n */\n restart: (key: string) => Promise<Tenant>;\n /** Forget any consecutive-failure state for `key`. Next `ensure()` retries immediately. */\n clearBackoff: (key: string) => void;\n /**\n * Begin draining: refuse new `ensure()` calls (they throw immediately).\n * In-flight spawns and existing tenants are untouched — wait for\n * `stats().running` to reach 0, or call `dispose()` for hard shutdown.\n * Useful for graceful shard shutdown before a host reboot.\n */\n drain: () => 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 /** Set by code that's about to kill the child, read by the exit handler. */\n pendingExitReason: ExitReason | null;\n};\n\ntype BackoffState = {\n attempt: number;\n retryAt: number;\n lastError: string;\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 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\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 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 isLinux = typeof process !== \"undefined\" && process.platform === \"linux\";\n\n/**\n * Read CPU + RSS for a pid from `/proc`. Returns `null` if the pid is gone\n * or we're not on Linux. The math: `utime + stime` from `/proc/<pid>/stat`\n * is in clock ticks; we divide by `Bun.clockTicksPerSecond` (or fall back\n * to 100 — the universal default for Linux kernels).\n */\nconst readProcStats = async (pid: number): Promise<{ cpuMs: number; rssBytes: number } | null> => {\n if (!isLinux) return null;\n try {\n const statText = await Bun.file(`/proc/${pid}/stat`).text();\n const statusText = await Bun.file(`/proc/${pid}/status`).text();\n // /proc/<pid>/stat: ... (comm) ... and utime/stime are fields 14 and 15\n // counting from 1; but `comm` can contain spaces, so we anchor on the\n // closing paren.\n const closeParen = statText.lastIndexOf(\")\");\n if (closeParen === -1) return null;\n const after = statText.slice(closeParen + 2).split(\" \");\n // After (comm), the fields are: state ppid pgrp session ... utime stime ...\n // utime = field 14 of the whole line = index (14 - 3 - 1) = 10 of `after`.\n const utime = Number(after[11]);\n const stime = Number(after[12]);\n if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;\n const ticksPerSec = (globalThis as { Bun?: { clockTicksPerSecond?: number } }).Bun?.clockTicksPerSecond ?? 100;\n const cpuMs = ((utime + stime) / ticksPerSec) * 1000;\n const match = statusText.match(/^VmRSS:\\s+(\\d+)\\s+kB/m);\n const rssBytes = match && match[1] ? Number(match[1]) * 1024 : 0;\n return { cpuMs, rssBytes };\n } catch {\n return null;\n }\n};\n\nconst defaultSpawn = (command: readonly string[]): SpawnFn => async ({\n cwd,\n env,\n onLogLine,\n key,\n}) => {\n const child = Bun.spawn({\n cmd: [...command],\n cwd,\n env,\n stderr: \"pipe\",\n stdout: \"pipe\",\n });\n\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 observeIntervalMs = options.observeIntervalMs ?? 30_000;\n const readiness = options.readiness ?? defaultReadiness;\n const command = options.command ?? [\"bun\", \"run\", \"start\"];\n const spawn = options.spawn ?? defaultSpawn(command);\n const onMetrics = options.onMetrics;\n const onLog = options.onLog;\n const onTransition = options.onTransition;\n const backoffOptions: Required<SpawnBackoff> = {\n baseMs: options.backoff?.baseMs ?? 1_000,\n maxFailures: options.backoff?.maxFailures ?? 10,\n maxMs: options.backoff?.maxMs ?? 60_000,\n };\n\n const entries = new Map<string, Entry>();\n const backoffs = new Map<string, BackoffState>();\n /** Pending exit reasons keyed by child pid — read by the .then exit handler. */\n const exitReasons = new Map<number, ExitReason>();\n let sweepTimer: ReturnType<typeof setInterval> | undefined;\n let lastObserveAt = 0;\n let disposed = false;\n let draining = 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 killChildWithReason = async (entry: Entry, reason: ExitReason): Promise<void> => {\n const child = entry.child;\n if (child === null) return;\n entry.pendingExitReason = reason;\n exitReasons.set(child.pid, reason);\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, reason: ExitReason): Promise<void> => {\n entries.delete(key);\n await killChildWithReason(entry, reason);\n };\n\n const recordBackoff = (key: string, error: unknown): void => {\n const prev = backoffs.get(key);\n const attempt = (prev?.attempt ?? 0) + 1;\n const wait = Math.min(backoffOptions.maxMs, backoffOptions.baseMs * 2 ** (attempt - 1));\n const message = error instanceof Error ? error.message : String(error);\n backoffs.set(key, { attempt, lastError: message, retryAt: Date.now() + wait });\n };\n\n const observeRunning = async (): Promise<void> => {\n if (!isLinux || observeIntervalMs <= 0 || onMetrics === undefined) return;\n const now = Date.now();\n if (now - lastObserveAt < observeIntervalMs) return;\n lastObserveAt = now;\n for (const [key, entry] of entries) {\n if (entry.tenant === null) continue;\n const stats = await readProcStats(entry.tenant.pid);\n if (stats === null) continue;\n emitMetric({\n at: now,\n cpuMs: stats.cpuMs,\n key,\n pid: entry.tenant.pid,\n rssBytes: stats.rssBytes,\n type: \"observation\",\n });\n }\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, \"idle-killed\").catch(() => {});\n }\n }\n void observeRunning().catch(() => {});\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;\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, \"lru-evicted\").catch(() => {});\n }\n };\n\n const spawnFresh = async (key: string): Promise<Tenant> => {\n if (disposed) throw new Error(\"runtime has been disposed\");\n if (draining) throw new Error(\"runtime is draining; ensure() refused\");\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 let child: Subprocess;\n try {\n child = await spawn({\n cwd,\n env,\n key,\n onLogLine: emitLog,\n });\n } catch (error) {\n entries.delete(key);\n recordBackoff(key, error);\n throw error;\n }\n\n emitTransition({ key, pid: child.pid, port, type: \"spawn\" });\n\n // Reap the entry when the process exits. We capture the entry's\n // `pendingExitReason` if some code path set one; otherwise classify\n // by exit code.\n void child.exited\n .then((exitCode) => {\n const stashed = exitReasons.get(child.pid);\n const reason: ExitReason =\n stashed ?? (exitCode === 0 ? \"exited-clean\" : \"crashed\");\n exitReasons.delete(child.pid);\n emitTransition({\n exitCode: exitCode ?? null,\n key,\n pid: child.pid,\n reason,\n type: \"exit\",\n });\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 const entry = entries.get(key);\n if (entry !== undefined) {\n entry.pendingExitReason = \"readiness-timeout\";\n }\n try {\n child.kill();\n } catch {\n /* ignore */\n }\n entries.delete(key);\n recordBackoff(key, error);\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 backoffs.delete(key);\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 const checkBackoff = (key: string): void => {\n const state = backoffs.get(key);\n if (state === undefined) return;\n if (state.attempt >= backoffOptions.maxFailures) {\n throw new Error(\n `Tenant \"${key}\" exceeded ${backoffOptions.maxFailures} consecutive spawn failures; clearBackoff() to retry. Last error: ${state.lastError}`,\n );\n }\n const remaining = state.retryAt - Date.now();\n if (remaining > 0) {\n emitTransition({\n attempt: state.attempt,\n key,\n retryAfterMs: remaining,\n type: \"backoff\",\n });\n throw new Error(\n `Tenant \"${key}\" is backing off after ${state.attempt} failure(s); retry in ${remaining}ms. Last error: ${state.lastError}`,\n );\n }\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 // From here we'd spawn a fresh process — drain only refuses NEW spawns.\n if (draining) throw new Error(\"runtime is draining; ensure() refused\");\n checkBackoff(key);\n const fresh: Entry = {\n child: null,\n key,\n pending: null,\n pendingExitReason: 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 { backoff: backoffs.size, draining, 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, \"killed\");\n },\n\n async restart(key) {\n if (disposed) throw new Error(\"runtime has been disposed\");\n const entry = entries.get(key);\n if (entry !== undefined) {\n await removeEntry(key, entry, \"restarted\");\n }\n // Same single-flight contract as ensure().\n const fresh: Entry = {\n child: null,\n key,\n pending: null,\n pendingExitReason: 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 clearBackoff(key) {\n backoffs.delete(key);\n },\n\n drain() {\n if (draining) return;\n draining = true;\n emitTransition({ reason: \"drain-requested\", type: \"drain\" });\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.entries()];\n entries.clear();\n await Promise.all(snapshot.map(([_key, entry]) => killChildWithReason(entry, \"disposed\")));\n },\n };\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "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",
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "6E6CE2813AF8623B64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/runtime",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
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
5
|
"repository": {
|
|
6
6
|
"type": "git",
|