Members

(static, constant) TASK_QUEUE_CAP

Launches that may wait at once; past it launchTask() refuses. A waiting node costs ~4 KB (measured 2026-09-23 on a 512 MB Streaming Stick 3600X: 3.6-4.1 KB for a LoadItemsTask with small inputs), so the full queue is ~2 MB there. ADR 0041.

Default Value
  • 500

(static, constant) TASK_QUEUE_RETRY_MAX_SECONDS

Default Value
  • 0.4

(static, constant) TASK_QUEUE_RETRY_MIN_SECONDS

How soon the queue first retries its head, and the longest it waits between retries while no slot frees. A refused retry costs render-thread time, and a busy render thread also slows the Tasks the queue waits on, so retries back off while nothing can start. The longest wait is also how late a waiting launch can notice a freed slot. Measured 2026-09-23 on a 512 MB Streaming Stick 3600X at the watermark (TaskLedgerBench drainBlocked): a refused retry costs 2.1 ms reading the ledger only, and cost 2.8 ms when it asked admitTaskLaunch, which also writes it twice.

Default Value
  • 0.1

(static, constant) TASK_THREAD_WATERMARK

Live Task threads above which launchTask() queues instead of starting. See the bound above.

Default Value
  • 50

Methods

(static) admitTaskLaunch(node) → {boolean}

Records node and decides whether its launch may proceed.

Returns false when the app is at or above the watermark, in which case the node is not recorded: a launch that has not started holds no thread, so counting it would inflate the ledger on exactly the path that must not spiral. False is not a refusal: launchTask() then queues the node, and the queue's drain asks again once taskLedgerHasRoom says a slot has freed.

Parameters:
NameTypeDescription
nodeobject
Returns:
Type: 
boolean

(static) countLiveTaskThreads(ledger) → {integer}

Counts the tracked nodes whose Task thread is still running.

This is the app's own launches only: it excludes the main and render threads and anything started outside launchTask().

Parameters:
NameTypeDescription
ledgerobject
Returns:
Type: 
integer

(static) drainTaskLaunchQueue(queue) → {object}

Starts every waiting launch the ledger now admits, oldest first. Called by TaskLaunchQueue's drain tick. Entries whose node was released while waiting are dropped first (taskQueueDropReleased); the first head the ledger refuses stops the pass, since nothing behind it may jump the order.

A tick with no free slot only reads the ledger. It is the tick that repeats while the app is saturated, and asking admitTaskLaunch would also write the ledger twice.

Parameters:
NameTypeDescription
queueobject
Returns:

an AA: waiting, the entries still waiting; started, how many launches started

Type: 
object

(static) enqueueTaskLaunch(queue, node) → {boolean}

Hands node to the queue; false (a refusal) only when the queue is full.

Parameters:
NameTypeDescription
queueobject
nodeobject
Returns:
Type: 
boolean

(static) isCurrentTaskEvent(event, current) → {boolean}

True when event came from current, the run the component still wants. False for an event queued by a node that has since been replaced or released, which the handler must ignore rather than read current in its place.

Parameters:
NameTypeDescription
eventobject

the roSGNodeEvent the observer handler received

currentdynamic

the field holding the node of the run in flight (may be invalid)

Returns:
Type: 
boolean

(static) isTaskInFlight(node) → {boolean}

True while node's run is live or waiting in the launch queue. Use it where a caller decides "is my run still going?": reading state = "run" alone calls a queued run finished, and replacing it sends the new one to the back of the queue.

Parameters:
NameTypeDescription
nodedynamic
Returns:
Type: 
boolean

(static) launchTask(node) → {boolean}

Starts node's Task thread through the accounted chokepoint.

(ADR 0041); false when nothing will start: an invalid node, or a launch over the watermark with no room to queue (a full queue, or none yet). Check it where state depends on the run delivering. A "loading" flag that only the delivery clears must take this result, or a refused launch leaves it set for good. A queued run has not started yet: ask isTaskInFlight(node), not node.state, whether it is still going.

Parameters:
NameTypeDescription
nodeobject

the Task node to start

Returns:

true when the node was started, or queued to start once a thread slot frees

Type: 
boolean

(static) noteTaskLaunchRefused(node) → {void}

A launch that starts nothing: past the queue's cap, or with no queue yet.

Parameters:
NameTypeDescription
nodeobject
Returns:
Type: 
void

(static) printTaskThreads() → {void}

Prints the ledger to the BrightScript console (telnet port 8085).

Callable from a paused console the same way as the debug flags: printTaskThreads() bsc-disable-next-line dead-code keep: debug-console: called from the BrightScript console, nothing in the app calls it

Returns:
Type: 
void

(static) pruneTaskLedger(ledger, node) → {object}

Returns ledger with finished threads dropped and node recorded.

