# defworker Spawn a persistent worker — Deft's answer to Node's `worker_threads`. ## Syntax
[defworker "path.dft"]            # → worker handle
[defworker "path.dft" 4]          # → pool of 4 workers (round-robin)
## How it differs from `deftask` `deftask { body }` snapshots the closure's bytecode and referenced globals into a fresh one-shot runtime — the body must be self-contained (literals, globals, and native stdlib functions only; captured locals arrive as `nil`). `defworker` loads a file into a long-lived child runtime on its own OS thread. The worker file gets its full module graph: user-defined functions, imports, top-level state — everything works, exactly like Node workers. The worker survives between jobs, so state set at load time (or mutated by handlers) persists. ## Marking handlers (`@worker`) A regular `def` marked with the `@worker` decorator becomes a sendable handler. Unmarked defs stay private to the worker file (callable internally, not reachable from the parent).
# workers/image.dft — top-level code runs at spawn (entry point, state setup)
def config %{ quality 90 }
def resize-bytes {|bytes^string w^number h^number| ... }   # internal helper

def process-image {|bytes^string w^number h^number|
    @worker
    @doc "Resize an image blob on a worker thread."
    [resize-bytes $bytes $w $h]       # full code access
}
## Dispatch
set w   [defworker "workers/image.dft"]
set fut [worker/send $w "process-image" $bytes 640 480]
set out [await $fut]                  # works in coroutines AND plain scripts
[worker/stop $w]
`worker/send` accepts a worker handle or a pool handle; pools round-robin. The result is a normal Future — `await` it (single or via `await-all`). ## Streaming (`worker/stream`) `worker/stream` is the streaming sibling of `worker/send`: `yield` in the handler pushes values incrementally onto a stream instead of returning one final result. Read them with the same `task/stream-*` family `deftask` { @stream ... }` uses:`
def generate {|n^number|
    @worker
    set i 0
    while ($i < $n) {
        incr i
        yield $i
    }
}

set s   [worker/stream $w "generate" 100]
set row [task/stream-next $s]
while (not [nil? $row]) {
    # ... process $row as it arrives ...
    set row [task/stream-next $s]
}
# or just: def rows [task/stream-collect $s]
- The handler's `yield` values cross the heap boundary deep-copied, like job results — structured payloads (maps, vectors, binary strings) work. - The handler's final return value is dropped; a handler error ends the stream (earlier yields survive) and the worker keeps running — the same contract as `deftask { @stream ... }`. - Backpressure is end-to-end: the worker blocks on a full result mailbox until the consumer drains the stream. - Stopping a worker mid-stream finishes any open streams — consumers see `nil`/done rather than hanging. - Calling `worker/stream` on a handler that never yields returns an immediately-empty stream. ### Streams implement ISeq A stream is seqable: `map`/`filter`/`take`/`drop`/`reduce`/`for` treat it like a lazy sequence, pulling items incrementally (with the runtime's worker drains pumped while waiting). This composes with pipelines:
# single item (plain call in pipeline position)
set one $s |> task/stream-next

# whole stream, transformed lazily
set evens $s |> filter {|x| ([mod $x 2] == 0)} |> take 10 |> collect

# or bracket form
set evens [collect [take 10 [filter $s {|x| ([mod $x 2] == 0)}]]]

# or a for loop over the stream directly
for x in $s { ... }
Note: pipelines (`|>`) parse at statement position only, not inside `[...]` — use the bracket forms there. `deftask { @stream ... }` streams are seqable the same way. ## Stdlib functions | Function | Purpose | |-----------|---------| | `[defworker "path.dft"]` | Spawn a worker; returns a handle | | `[defworker "path.dft" n]` | Spawn a pool of n workers; returns a pool handle | | `[worker/send $w "name" args...]` | Dispatch a job; returns a Future | | `[worker/stream $w "name" args...]` | Dispatch a streaming job; `yield` in the handler pushes values onto the returned stream | | `[worker/stop $w]` | Stop accepting, drain queued jobs, join, free the child runtime | | `[worker/jobs $w]` | Pending job count (queued + in-flight) | | `[worker/handlers $w]` | List of `@worker`-marked handler names | ## Value passing Job arguments and results are deep-copied across the heap boundary (structured values: strings, keywords, vectors, sets, maps, tagged maps). There is no serialization — copies are structural, so binary strings (image bytes) cross at memcpy cost. Return data only: functions, futures, and resources degrade to their display strings (as with `deftask`). ## Error semantics A handler throw resolves the job's future with `%Err{:message "..."}` — and the worker keeps running (persistent). Check for the error shape:
set v [await $fut]
if [is? "result" $v] {           # Ok/Err tagged maps report type "result"
    echo "job failed:" $v~message
}
Unknown handler names are rejected at `worker/send` time with `%Err`. ## Backpressure Each worker has bounded job/result mailboxes (64 slots). `worker/send` blocks when a worker's queue is full — a natural backpressure valve. ## Shutdown `[worker/stop $w]` stops accepting jobs, drains queued ones (their futures resolve normally), joins the worker thread, and frees the child runtime. If the parent runtime is torn down with workers still alive, teardown cascades automatically. Stale handles fail cleanly (`worker/send` on a stopped worker returns `%Err`). ## Notes - `[await $f1 $f2]` (multi-arg form) blocks the runtime and must not be used with worker futures; use single `[await $f]` or `await-all`. - Worker→parent progress reporting: workers share the parent's pub/sub broker, so a worker can `pub/publish` on a per-worker topic and the parent `pub/subscribe`s it. - Job cancellation and timeouts are not yet implemented. ## See Also `deftask`, `await`, `await-all` (stdext/async), `defcoro`, `pub/publish`