Methods
(static) closeSocket() → {void}
Best-effort teardown of the socket child + its observers.
- Type:
- void
(static) connectAndPump(url) → {boolean}
Open the socket and pump events until it closes or errors. Sends a KeepAlive whenever the wait times out at the interval Jellyfin's ForceKeepAlive requested (0 = block until an event, before any ForceKeepAlive arrives). Marshals each dispatchable command to the main thread. Returns on close. Returns TRUE when the connection was ESTABLISHED and STABLE — it opened AND stayed up at least STABLE_SESSION_MS — so runReceiver can reset its reconnect backoff. A never-opened socket, or one that opened but dropped sooner than that (a flap), returns FALSE so the backoff keeps climbing. Threshold = the backoff cap (30s): a session that outlived the max backoff interval was clearly real.
| Name | Type | Description |
|---|---|---|
url | string |
- Type:
- boolean
(static) dispatchBatch(body) → {integer}
Parse a long-poll 200 body (a JSON array of { MessageType, Data, MessageId } envelopes) and marshal each dispatchable command to the main thread, in array order. Returns how many commands were actually dispatched (0 for an empty array, a garbled body, or an all-ignore batch) so the caller can floor a pointless fast 200 against a hot-loop.
| Name | Type | Description |
|---|---|---|
body | string |
- Type:
- integer
(static) dispatchEnvelope(envelope) → {boolean}
Normalize one long-poll envelope and marshal it if dispatchable; returns true iff it was dispatched. The long-poll contract never carries KeepAlive/ForceKeepAlive (the poll request is the keepalive), so keepalive is treated like ignore here — there's no socket to answer on. Reuses the same pure parseMessage as the ws path.
| Name | Type | Description |
|---|---|---|
envelope | object |
- Type:
- boolean
(static) handleFrame(data) → {void}
Parse one inbound frame and either answer a keepalive or marshal a dispatchable command.
| Name | Type | Description |
|---|---|---|
data | object |
- Type:
- void
(static) hasDispatched(messageId) → {boolean}
Dedupe ring for at-least-once redelivery. Records ids we've actually dispatched and evicts the oldest past a bounded window — a duplicate only ever arrives back-to-back (a redelivery on the very next poll), so a small window is ample. An empty id (the ws path, or a malformed envelope) is never tracked, so it's never deduped — those transports/paths don't redeliver.
| Name | Type | Description |
|---|---|---|
messageId | dynamic |
- Type:
- boolean
(static) httpGet(url, timeoutMs) → {object}
Authenticated async GET on this Task thread. Blocks up to timeoutMs for the response. Returns { code, body }; code = 0 on a failed initiation or a timeout. A fresh roUrlTransfer + roMessagePort per call avoids cross-call event bleed. NEVER logs the url — the Authorization header carries the token. Mirrors the raw-transfer pattern in captionTask.bs / ServerReachableTask.bs.
| Name | Type | Description |
|---|---|---|
url | string | |
timeoutMs | integer |
- Type:
- object
(static) init() → {void}
Remote-control receiver (#666) — the ws:// "Cast to JellyRock" listener. See RemoteControlTask.xml for the lifecycle/threading overview. This task owns the vendored WebSocketClient node and runs the Jellyfin session socket off the render thread: open -> parse frames -> answer keepalive -> marshal commands to the main thread -> reconnect with backoff. It NEVER dispatches (that's remoteDispatch on the main thread) and NEVER logs the socket url (it carries the auth token).
- Type:
- void
(static) isTerminalSocketEvent(field, msg, opened) → {boolean}
True when this event means the connection is over, so the pump loop should tear down and let runWsReceiver reconnect. Two signals, because the receiver and the vendored socket loop key off DIFFERENT ones:
- on_close / on_error — the fast path, and the only one that fires for an error raised while the socket is still OPEN or CLOSING (a failed read, a rejected handshake). That lands here at once, ~30s before _CLOSING_DELAY would force the client to CLOSED.
- ready_state = CLOSED — the backstop. The vendored task loop releases its Task thread on that transition (#728), but the receiver would otherwise only ever wake on on_close/on_error. Those coincide in the client today — every _close() path in WebSocketClient.brs posts one of the two — yet nothing enforces it, and no lint can see it (it is a property of the vendored CLIENT, not of the files lint:socket-thread-release reads). Without this arm, an upstream re-sync that broke the coincidence would leave the pump blocked forever on a socket whose thread is already gone: no crash, no failing test, cast silently dead.
opened is load-bearing on the second arm. ready_state is CLOSED before open() is ever called AND the task seeds m.top.ready_state from it at startup, so an ungated test would race that seed and tear down every connection before it could open. Whichever signal arrives first wins; because _close() posts ready_state before on_close, the backstop usually wins the normal server-close path — the outcome is identical either way.
| Name | Type | Description |
|---|---|---|
field | string | |
msg | object | |
opened | boolean |
- Type:
- boolean
(static) marshalCommand(command) → {void}
Marshal a normalized command to the main thread. Drops the bulky original 'raw' Data first (a whole ItemIds list for a series/album can be large) — the normalized fields carry everything the main thread needs. Shared by both transports; callers gate out keepalive/ignore before calling.
| Name | Type | Description |
|---|---|---|
command | object |
- Type:
- void
(static) recordDispatched(messageId) → {void}
| Name | Type | Description |
|---|---|---|
messageId | dynamic |
- Type:
- void
(static) runLongPollReceiver(serverUrl) → {void}
HTTPS long-poll transport (#667). Roku can't ws:// a secure server, so on https we consume the companion plugin's authenticated long-poll command channel over TLS (roUrlTransfer). The poll request ITSELF is the keepalive (it refreshes server-side session activity), so — unlike the ws path — there's no client KeepAlive to send. See docs/architecture/remote-control-longpoll-contract.md.
| Name | Type | Description |
|---|---|---|
serverUrl | string |
- Type:
- void
(static) runReceiver() → {void}
Post-login entry point (control="RUN" from Home.isFirstRun, once the session token exists). Selects the transport by server scheme: an http:// server uses the ws:// session socket (#666); an https:// server can't ws:// (Roku has no socket TLS) so it probes for the companion plugin and, if present, runs the long-poll command channel (#667). SignOut sets control="STOP", terminating this thread.
- Type:
- void
(static) runWsReceiver(serverUrl) → {void}
ws:// transport (#666): build the session-socket url and run the connect/reconnect loop until the session ends (token rotated, or control="STOP" kills the thread).
| Name | Type | Description |
|---|---|---|
serverUrl | string |
- Type:
- void
(static) throttleEmptyPoll(pollTimer) → {void}
Hot-loop floor for an abnormally fast EMPTY poll. A compliant plugin holds an empty poll for ~POLL_WAIT_MS, so this never fires against it; a misbehaving plugin that returns an empty 200/204 near-instantly would otherwise spin the poll loop with no backoff. Sleep out the remainder of a minimum interval. Only ever called on the empty path, so it never delays a real command.
| Name | Type | Description |
|---|---|---|
pollTimer | object |
- Type:
- void