Tryst::BackgroundWork(Data, Result)
Background work built on Fiber::ExecutionContext::Isolated + Channel, in place of ruby-tryst's Thread/Thread::Queue - the queue_for_main cross-context pattern already established elsewhere in this port (see Interp#queue_for_main) generalizes directly to a dedicated worker-to-main channel plus App#after-driven polling instead of a single shared queue.
Unified: no mode: argument, no register_background_mode pluggable system, no Ractor variant (ruby-tryst's background_ractor4x.rb/ ractor_support.rb are dropped entirely, per the epic's agreed simplification) - this is the only implementation.
For CPU-bound work only. Isolated buys a real second OS thread, which
is what a busy fiber needs to not starve the default context's other
fibers (Tk's mainloop included) - but for IO-bound work (an HTTP
fetch, file/socket IO through Crystal's IO layer), a plain spawn
fiber already runs alongside mainloop with no thread at all: see
examples/fiber_io_demo.cr and the README's Concurrency section. The
work block below never gets a way to touch Tk/widget/Var state
directly, for the same reason: it runs on a different OS thread, and
that state has no locking around it. Only on_progress/on_done/
on_message/on_error - invoked back on the main thread from #poll -
may touch it, same rule a plain spawn fiber's body already satisfies
by virtue of sharing mainloop's own thread.
@example task = Tryst::BackgroundWork.new(app, data) do |t, d| d.each do |item| break if t.check_message == Tryst::BackgroundControl::Stop t.yield(process(item)) end end.on_progress { |r| update_ui(r) } .on_done { puts "Done!" }
task.send_message("pause") task.pause task.resume task.stop
Constants
Default for close_drain_timeout below.
A floor under close_drain_timeout - below this the drain loop could give up and mark the task done while a perfectly cooperative worker is still on its way to its next #check_message, turning an honest wait into a false "done".
Constructors
Crystal doesn't support a generic (parameterized) alias, and a class-level alias can't see its own enclosing generic class's type param either (both confirmed directly) - so the output event union is spelled out at each of its three use sites (the two channel declarations and #dispatch_event) instead of being named once. Adding a member means updating all three; #dispatch_event's exhaustive case then points at any branch still missing.
Class methods
When true, only the latest progress value per poll cycle is delivered (default true) - prevents UI choking when the worker yields faster than the UI polls.
When true, only the latest progress value per poll cycle is delivered (default true) - prevents UI choking when the worker yields faster than the UI polls.
UI poll interval in milliseconds (default 16). Crystal class variables in a generic class are shared across every concrete instantiation, not per-instantiation (confirmed directly) - so this is one process-wide setting regardless of task type, matching ruby-tryst's Tryst::BackgroundWork.poll_ms exactly, including the bare (no type argument) Tryst::BackgroundWork.poll_ms = call syntax.
UI poll interval in milliseconds (default 16). Crystal class variables in a generic class are shared across every concrete instantiation, not per-instantiation (confirmed directly) - so this is one process-wide setting regardless of task type, matching ruby-tryst's Tryst::BackgroundWork.poll_ms exactly, including the bare (no type argument) Tryst::BackgroundWork.poll_ms = call syntax.
Instance methods
Crystal has no equivalent of Ruby's Thread#kill - there is no hard-kill primitive for a fiber/execution context, so this can only ask the worker to stop cooperatively via the same message #stop uses. Unlike #stop, though, #close also marks the poll chain as closing: #poll keeps draining @output_queue (discarding what it drains, no callbacks) instead of returning early the way an immediate @done = true used to make it. A worker that's still yielding when #close is called would otherwise fill the bounded output_queue and block forever inside #yield, never reaching the #check_message call that would have seen this Stop - close draining the queue keeps that from happening.
@done itself only flips once the worker's own BackgroundDone arrives (or close_drain_timeout elapses without one, for a worker that never calls #check_message/#check_pause at all - a bare infinite loop keeps running in the background regardless, a real, deliberate deviation, not a silent gap: Ruby's true kill has no Crystal analogue), so #done? reflects the worker actually having stopped rather than the moment #close was called.
How long #close's drain loop keeps polling for the worker's BackgroundDone before giving up and marking the task done anyway - only reached by a worker that never calls #check_message/ #check_pause, since a cooperative one sees the queued Stop on its very next check. Per-instance rather than global, since how long that's worth waiting depends on how the specific work block is written (how far apart its #check_message calls are).
Called when the work block raises. text is "ExceptionClass: message" plus a trimmed backtrace - the exception object itself never crosses the thread boundary, so this is the payload, not a Crystal Exception. #on_done still fires afterward (the worker's rescue sends BackgroundFailure then BackgroundDone); done? is true either way. With no handler set, a failure prints to STDERR instead - the pre-#on_error behavior, unchanged for existing code. A failure that arrives after #close is silently dropped, same as every other event #close's drain discards - the caller already asked to tear this down, not to be told how it ended.
Cancels the armed poll (if any) as well as sending Pause - #poll stops re-arming itself once @paused, but the poll already armed before this call would otherwise still fire once, see @paused == false (this hadn't taken effect on the worker side yet) and re-arm anyway, racing whatever #resume arms next. See #arm_poll.
A no-op unless currently paused, so calling #resume twice (or #resume racing a #pause that hasn't landed) can't arm a second, never-converging poll chain alongside whatever's already running - see #arm_poll.
Send a message to the worker (BackgroundControl::Pause/Resume/Stop, or a custom String).