@supermachine/core 0.4.25

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.
@@ -0,0 +1,54 @@
1
+ // 02-pool.mjs — keep a warm pool, dispatch N concurrent commands.
2
+ //
3
+ // Demonstrates:
4
+ // - `min`/`max` pool sizing (eager pre-spawn + auto-grow)
5
+ // - Concurrent acquire(): pool serializes correctly under load
6
+ // - `pool.stats()` for observability / autoscaling decisions
7
+ //
8
+ // Run:
9
+ // node examples/02-pool.mjs
10
+
11
+ import { Image } from "@supermachine/core";
12
+
13
+ const img = await Image.build({
14
+ ref: "alpine:latest",
15
+ name: "pool-demo",
16
+ });
17
+
18
+ // Pre-spawn 4 VMs; grow up to 8 under load.
19
+ const pool = await img.pool({
20
+ min: 4,
21
+ max: 8,
22
+ // Per-cycle clean state on release. Trade ~3 ms cycle-restore
23
+ // for guaranteed fresh /tmp etc. across acquires.
24
+ restoreOnRelease: true,
25
+ });
26
+
27
+ console.log("pool stats (idle):", pool.stats());
28
+ // → { alive: 4, inUse: 0, idle: 4, waiting: 0, min: 4, max: 8 }
29
+
30
+ // Launch 16 concurrent commands; pool will serve from 4 idle
31
+ // slots, grow to 8, then queue the remaining 8 fairly.
32
+ const work = Array.from({ length: 16 }, async (_, i) => {
33
+ const vm = await pool.acquire();
34
+ try {
35
+ const { stdout } = await vm.exec({
36
+ argv: ["sh", "-c", `echo "task ${i} on $(hostname)"`],
37
+ timeoutMs: 5_000,
38
+ });
39
+ return stdout.toString().trim();
40
+ } finally {
41
+ await vm.release();
42
+ }
43
+ });
44
+
45
+ // Snapshot stats mid-flight so the user can see the queue depth.
46
+ setTimeout(() => {
47
+ console.log("pool stats (mid-flight):", pool.stats());
48
+ }, 50);
49
+
50
+ const outputs = await Promise.all(work);
51
+ console.log("\nall results:");
52
+ for (const line of outputs) console.log(" ", line);
53
+
54
+ console.log("\npool stats (drained):", pool.stats());
@@ -0,0 +1,54 @@
1
+ // 03-snapshot-then-restore.mjs — capture mid-flight VM state,
2
+ // reload it later for instant warm starts.
3
+ //
4
+ // The story: you have a slow setup phase (compile a seed program,
5
+ // warm a page cache, fetch a model). Do it ONCE, snapshot, then
6
+ // restore the post-warmup state on every subsequent run in <50 ms.
7
+ //
8
+ // Run:
9
+ // node examples/03-snapshot-then-restore.mjs
10
+
11
+ import { Image } from "@supermachine/core";
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ const img = await Image.build({
17
+ ref: "alpine:latest",
18
+ name: "snapshot-demo",
19
+ });
20
+
21
+ // Phase 1: do expensive setup in a regular VM.
22
+ const pool = await img.pool({ min: 1, max: 1 });
23
+ const vm = await pool.acquire();
24
+
25
+ // Simulate setup: drop a "computed" file into the guest. In a real
26
+ // workload this might be `apk add gcc && rustc -O example.rs`.
27
+ await vm.writeFile(
28
+ "/etc/seed.txt",
29
+ Buffer.from("expensive setup result: 0xCAFEBABE\n"),
30
+ );
31
+ console.log("phase 1: setup complete, file written to guest");
32
+
33
+ // Capture the post-setup state. Returns a new Image pointing at
34
+ // a freshly-written restore.snap.
35
+ const captureDir = mkdtempSync(join(tmpdir(), "sm-snap-"));
36
+ const captured = await vm.snapshot(captureDir);
37
+ console.log(`phase 2: snapshot captured (${captured.memoryMib} MiB)`);
38
+ console.log(` path: ${captured.snapshotPath}`);
39
+
40
+ await vm.release();
41
+
42
+ // Phase 3: a brand-new VM from the captured state has the file
43
+ // already there — no need to rerun setup.
44
+ const restored = await Image.fromSnapshot(captured.snapshotPath);
45
+ const pool2 = await restored.pool({ min: 1, max: 1 });
46
+ const vm2 = await pool2.acquire();
47
+
48
+ const seed = await vm2.readFile("/etc/seed.txt");
49
+ console.log("phase 3: restored VM reads back seed →", seed.toString().trim());
50
+
51
+ await vm2.release();
52
+
53
+ // Tidy up the captured snapshot.
54
+ rmSync(captureDir, { recursive: true, force: true });
@@ -0,0 +1,80 @@
1
+ // 04-expose-tcp.mjs — bake a service image, expose it as a local
2
+ // TCP port, connect from the host.
3
+ //
4
+ // Demonstrates the `TcpForwarder` API surface:
5
+ // - `vm.exposeTcp(hostPort, guestPort)` — TCP→vsock proxy
6
+ // - `TcpForwarder.localAddr` getter
7
+ // - `TcpForwarder.stop()` (idempotent; in-flight connections survive)
8
+ //
9
+ // The example bakes an alpine "echo server": `nc -l -p 8080` in a
10
+ // loop. We then connect from the host and verify the forwarder
11
+ // landed at a listening port. Getting bidirectional data through
12
+ // busybox `nc` is finicky across alpine releases — for a real
13
+ // service workload (nginx, redis, your own app), the
14
+ // TcpForwarder API stays identical and the data path works
15
+ // without any of the BusyBox-nc gymnastics here.
16
+ //
17
+ // Run:
18
+ // node examples/04-expose-tcp.mjs
19
+
20
+ import { Image } from "@supermachine/core";
21
+ import { createConnection } from "node:net";
22
+
23
+ const img = await Image.build({
24
+ ref: "alpine:latest",
25
+ name: "expose-tcp-demo",
26
+ cmd: ["sh", "-c", "while true; do nc -l -p 8080; done"],
27
+ guestPort: 8080,
28
+ // Wait for the listener to bind before capturing.
29
+ listenerRequired: true,
30
+ });
31
+
32
+ const pool = await img.pool({ min: 1, max: 1 });
33
+ const vm = await pool.acquire();
34
+
35
+ // hostPort: 0 → OS picks a free port. Read it back via .localAddr.
36
+ // guestPort: 8080 is plumbed through but currently advisory; the
37
+ // Rust crate's expose_tcp routes into whatever the in-guest TSI
38
+ // mux accepts. Per-guest-port routing is on the roadmap; for
39
+ // now, a single guest workload bound to whatever port works.
40
+ const fwd = await vm.exposeTcp(0, 8080);
41
+ console.log(`forwarder bound on ${fwd.localAddr}`);
42
+
43
+ const [host, port] = fwd.localAddr.split(":");
44
+
45
+ // Verify the forwarder is accepting connections — a successful
46
+ // TCP connect (vs ECONNREFUSED) confirms the host-side proxy
47
+ // thread is up and the vsock relay accepted us.
48
+ const connected = await new Promise((resolve) => {
49
+ const sock = createConnection({ host, port: Number(port) });
50
+ sock.once("connect", () => {
51
+ sock.destroy();
52
+ resolve(true);
53
+ });
54
+ sock.once("error", () => resolve(false));
55
+ sock.setTimeout(3_000, () => {
56
+ sock.destroy();
57
+ resolve(false);
58
+ });
59
+ });
60
+ console.log(`connect ok: ${connected}`);
61
+
62
+ await fwd.stop();
63
+ console.log("forwarder stopped");
64
+
65
+ // After stop(), connect must be refused.
66
+ const refused = await new Promise((resolve) => {
67
+ const sock = createConnection({ host, port: Number(port) });
68
+ sock.once("connect", () => {
69
+ sock.destroy();
70
+ resolve(false);
71
+ });
72
+ sock.once("error", () => resolve(true));
73
+ sock.setTimeout(500, () => {
74
+ sock.destroy();
75
+ resolve(true);
76
+ });
77
+ });
78
+ console.log(`post-stop refused: ${refused}`);
79
+
80
+ await vm.release();
@@ -0,0 +1,59 @@
1
+ // 05-spawn-streaming.mjs — bidirectional stdin/stdout/stderr
2
+ // streaming with vm.spawn().
3
+ //
4
+ // Unlike `vm.exec()` which collects all output into Buffers,
5
+ // `vm.spawn()` returns an ExecChild handle for incremental I/O:
6
+ // - writeStdin / closeStdin
7
+ // - readStdout / readStderr (pull-style, returns when bytes
8
+ // are available or 0 on EOF)
9
+ // - signal(signum), resize(cols, rows)
10
+ // - wait() → exit status
11
+ //
12
+ // Useful for: LLM-style token streaming, interactive REPLs,
13
+ // long-running daemons, anything where you don't want to wait
14
+ // for the whole output before processing.
15
+ //
16
+ // Run:
17
+ // node examples/05-spawn-streaming.mjs
18
+
19
+ import { Image } from "@supermachine/core";
20
+
21
+ const img = await Image.build({
22
+ ref: "alpine:latest",
23
+ name: "spawn-stream-demo",
24
+ });
25
+
26
+ const pool = await img.pool({ min: 1, max: 1 });
27
+ const vm = await pool.acquire();
28
+
29
+ // Spawn a process that echoes back each line of stdin with a
30
+ // prefix. The host writes lines incrementally and reads each
31
+ // response as it arrives — no buffering on either side.
32
+ const child = await vm.spawn({
33
+ argv: [
34
+ "sh",
35
+ "-c",
36
+ `while IFS= read -r line; do echo "[guest] $line"; done`,
37
+ ],
38
+ });
39
+
40
+ const inputs = ["hello", "world", "supermachine"];
41
+ for (const line of inputs) {
42
+ await child.writeStdin(Buffer.from(`${line}\n`));
43
+ // Pull one line of output. readStdout returns at first data;
44
+ // we accumulate until we see a \n.
45
+ let acc = "";
46
+ while (!acc.includes("\n")) {
47
+ const chunk = await child.readStdout(1024);
48
+ if (chunk.length === 0) break;
49
+ acc += chunk.toString();
50
+ }
51
+ process.stdout.write(`host wrote "${line}" → ${acc}`);
52
+ }
53
+
54
+ // Close stdin so the shell's `read` returns 0 and the loop exits.
55
+ await child.closeStdin();
56
+ const result = await child.wait();
57
+ console.log(`exit ${result.exitCode}, peak RSS ${result.peakRssKib ?? "?"} KiB`);
58
+
59
+ await vm.release();
@@ -0,0 +1,43 @@
1
+ # `@supermachine/core` examples
2
+
3
+ Runnable scripts demonstrating each integrator-facing surface.
4
+ Every example is self-contained — `node examples/NN-name.mjs`
5
+ with the package installed.
6
+
7
+ | File | Surfaces |
8
+ |---|---|
9
+ | `01-hello.mjs` | `Image.build` + `pool.acquire` + `vm.exec` — the minimum program |
10
+ | `02-pool.mjs` | Pool sizing (`min`/`max`), `pool.stats()`, concurrent acquire |
11
+ | `03-snapshot-then-restore.mjs` | `vm.writeFile` / `readFile`, mid-flight `vm.snapshot()`, `Image.fromSnapshot` |
12
+ | `04-expose-tcp.mjs` | `listenerRequired: true` bakes, `vm.exposeTcp()`, `TcpForwarder` |
13
+ | `05-spawn-streaming.mjs` | `vm.spawn()` → `ExecChild` (writeStdin / readStdout / wait) |
14
+
15
+ ## Requirements
16
+
17
+ - macOS on Apple Silicon (no Linux/Windows backends yet)
18
+ - Docker available for the OCI image pull on first run
19
+ - Node ≥ 18.17
20
+
21
+ ## Running locally from a source checkout
22
+
23
+ The examples import `"@supermachine/core"` as if installed from
24
+ npm. From this repo, run them with a path-rewrite:
25
+
26
+ ```sh
27
+ cd npm/supermachine-core
28
+ npm install
29
+ npm run build
30
+ # then either: `npm link` once, then `node examples/01-hello.mjs`
31
+ # or: rewrite the import inline
32
+ node --input-type=module -e \
33
+ "$(sed 's|@supermachine/core|./index.js|' examples/01-hello.mjs)"
34
+ ```
35
+
36
+ For a quick smoke test that all five work end-to-end:
37
+
38
+ ```sh
39
+ for f in examples/*.mjs; do
40
+ echo "=== $f ===";
41
+ node --input-type=module -e "$(sed 's|@supermachine/core|./index.js|' $f)";
42
+ done
43
+ ```