Task manager
taskManager.run/cancel, priority queues, alerting, onResult.
A built-in global singleton that throttles how many Roku Task nodes run at
once, app-wide (RokuOS's own soft/hard concurrency limits). taskManager never
creates a Task itself — you construct it the ordinary way and hand it off.
run / cancel / concurrency
taskManager.run(node, [priority]) starts (or queues, FIFO within
"high"/"normal"/"low"
tiers) and returns a task id for cancel(id). Defaults to a concurrency budget
of 50 (below Roku's hard 100 limit, leaving headroom for tasks started outside this compiler entirely).
A task started from an ordinary component is auto-cancelled the moment that component is torn down
(a navigation away, {#if:destroy}, or an
{#each} removal) — keeping the id and calling
cancel(id) yourself is only needed to stop a task earlier than that.
private function startWork(key: string, press: boolean) {
if (press) {
task = CreateObject("roSGNode", "MyDownloadTask")
task.url = "https://example.com/data.json"
taskId = taskManager.run(task) ' priority: "normal"
urgentTask = CreateObject("roSGNode", "MyDownloadTask")
urgentId = taskManager.run(urgentTask, "high")
}
}
public function setup() {
taskManager.setMaxConcurrent(75) ' typically called once, from the root Scene's own setup()
}Queue-depth alerting
onAlertChanged fires only on a real hysteresis-gated crossing
("none"/"warning"/"critical") —
not on every queue mutation — so a fluctuating queue near a threshold doesn't spam a subscriber.
public function setup() {
taskManager.setAlertThresholds({ warning: 30, critical: 50 })
taskManager.onAlertChanged(onQueueAlert)
}
private function onQueueAlert(level: string) {
if (level == "critical") {
' forward level, taskManager.runningCount, taskManager.queuedCount to reporting
}
}Promise-style request consumption
taskManager.onResult(task, onSuccess, [onError]) — sugar over hand-wiring
observeFieldScoped on a request Http
task's own result/error fields. Pass the
task node itself, not the id.
private function loadPosts(key: string, press: boolean) {
if (press) {
task = CreateObject("roSGNode", "GetPosts")
taskManager.run(task)
taskManager.onResult(task, onPostsLoaded, onPostsFailed)
}
}
private function onPostsLoaded(result: dynamic) {
' result is exactly what parseResponse(...) returned — already unwrapped
}
private function onPostsFailed(error: dynamic) {
' same shape as parseError(...)'s own return value
}Global request/response interceptors
onRequestSent/onResponseReceived — register
once, anywhere, and see every request Http {} call app-wide, regardless of
which component or screen created it. Useful for one centralized telemetry/logging hook.
public function setup() {
taskManager.onRequestSent(function (sentInfo: dynamic) {
' fires for every request Http {} component in the app, app-wide
' ("request" itself is a reserved DSL keyword, so the parameter needs another name)
})
taskManager.onResponseReceived(function (response: dynamic) {
' the RAW ft_httpFetch response, plus parseSucceeded/parseErrorMessage
})
}Reference implementation — apps/task-manager-demo
Every mechanism on this page has a router-mounted, scaled
chapter in apps/task-manager-demo — 4 chapters
(/run-cancel through /interceptors),
reachable with REWIND/FAST-FORWARD once compiled and sideloaded. Each chapter shows a default
example alongside a deliberately different, customized one — e.g.
RunCancelDemo.thr's priority burst at
maxConcurrent=1 next to the same burst re-run at
maxConcurrent=3, directly contrasting which tasks win an
immediate slot against which ones only ever affect queue drain order. See
findings/task-manager-demo-app.md for what each chapter
covers and findings/demo-app-conventions.md for the
app-structure convention it follows.
⚠️ Not (yet) supported
- ○ A task started from a .flsh class body has no auto-cancel — a class instance has no node of its own for the owning component's teardown hook to key off of, so keep the returned id and call
cancel(id)yourself. - ○ Task nodes created outside
.thr/.flsh(hand-written BrightScript) are invisible to the manager unless explicitly routed throughrun(...). - ○ No preemption — a burst of
"high"-priority work still waits for already-running"low"-priority tasks to finish naturally. - ○ Re-prioritizing an already-queued task — calling
run(...)again with a different priority has no effect once queued. - ○
onAlertChanged/onResult/onRequestSent/onResponseReceivedare not callable from a .flsh class body — call them from the owning.thrcomponent instead. - ○ No timeout for a task whose own
statenever leaves"init"— a hung task occupies a concurrency slot indefinitely, with no built-in watchdog to reclaim it. - ○
runningCount/queuedCount/alertLevelare plain snapshots, notwatch-able — a live "N tasks running" display has to poll rather than bind reactively.
Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.