⚙️

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 through run(...).
  • ○ 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/onResponseReceived are not callable from a .flsh class body — call them from the owning .thr component instead.
  • ○ No timeout for a task whose own state never leaves "init" — a hung task occupies a concurrency slot indefinitely, with no built-in watchdog to reclaim it.
  • ○ runningCount/queuedCount/alertLevel are plain snapshots, not watch-able — a live "N tasks running" display has to poll rather than bind reactively.

Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.