Methods

(static) SubmitSideEffect(req) → {void}

Submits a fire-and-forget request to the global SideEffectTask. No response is observed. Calls are serialized, so do not use for cancellable UI requests.

Uses the children-as-vehicle pattern (like submitApiRequest / fetchRes): a FRESH ApiResultNode per request means back-to-back submits can never clobber one another. The previous single shared 'request' field could: two rapid writes coalesced and a POST was silently dropped (issue #744). The 'enqueue' write is only a wake signal; the task reads the appended children, not the value.

Parameters:
NameTypeDescription
reqdynamic

request AA: { method, url, headers?, body?, timeoutMs? }

Returns:
Type: 
void

(static) abandonApiRequest(resultNode) → {void}

Marks a request as one its caller has stopped waiting for, so the coordinator can skip it if it has not been dispatched yet. Safe on invalid and on a finished request.

Parameters:
NameTypeDescription
resultNodedynamic
Returns:
Type: 
void

(static) apiPoolNextDispatch(queue, inFlight) → {dynamic}

The coordinator's per-slot decision, as one step: take the next queued entry and say what to do with it. Extracted from ApiQueueTask.tryDispatch so tests can drive the rules: a skip that claimed a slot would leave that slot marked busy with nothing running on it, dead for the rest of the session, and the shell is not reachable.

Mutates queue and inFlight exactly as the coordinator needs: the returned entry is off the queue, and a dispatched one is already recorded in its slot.

for an entry completed as abandoned, or invalid when nothing can move now

Parameters:
NameTypeDescription
queueobject

the FIFO of { req, resultNode, waited } entries

inFlightobject

one element per pool slot: invalid = free

Returns:

an AA { slot, entry } naming the slot to send it to, the same AA with slot -1

Type: 
dynamic

(static) apiPoolSlotsWatchedBy(inFlight, caller) → {object}

The slots whose requests caller is waiting for, [] when none are: each answer was already delivered, or no request of its was watched. More than one when a Task has several long requests on slots at once (an apiPipeline run), which share one subscription to it.

watchedCaller is the Task the coordinator watches for it, if any

Parameters:
NameTypeDescription
inFlightobject

one element per pool slot: invalid = free, else an entry whose

callerobject

a Task node

Returns:
Type: 
object

(static) apiRequestCallerGone(abandoned, owner) → {boolean}

True when nobody will read the answer: the caller marked the request abandoned, or the Task thread that was waiting for it is no longer running.

Parameters:
NameTypeDescription
abandonedboolean
ownerdynamic
Returns:
Type: 
boolean

(static) apiRequestIsSkippable(req) → {boolean}

True when the pool may skip req once its caller is gone: a GET or HEAD (a request with no method is a GET, executeHttpRequest's pool default), or a request that declares skippable: true.

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
boolean

(static) apiRequestIsStoppable(req) → {boolean}

True when the coordinator watches req's caller and stops req if the caller goes away: a read (apiRequestIsSkippable) with a longer-than-default HTTP limit.

Parameters:
NameTypeDescription
reqdynamic
Returns:
Type: 
boolean

(static) apiSlotClearQuit(slot) → {void}

Ends a stop apiSlotQuit started. Called before the slot is sent its next request, which a quit still set would stop too. Safe on a slot that was never told to quit.

Parameters:
NameTypeDescription
slotobject
Returns:
Type: 
void

(static) apiSlotQuit(slot) → {void}

Tells a pool slot to give up the request it is waiting on. roku-requests reads m.top.quit on every pass of its wait and cancels the transfer once it is true.

The field exists only while a stop is pending, so ApiTask.xml must not declare it: roku-requests reads it on every pass of every request in the app, and a read of a field the node has costs about twice a read of one it lacks (docs/architecture/api.md, "A long request already on a slot"). Added here, removed by apiSlotClearQuit before the slot's next request.

Parameters:
NameTypeDescription
slotobject
Returns:
Type: 
void

(static) collectApiRequest(resultNode, port, maxWaitMsopt) → {dynamic}

Waits for a request a Task thread sent earlier with submitApiRequest(req, id, port), and returns its response as fetchRes would have. Lets that Task send a request it needs later, do a blocking fetchRes meanwhile, then collect it, so the two waits overlap instead of adding up. Waits as long as fetchRes unless the caller sets a shorter limit, and abandons the request the same way when that runs out.

long as fetchRes would (apiTimeout.waitMs)

Parameters:
NameTypeAttributesDefaultDescription
resultNodedynamic

what submitApiRequest returned; invalid (not sent) returns invalid

portobject

the port passed to submitApiRequest

maxWaitMsdynamic<optional>
invalid

the most to wait, for a caller with its own deadline; invalid waits as

Returns:

the response AA, or invalid when it was not sent or did not answer in time

Type: 
dynamic

(static) dropApiRequest(resultNode) → {void}

Releases a request a Task thread sent with submitApiRequest(req, id, port) and no longer needs, instead of collecting it: stops listening and abandons it.

Parameters:
NameTypeDescription
resultNodedynamic

what submitApiRequest returned; invalid is a no-op

Returns:
Type: 
void

(static) fetchJson(req, requestId) → {dynamic}

Convenience wrapper: submits via the queue and returns the parsed JSON body. Returns invalid on timeout, HTTP error, or if req is invalid.

Parameters:
NameTypeDescription
reqdynamic

request AA from a Build*Request() method

requestIdstring

unique string identifying this request

Returns:

res.json or invalid

Type: 
dynamic

(static) fetchRes(req, requestId) → {dynamic}

Submits a request to the API queue coordinator and blocks the calling Task thread until the response arrives (or apiTimeout.waitMs(req) elapses: timeouts.API_WAIT_MS unless the request carries its own timeoutMs).

Safe to call concurrently from multiple Task threads: each request gets its own ApiResultNode, so responses are never mixed up between callers. The coordinator (ApiQueueTask) is the only entity that dispatches to pool slots, eliminating the TOCTOU race that existed when callers claimed slots directly.

Delivery mechanism: the ApiResultNode is appended as a CHILD of the coordinator node. This is immune to SceneGraph port-event coalescing: even if multiple concurrent enqueue wakeup events merge into one, the coordinator reads ALL unprocessed children when it wakes up, so no request is ever lost.

Parameters:
NameTypeDescription
reqdynamic

request AA from a Build*Request() method; returns invalid if invalid

requestIdstring

unique string identifying this request (echoed in the response AA)

Returns:

response AA { requestId, ok, statusCode, json, text } or invalid on timeout (apiTimeout.waitMs)

Type: 
dynamic

(static) poolCallerTask() → {dynamic}

The Task node whose thread is running this code, or invalid on the render or main thread. Only fetchRes and a pipeline submit (both Task-thread only) record it.

Returns:
Type: 
dynamic

(static) settleIfCallerGone(entry) → {boolean}

Skips the entry when its caller is gone and it is safe to skip, completing its result node so the node never looks pending; returns true if it did.

Called by apiPoolNextDispatch, and only for an entry that waited for a slot: every read here crosses to the render thread, and the coordinator is the serial path every request passes through. Measured on a Stick 4K (2026-09-22, Home load, 96 requests): 7796 us per entry checked, against 1559 us for the slot write beside it.

Reads the request AA the coordinator already holds before touching the node, so a request that can never be skipped costs no cross-thread reads at all.

Parameters:
NameTypeDescription
entryobject

{ req: request AA, resultNode: ApiResultNode }

Returns:
Type: 
boolean

(static) submitApiRequest(req, requestId, portopt) → {dynamic}

Submits a request to the API queue coordinator and returns immediately without blocking. The caller observes 'isDone' on the returned ApiResultNode via a render-thread callback, then reads 'result' for the response.

The render thread does not make the HTTP call. It only creates a node, appends it to the coordinator, and returns (~microseconds). The coordinator (ApiQueueTask, a Task thread) dispatches to an ApiTask pool slot (another Task thread) which performs the actual HTTP request. When done, the result is written back to the ApiResultNode and the render-thread observeField callback fires.

Use for single API calls with trivial callbacks (set a boolean, read one value). Do not use when the callback needs data transforms or array processing: use an Orchestrator Task with fetchJson() instead.

Task-thread callers that pipeline several non-blocking requests pass their roMessagePort as port: isDone is then observed BEFORE the request enters the queue, so a response can never land ahead of the observer. Render-thread callers omit it (and should prefer fetchAsync).

ORDERING DEPENDENCY: this function does not wait for the pipeline to come up. Where fetchRes blocks up to ~10s on apiQueue.isReady, this returns invalid on the spot, which callers reasonably read as "no answer" and degrade on. That is only safe when something that does wait has already run: LoadLatestRowsTask, for instance, is reached from Home only after the libraries fetch (a blocking fetchRes) has returned, so the pool is provably up by then. A new caller that can run before any blocking request must not assume the same.

Parameters:
NameTypeAttributesDefaultDescription
reqdynamic

request AA from a Build*Request() method; returns invalid if invalid

requestIdstring

unique string identifying this request

portdynamic<optional>
invalid

optional roMessagePort to observe isDone on before enqueueing

Returns:

ApiResultNode to observe, or invalid if req is invalid or the pipeline is not up yet

Type: 
dynamic