class

Tryst::BackgroundWork(Data, Result)

Inherits Reference < Object

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_CLOSE_DRAIN_TIMEOUT = 5.seconds

Default for close_drain_timeout below.

MIN_CLOSE_DRAIN_TIMEOUT = 1.second

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

new(app : App, data : Data, close_drain_timeout : Time::Span = DEFAULT_CLOSE_DRAIN_TIMEOUT, &work_block : TaskContext(Result), Data -> Nil)

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.

Source

Class methods

drop_intermediate

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.

Source
drop_intermediate=(drop_intermediate : Bool)

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.

Source
poll_ms

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.

Source
poll_ms=(poll_ms : Int32)

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.

Source

Instance methods

close

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.

Source
close_drain_timeout

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).

Source
done?
Source
on_done
Source
on_error

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.

Source
on_message

Called when the worker sends a non-result message back.

Source
on_progress
Source
pause

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.

Source
paused?
Source
resume

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.

Source
send_message(msg : BackgroundMessage) : self

Send a message to the worker (BackgroundControl::Pause/Resume/Stop, or a custom String).

Source
start
Source
stop
Source