Methods
(static) answeredOnPurpose(entry) → {boolean}
Applies the rule an RTA spec set for a queued request (m.global.rtaFailRequests), so a spec can drive a screen's failure and slow-server paths against a healthy, fast server. Decides from the request AA already in entry: no node read is added to this loop.
A failure is answered here, exactly as a slot's answer would be, and never takes a slot: true, the request is done. A slow request is marked with how long to hold its answer and queued like any other (false); tryDispatch stamps when it is due, heldOnPurpose holds it.
| Name | Type | Description |
|---|---|---|
entry | object |
- Type:
- boolean
(static) deliverResponse(slotIdx, entry, response) → {void}
Hands a slot's answer to the orchestrator's result node and frees the slot. Result is written before done, so it is readable when the observer fires.
| Name | Type | Description |
|---|---|---|
slotIdx | integer | |
entry | object | |
response | dynamic |
- Type:
- void
(static) emitDispatchWindow(dispatched) → {void}
One line per DISPATCH_WINDOW entries taken off the queue: what the coordinator spent per entry deciding whether to skip it (check), handing it to a slot (send), and delivering answers back (deliver, counted per response, so its window is the same entries seen from the other end). Read by the api-dispatch measurement family in scripts/measurements.js.
| Name | Type | Description |
|---|---|---|
dispatched | integer |
- Type:
- void
(static) forgetCaller(entry) → {void}
Ends this entry's part in the watch watchCaller started, if any, and the subscription with it once no other long request of the same Task is still on a slot.
unobserveField drops every observer of the field, not only this loop's, so the subscription is one per Task, not one per request (apiPool.bs, "Stopping a request nobody is waiting for"). Dropping observers that are not ours is safe because nothing else observes a Task's state: the task ledger reads it instead, by design (source/utils/tasks.bs). The scoped pair, observeFieldScopedEx / unobserveFieldScoped, should drop only this one, but on device unobserveFieldScoped has removed another component's observer (#898, components/CLAUDE.md), so it is not used until this case is measured (followup api-pool-watch-scoped-observer).
| Name | Type | Description |
|---|---|---|
entry | object |
- Type:
- void
(static) heldOnPurpose(slotIdx, entry, response) → {boolean}
Holds a slowed request's answer when it arrives before it is due. Its slot stays in flight until the loop delivers it, so the pool is as busy as a slow server would keep it.
| Name | Type | Description |
|---|---|---|
slotIdx | integer | |
entry | object | |
response | dynamic |
- Type:
- boolean
(static) init() → {void}
- Type:
- void
(static) onCallerStateChanged(pool, msg) → {void}
A watched Task changed state. Once it is no longer running, nobody will read the answers its requests are still waiting for, so every slot it was waiting on is stopped. A Task that finished normally also reports "stop", but only after its answers were delivered (and delivery ends the watch), so it matches no slot here.
| Name | Type | Description |
|---|---|---|
pool | object | |
msg | object |
- Type:
- void
(static) processNewChildren(pool) → {void}
Read all unprocessed children (ApiResultNodes appended by fetchRes callers), extract their request AAs, add to the internal FIFO queue, and dispatch. Periodically prunes processed children to prevent unbounded accumulation.
| Name | Type | Description |
|---|---|---|
pool | object |
- Type:
- void
(static) publishHeld() → {void}
Tells specs how many held answers are keeping a slot busy (m.global.rtaHeldRequests).
- Type:
- void
(static) releaseHeldOnPurpose(slotIdx, entry) → {void}
A slowed request that is stopped gives its slot back at once, as a stopped real one does. Its answer, held already or still to come, is not held (or no longer): the slot answers "no answer" now if it was held, and on arrival otherwise.
| Name | Type | Description |
|---|---|---|
slotIdx | integer | |
entry | object |
- Type:
- void
(static) resolvePoolSlots() → {object}
The pool's width is chosen per device class (apiPool.widthFor, stored as m.global.apiPoolWidth), but its slots are separately named globals (apiPool0, apiPool1, ...) that setGlobalNodes() in source/utils/globals.bs creates one guarded block at a time, because no-task-fanout rejects launching fresh Task nodes in a loop. Nothing else ties those blocks to the width, so they are checked against it here, in both directions. A slot the width expects but globals.bs never created (a width raised past the blocks that exist) would dispatch to invalid; one created beyond the width is a thread that runs for the whole session and is never used.
- Type:
- object
(static) runQueueLoop() → {void}
FIFO request queue coordinator (Architecture C, continuous-server).
Receives ApiResultNode children appended by fetchRes() callers. Each node carries the request AA on its 'request' field. The coordinator reads ALL unprocessed children when woken up, dispatches each to the first free ApiTask pool slot, then routes the completed response back to the ApiResultNode so the orchestrator unblocks.
A queued READ whose caller has stopped waiting is completed as abandoned instead of being sent, so the slot goes to the next request. The decision (including which entries are worth the cross-thread check at all) is apiPoolNextDispatch in apiPool.bs ("Skipping a request nobody is waiting for"); this file is its shell.
A long request already SENT is stopped the same way once its caller has gone: the coordinator watches the waiting Task and tells the slot to give up (apiPool.bs, "Stopping a request nobody is waiting for"; watchCaller / stopSlot below).
Why children instead of a shared field: SceneGraph can coalesce port events when multiple Task threads write to the same field in rapid succession: the coordinator would see fewer events than writes, silently losing requests. Children are immune: even if wakeup events coalesce, the coordinator reads ALL pending children from the node tree, so no request is ever lost regardless of timing.
Startup ordering guarantee:
- Each ApiTask pool slot sets isReady=true after registering its request observer.
- This coordinator waits for ALL pool slots to be ready before registering its own observers and setting isReady=true.
- fetchRes() waits for this coordinator's isReady=true before appending children. Result: pool observers, then coordinator ready, then fetchRes appends. No startup race.
Thread safety: Only this task thread reads/writes m.queue, m.inFlight, and m.processedIndex, so there is no TOCTOU race. Child appends from other threads go through SceneGraph rendezvous.
- Type:
- void
(static) stopSlot(pool, slotIdx) → {void}
Tells a slot to give up its request: roku-requests cancels the transfer on its next pass and the slot answers "no answer", which frees it through the normal delivery path. The answer goes to a result node nobody reads.
| Name | Type | Description |
|---|---|---|
pool | object | |
slotIdx | integer |
- Type:
- void
(static) tryDispatch(pool) → {void}
Dispatch queued requests to free pool slots (FIFO). Only ever called from this coordinator's task thread, so there is no concurrent access.
| Name | Type | Description |
|---|---|---|
pool | object |
- Type:
- void
(static) watchCaller(pool, slotIdx, entry) → {void}
Watches the Task waiting for a long request, so its slot can be stopped if that Task goes before the answer arrives (apiPool.bs, "Stopping a request nobody is waiting for"). Every other request returns on the request AA alone: no crossing on the common path.
| Name | Type | Description |
|---|---|---|
pool | object | |
slotIdx | integer | |
entry | object |
- Type:
- void