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