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.
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]}`)
}