spawn
Signature: spawn<T>(fn: () => T): Promise<T> — imported from perry/thread.
Runs a closure on a new OS thread and returns a Promise that resolves when the thread completes. The main thread continues immediately — UI and other work are not blocked.
Basic Usage
async function spawnBasic(): Promise<void> {
const result = await spawn(() => {
// This runs on a separate OS thread.
let sum = 0
for (let i = 0; i < 100_000_000; i++) {
sum += i
}
return sum
})
console.log(result) // 4999999950000000
}
Non-Blocking
spawn returns immediately. The main thread doesn’t wait:
async function spawnNonBlocking(): Promise<void> {
console.log("1. Starting background work")
const handle = spawn(() => {
// Runs on a background thread — heavier work elided here.
let n = 0
for (let i = 0; i < 10_000_000; i++) n++
return n
})
console.log("2. Main thread continues immediately")
const result = await handle
console.log(`3. Got result: ${result}`)
}
Output:
1. Starting background work
2. Main thread continues immediately
3. Got result: <computed value>
Multiple Concurrent Tasks
Spawn multiple tasks and they run truly concurrently — one OS thread per spawn call:
async function spawnMultiple(): Promise<void> {
const t1 = spawn(() => analyseChunk(0, 1_000_000))
const t2 = spawn(() => analyseChunk(1_000_000, 2_000_000))
const t3 = spawn(() => analyseChunk(2_000_000, 3_000_000))
// All three run simultaneously on separate OS threads.
const results = await Promise.all([t1, t2, t3])
console.log(`Region A: ${results[0]}`)
console.log(`Region B: ${results[1]}`)
console.log(`Region C: ${results[2]}`)
}
function analyseChunk(start: number, end: number): number {
let acc = 0
for (let i = start; i < end; i++) acc += i & 0xff
return acc
}
Unlike Node.js worker_threads, each spawn is a lightweight OS thread (~8MB stack), not a full V8 isolate (~2MB heap + startup cost).
Capturing Variables
Like parallelMap, spawn closures can capture outer variables. They are deep-copied to the background thread:
async function spawnCapture(): Promise<void> {
const config = { iterations: 1000, seed: 42 }
const dataset = [1, 2, 3, 4, 5, 6, 7, 8]
const result = await spawn(() => {
// config and dataset are deep-copied to this thread.
let acc = config.seed
for (let i = 0; i < config.iterations; i++) {
acc = (acc * 1103515245 + 12345) & 0x7fffffff
}
for (const v of dataset) acc ^= v
return acc
})
console.log(`spawn-capture: ${result}`)
}
Mutable variables cannot be captured — this is enforced at compile time.
File System Handles
Do not capture numeric fds or fs.promises.FileHandle objects for file I/O in
spawn. Perry’s fd registry is per thread: a numeric fd captured from the main
thread is not open in the worker, and a captured FileHandle arrives detached
with fd === -1. Capture a path string instead, then call fs.openSync or
fs.promises.open inside the spawned function.
Returning Complex Values
spawn can return any value type. Complex values (objects, arrays, strings) are serialized back to the main thread automatically:
async function spawnComplexReturn(): Promise<void> {
const stats = await spawn(() => {
const values = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]
let sum = 0
let max = values[0]
let min = values[0]
for (const v of values) {
sum += v
if (v > max) max = v
if (v < min) min = v
}
return {
mean: sum / values.length,
min,
max,
count: values.length,
}
})
console.log(`mean=${stats.mean} min=${stats.min} max=${stats.max} count=${stats.count}`)
}
UI Integration
spawn is ideal for keeping native UIs responsive during heavy computation:
const analyzeButton = Button("Analyze", async () => {
status.set("Processing...")
// Background thread — UI stays responsive
const data = await spawn(() => {
let count = 0
for (let i = 0; i < 1_000_000; i++) {
if ((i & 0xff) === 0) count++
}
return { count }
})
result.set(`Found ${data.count} patterns`)
status.set("Done")
})
Without spawn, the analysis would freeze the UI. With spawn, the user can still scroll, tap other buttons, or navigate while the computation runs.
Compared to Node.js worker_threads
// ── Node.js: ~15 lines, separate file needed ──────────
// worker.js
const { parentPort, workerData } = require("worker_threads");
const result = heavyComputation(workerData);
parentPort.postMessage(result);
// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js", {
workerData: inputData,
});
worker.on("message", (result) => {
console.log(result);
});
worker.on("error", (err) => { /* handle */ });
// ── Perry: 1 line ─────────────────────────────────────
// const result = await spawn(() => heavyComputation(inputData));
No separate files. No message ports. No event handlers. No structured clone. One line.
Examples
Background File Processing
async function spawnBgFile(): Promise<void> {
// Read and process a "large" file without blocking. We inline a tiny CSV
// so the snippet runs hermetically — the docs' real version would call
// readFileSync from "fs".
const content = "id,value\n1,10\n2,20\n3,30\n"
const analysis = await spawn(() => {
const lines = content.split("\n").filter((l: string) => l.length > 0).slice(1)
let total = 0
for (const line of lines) {
const parts = line.split(",")
total += parseInt(parts[1], 10)
}
return { rows: lines.length, total }
})
console.log(`spawn-bg-file rows=${analysis.rows} total=${analysis.total}`)
}
Parallel API Calls with Processing
async function spawnApiThenProcess(): Promise<void> {
// The docs example fetches a remote API; for a hermetic test we
// just hand-roll the same pipeline shape with synthetic data.
const rawData = { items: [1, 2, 3, 4, 5] }
// CPU-intensive processing happens off the main thread
const processed = await spawn(() => {
let total = 0
for (const v of rawData.items) total += v * v
return { total, count: rawData.items.length }
})
console.log(`spawn-api-then-process total=${processed.total} count=${processed.count}`)
}
Deferred Computation
async function spawnDeferred(): Promise<void> {
const params = { size: 8 }
// Start computation early, use result later
const precomputed = spawn(() => {
const table: number[] = []
for (let i = 0; i < params.size; i++) table.push(i * i)
return table
})
// ... do other setup work ...
// Result is ready (or we wait for it)
const table = await precomputed
console.log(`spawn-deferred len=${table.length} last=${table[table.length - 1]}`)
}