Pruning at launch (rather than at read) keeps the ledger bounded without depending on anyone calling the readout: it holds the tasks live right now plus any that finished since the previous launch.

node is appended unconditionally and de-duplicated against the existing entries, because a persistent Task node can be relaunched many times over its life.

Parameters:
NameTypeDescription
ledgerobject
nodeobject
Returns:
Type: 
object

(static) recordTaskLaunch(store, node) → {object}

Records node into store's ledger, creating the field on first use.

The field is CREATED here rather than assumed to exist, because a write to a field an roSGNode does not have is a silent no-op. Measured on device: with the field declared at the end of setGlobalNodes(), the five Task threads that setGlobalNodes() starts before that line (the three ApiTasks, ApiQueueTask and SideEffectTask) were dropped without a trace, and printTaskThreads() under-reported by a permanent five. Creating on first use makes the ledger correct whatever order bootstrap and launches happen in, so the bug cannot come back by someone adding a launch earlier.

store is passed in rather than read from m.global, so a Rooibos test can drive it with a node of its own.

Parameters:
NameTypeDescription
storeobject
nodeobject
Returns:
Type: 
object

(static) releaseTask(node, observedField) → {dynamic}

Stops node's run, if any, and detaches the observer the caller registered on observedField.

Called from the handler of the field the Task writes, it can land before the Task's function returns (the node still reads run there), and a stopped run executes nothing further. So a caller that releases on delivery relies on that field being the Task's LAST write; anything the Task does after it may silently not happen.

Parameters:
NameTypeDescription
nodedynamic
observedFieldstring
Returns:

invalid, so dropping the node for good reads m.task = releaseTask(m.task, "field")

Type: 
dynamic

(static) replaceTask(existing, nodeType, observedField, fields) → {object}

Releases existing (see releaseTask) and returns a NEW nodeType Task node with fields set, ready to be observed and launched. Always assign the result back: m.task = replaceTask(m.task, "LoadItemsTask", "content", { itemsToLoad: "x" }).

Parameters:
NameTypeDescription
existingdynamic

the node of the run being replaced, or invalid when there is none

nodeTypestring

the Task component to create

observedFieldstring

the field the caller observes on existing

fieldsobject

inputs for the new node, set in one call

Returns:
Type: 
object

(static) sharedTaskLedger() → {dynamic}

The ledger launchTask() keeps on m.global, or invalid when there is none yet.

Returns:
Type: 
dynamic

(static) taskLaunchQueue() → {dynamic}

The launch queue on m.global, or invalid before setGlobalNodes() has created it.

Returns:
Type: 
dynamic

(static) taskLedgerHasRoom(ledger) → {boolean}

True when ledger has a slot for one more thread: admitTaskLaunch admits a node that holds no thread yet exactly when this is true. Read-only, unlike admitTaskLaunch, so asking costs no ledger write.

Parameters:
NameTypeDescription
ledgerdynamic
Returns:
Type: 
boolean

(static) taskQueueAdd(queue, node, cap) → {string}

Adds node to queue (in place) and says what happened: "queued" - appended at the back "already" - it already waits; its entry is re-armed with its current state, so a release-and-relaunch while waiting still starts it once "running" - its thread is live; not queued, the same way Roku ignores relaunching a running node "full" - cap entries already wait; not queued

Parameters:
NameTypeDescription
queueobject
nodeobject
capinteger
Returns:
Type: 
string

(static) taskQueueDropReleased(queue) → {object}

Returns queue without the entries whose node was released while waiting.

Parameters:
NameTypeDescription
queueobject
Returns:
Type: 
object

(static) taskQueueEntryStands(entry) → {boolean}

Parameters:
NameTypeDescription
entryobject
Returns:
Type: 
boolean

(static) taskQueueHolds(queue, node) → {boolean}

True when node waits in queue and has not been released since it was queued.

Parameters:
NameTypeDescription
queueobject
nodeobject
Returns:
Type: 
boolean

(static) taskQueueRetrySeconds(lastWait, started) → {float}

The wait before the queue's next retry, given the last wait and how many launches the last retry started: back to the shortest once one started (the slots are freeing, so more may follow), otherwise twice the last, up to the longest.

Parameters:
NameTypeDescription
lastWaitfloat
startedinteger
Returns:
Type: 
float

(static) taskStateIsLive(state) → {boolean}

taskThreadIsLive's rule for a state value already in hand: the data of a state change event, where reading the node again would cost a crossing for the same answer.

Parameters:
NameTypeDescription
statedynamic
Returns:
Type: 
boolean

(static) taskThreadIsLive(node) → {boolean}

True while node's Task thread is running.

"init" counts: a node told to RUN has not necessarily entered its function yet, but it is already holding a thread. A node that has never been launched also reports "init", which is why only nodes the ledger has recorded are ever passed here.

Parameters:
NameTypeDescription
nodeobject
Returns:
Type: 
boolean