components_api_ApiTask.bs
import "pkg:/source/api/apiResponse.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("ApiTask")
m.top.functionName = "runApiLoop"
end sub
' Continuous-server event loop (Architecture C).
' Waits for request field writes, executes HTTP via roku-requests, writes response.
' Never exits: it lives for the lifetime of the app alongside the global pool.
sub runApiLoop()
port = CreateObject("roMessagePort")
m.top.observeField("request", port)
' Signal that our observer is registered. The ApiQueueTask coordinator waits
' for all pool slots to be ready before dispatching. Without this, the
' coordinator could write to our request field before we're listening,
' causing a permanently dead slot.
m.top.isReady = true
while true
msg = wait(0, port)
if type(msg) = "roSGNodeEvent" and msg.getField() = "request"
' Safe to read m.top.request directly: the coordinator only dispatches
' one request per slot at a time (m.inFlight prevents reuse until we
' write the response), so the field always reflects our pending request.
req = m.top.request
if isValid(req)
m.top.response = executeRequest(req)
end if
end if
end while
end sub
' Executes a single HTTP request via the shared executeHttpRequest() helper and shapes
' the roku-requests result into the pool's response AA (an invalid URL becomes an error AA so the
' waiting orchestrator still unblocks). Auth/timeout/Content-Type live in the helper.
'
' The body is then held to the shape the request declared (apiResponse.enforceShape): a list
' request whose body holds no list comes back not-ok with `error = "unexpectedShape"`.
' @param req - request AA: { requestId, method, url, headers?, body?, timeoutMs?, expect? }
' @return response AA: { requestId, ok, statusCode, json, text, error? }
function executeRequest(req as object) as object
r = executeHttpRequest(req, "GET", "[ApiTask]")
if not isValid(r)
return {
requestId: req.requestId,
ok: false,
statusCode: 0,
json: invalid,
text: ""
}
end if
res = {
requestId: req.requestId,
ok: r.ok,
statusCode: r.statusCode,
json: r.json,
text: r.text
}
bodyType = type(r.json)
res = apiResponse.enforceShape(req, res)
' Stock Jellyfin answers every list endpoint with a bare array, so anything else (a wrapper
' read through, or a body rejected) is a server worth knowing about.
if r.ok and bodyType <> "roArray" and apiResponse.expectsList(req)
m.log.warn("list endpoint answered with a non-array body", req.requestId, "body", bodyType, "accepted", res.ok)
end if
return res
end function