components_api_SideEffectTask.bs
import "pkg:/source/api/baseRequest.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/misc.bs"
sub init()
m.log = new log.Logger("SideEffectTask")
m.top.functionName = "runSideEffectLoop"
end sub
' Tier 2 fire-and-forget writer — FIFO children-as-vehicle queue (Architecture C,
' continuous-server). Mirrors ApiQueueTask, minus the 3-slot pool and response routing:
' side effects have no response to deliver, so each request executes inline on this single
' thread in append order (serialised, must-complete: mark watched, playstate, favorite, …).
'
' Why children instead of the old shared 'request' field:
' SceneGraph coalesces port events when a field is written rapidly from the render thread.
' Two back-to-back SubmitSideEffect() calls could merge into one event, and the second
' 'request' write clobbered the first before this thread read it — silently dropping a POST
' (issue #744: the capabilities POST lost behind the cold-launch /pair POST). Children are
' immune: even if wake-up events coalesce, we read ALL pending children from the node tree,
' so no request is ever lost regardless of timing.
sub runSideEffectLoop()
' Index of the next unprocessed child (children accumulate, never re-processed).
m.processedIndex = 0
port = CreateObject("roMessagePort")
m.top.observeField("enqueue", port)
' Process any children appended before the observer was registered (SubmitSideEffect
' may have appended + pulsed 'enqueue' while this thread was still starting up — that
' wake-up was lost, but the child is in the tree). The node tree is the source of truth.
processNewChildren()
while true
msg = wait(0, port)
' Any wake (even coalesced enqueue events) drains the full backlog: read ALL
' unprocessed children from the tree, not the event payload.
if type(msg) = "roSGNodeEvent" then processNewChildren()
end while
end sub
' Execute every unprocessed child's request AA in FIFO order, then prune processed
' children so the tree doesn't grow unbounded over a long session.
sub processNewChildren()
childCount = m.top.getChildCount()
while m.processedIndex < childCount
child = m.top.getChild(m.processedIndex)
m.processedIndex++
if isValid(child) and isValid(child.request)
' Blocking HTTP on this task thread — serialises back-to-back side effects.
' Fire-and-forget: the result is intentionally discarded.
'
' Per-request try/catch: this is a continuous-server loop, so an uncaught error
' would kill the thread and silently drop EVERY later side effect for the session.
' The pre-#744 task re-ran per call (control="RUN" each submit), so a crash
' self-healed on the next submit; the persistent loop has no such reset, which makes
' per-request isolation load-bearing rather than defensive nicety.
try
executeHttpRequest(child.request, "POST", "[SideEffectTask]")
catch error
m.log.error("side-effect request threw; continuing drain", { message: error.message })
end try
end if
end while
' Prune processed children. Safe: side effects hold no post-execution references to
' these nodes (unlike ApiQueueTask, where callers await a result), so detaching them
' from the tree is a clean drop.
if m.processedIndex >= 50
m.top.removeChildrenIndex(m.processedIndex, 0)
m.processedIndex = 0
end if
end sub