Methods

(static) applyPendingRowRemovals() → {void}

applyPendingRowRemovals: Drop the rows whose libraries returned nothing, queued during a run so the row list and the geometry arrays never disagree mid-load. Looked up by sectionId rather than a stored index — each removal shifts the ones after it.

Returns:
Type: 
void

(static) buildSectionPlan() → {object}

buildSectionPlan: Reads homeSection0-6 settings, returns ordered array of sections to display

Returns:

Array of { type: string, settingIndex: integer }

Type: 
object

(static) createSkeletonRow(sectionType) → {object}

createSkeletonRow: Creates a single empty HomeRow with correct title, sectionId, and cursorSize. Adds a single placeholder child node so the RowList renders the row.

Parameters:
NameTypeDescription
sectionTypestring

The section type from user settings

Returns:

HomeRow node, or invalid if section type is unsupported

Type: 
object

(static) createSkeletonRows() → {void}

createSkeletonRows: Creates empty HomeRow nodes for all planned sections except latestmedia. Each skeleton row gets a single placeholder child so the RowList renders the row label and a loading indicator at the correct slot size.

Returns:
Type: 
void

(static) discardRowBatch() → {void}

discardRowBatch: Drop pending row work without paying for it.

Teardown only. setRowItemSize() is the most expensive call in this file (~90-200 ms, nearly all of it the RowList re-measuring every cell against the new rowItemSize) and it runs on the render thread — so flushing a batch for a node that is about to be released spends that on geometry nobody will see. Clearing the state first makes resetLatestRowsRun's applyPendingRowRemovals + flushRowSizes no-op.

WHEN this is reached, because the obvious answer is wrong: NOT on navigating away from Home. Home is SUSPENDED rather than destroyed by the router (see startLatestMediaLoads, which relies on that). HomeRows.onDestroy comes from Home.destroyActiveContent() — a tab switch to Favorites — and from Home.onDestroy when sgRouter permanently closes the view. So the way to force a run in flight into this path is to switch tabs during a Home load.

Returns:
Type: 
void

(static) findInsertIndexForSection(sectionId) → {integer}

findInsertIndexForSection: Determines the correct content index for a section that needs to be inserted. Uses the section plan ordering to maintain row order.

Parameters:
NameTypeDescription
sectionIdstring

The sectionId to insert

Returns:

Index where the row should be inserted

Type: 
integer

(static) findLatestMediaInsertIndex() → {integer}

findLatestMediaInsertIndex: Determines the correct content index for latestmedia rows based on the section plan ordering.

Returns:

Index where latestmedia rows should be inserted

Type: 
integer

(static) findRowBySectionId(sectionId) → {object}

findRowBySectionId: Find a row in content by its sectionId field

Parameters:
NameTypeDescription
sectionIdstring

The sectionId to search for

Returns:

{ row: node, index: integer } or invalid if not found

Type: 
object

(static) flushRowSizes() → {void}

flushRowSizes: Recompute if a structural change is still pending. No-op otherwise, so it is safe to call unconditionally at a batch boundary.

Returns:
Type: 
void

(static) getItemAtIndices(indices) → {dynamic}

Gets an item from content at specified row and item indices Performs all necessary bounds checking and validation

Parameters:
NameTypeDescription
indicesroArray

[rowIndex, itemIndex]

Returns:

ContentNode if valid, invalid otherwise

Type: 
dynamic

(static) getRowConfigForSection(sectionId) → {object}

getRowConfigForSection: Returns title and slotSize for a sectionId. Used when inserting a row that was previously removed (no skeleton exists).

Parameters:
NameTypeDescription
sectionIdstring

The sectionId

Returns:

{ title: string, slotSize: array } or invalid if unknown

Type: 
object

(static) getSectionIdForType(sectionType) → {dynamic}

getSectionIdForType: Maps a section type string to its sectionId

Parameters:
NameTypeDescription
sectionTypestring

Section type from user settings

Returns:

sectionId, or invalid if type is unsupported

Type: 
dynamic

(static) init() → {void}

Returns:
Type: 
void

(static) insertLatestMediaSkeletons() → {void}

insertLatestMediaSkeletons: Creates skeleton rows for each non-excluded library and inserts them at the correct position in the content node.

Returns:
Type: 
void

(static) itemSelected() → {void}

Returns:
Type: 
void

(static) loadLibraries() → {void}

loadLibraries: Entry point called by Home.bs via callFunc. Builds ordered section plan, creates skeleton rows, then fires data tasks.

Returns:
Type: 
void

(static) logRowRemoved(sectionId) → {void}

logRowRemoved: Publish an immediate row removal AT THE INSTANT IT HAPPENS.

Its own line and its own moment, because sizeRemove cannot answer the question it was built for: those counters are zeroed at run start and read at run end, so a removal landing outside that window increments nothing anybody reports — and on this server the removal is unconditional, which makes sizeRemove a race outcome rather than an occurrence count. Why it is unconditional is a four-step chain through session.bs, createSkeletonRow, startParallelLoads and removalIsDeferrable; it is written out once, in docs/dev/home-first-paint-performance.md ("row removed"), rather than twice.

What a change to this function has to preserve:

  • The ORDINAL on at, and that it is per HomeRows INSTANCE. assembleSamples closes a sample when a line it already holds repeats, so a second immediate removal in one launch splits that launch across two samples. The ordinal is what makes the split visible in the record instead of a silent short read.
  • BOTH halves of cells, and that both are signed. loadsStarted is the discriminator and binds is the postcondition on it — loadsStarted 0 is what a genuinely early removal prints AND what a probe reading a counter-less root would print, so dropping binds makes those one reading. The -1 guard cannot fire in HomeRows today (init creates m.top.content and attaches the counters to that same node in the same init), but it is nearly free and the alternative to printing it is dropping the line, which reads as "no removal happened" — the one wrong answer.
  • The argument COUNT. roku-log faults at RUNTIME (&hf1, straight into the debugger) past nine call-site arguments once the BSC plugin has spent one on the injected pkg path. Three label/value pairs with two composed values sits a pair clear of that; nine is legal and this line would fit, but a probe whose overflow costs a whole device run is not worth writing at the ceiling.
Parameters:
NameTypeDescription
sectionIdstring

the sectionId of the row being removed

Returns:
Type: 
void

(static) onDestroy() → {void}

onDestroy: Full teardown releasing all resources before component removal Called by Home.bs onDestroy() before nulling the homeRows reference

Returns:
Type: 
void

(static) onItemFocused() → {void}

Observer for rowItemFocused field - delegates to updateBackdropForFocusedItem

Returns:
Type: 
void

(static) onKeyEvent(key, press) → {boolean}

Parameters:
NameTypeDescription
keystring
pressboolean
Returns:
Type: 
boolean

(static) onLatestRowsReady() → {void}

onLatestRowsReady: Drains all unprocessed result children from the orchestrator. Each wake may cover several results — wake events can coalesce, the children can't.

Returns:
Type: 
void

(static) onLibrariesLoaded() → {void}

onLibrariesLoaded: Handler when LoadLibrariesTask returns data. Populates the library row, creates latestmedia skeleton rows, then fires latest tasks.

Returns:
Type: 
void

(static) onProgramsExpired() → {void}

Fires when JRRowList's progress tick detects at least one expired Program or Recording. Re-runs LoadOnNowTask and/or LoadActiveRecordingsTask to pull fresh data. Loading guards debounce repeated expiry signals while a load is already in flight, and section plan checks avoid wasted requests when the user has disabled the relevant sections.

Returns:
Type: 
void

(static) onSectionPopulated() → {void}

onSectionPopulated: Called after each section is populated with data. Signals AppLaunchComplete after 2 sections have loaded.

Returns:
Type: 
void

(static) populateLibraryRow() → {void}

populateLibraryRow: Fills the library row with filtered library items. Uses populateRowFromData for consistent in-place update behavior.

Returns:
Type: 
void

(static) populateRowFromData(sectionId, itemData) → {void}

populateRowFromData: Unified row population logic used by all update callbacks. Updates the children of the existing row node in place to avoid RowList re-layout and focus disruption. Removes the row if data is empty. If the row doesn't exist but data is available (e.g., a previously empty section now has content on refresh), creates and inserts the row at the correct position.

Parameters:
NameTypeDescription
sectionIdstring

The sectionId of the target row

itemDatadynamic

Array of content nodes from the task, or invalid/empty

Returns:
Type: 
void

(static) removeRowAtIndex(index) → {void}

removeRowAtIndex: Removes a row at the given index and recalculates sizes

Recomputes IMMEDIATELY, batch or no batch. This path is only reached for a row the current run does not own (latestRows.removalIsDeferrable said no), so nothing later is going to flush on its behalf — and a removal without its recompute leaves the three geometry arrays describing the old row list, which draws every row below it at its neighbour's size.

Parameters:
NameTypeDescription
indexinteger

Content child index to remove

Returns:
Type: 
void

(static) resetLatestRowsRun() → {void}

resetLatestRowsRun: Return the orchestrator to a clean pre-run state — unobserved, stopped, its delivered result children dropped and the drain cursor rewound.

Called before starting a run and from onDestroy. Named for the run rather than the task (the old cleanupLatestMediaTasks) because there is exactly one task now; what gets reset is the run's state around it.

Returns:
Type: 
void

(static) rowStructureChanged(immediateopt) → {void}

rowStructureChanged: Announce that a row was added to or removed from m.top.content.

Outside a batch this recomputes immediately, so the single-row callers are unchanged.

Don't go looking for the deferred path — no caller reaches it. Every mid-run caller passes immediate (it MUST, or the geometry arrays stop describing the row list) and the rest only run while m.rowSizesBatching is already false, so m.rowSizesDirty is false at both batch boundaries and flushRowSizes() there is a no-op. What actually coalesces a run into one recompute is m.pendingRowRemovals.

m.rowSizesDirty is a FAIL-SAFE, not scaffolding for a future feature: a new mid-run caller that forgets immediate recomputes at the batch boundary instead of never. That turns the failure from wrong geometry — rows drawn at their neighbour's size, which produces no error, no log line and no timing change — into geometry that is merely late. Deleting it makes that mistake silent, which is the one failure mode in this file nothing else catches.

Parameters:
NameTypeAttributesDefaultDescription
immediateboolean<optional>
false
Returns:
Type: 
void

(static) setRowItemSize() → {void}

setRowItemSize: Loops through all home sections and sets the correct item sizes, heights, and spacings per row. rowItemSize[i] = slot size [width, posterHeight] — determines focus ring dimensions (poster only, no text). rowHeights[i] = total row height: slot + 90px text area for standard rows; slot-only for library tiles. rowSpacings[i] = gap after each row before the next row label. Must be set for ALL rows because Roku ignores itemSpacing entirely once rowSpacings is assigned (even partially). Standard rows use 60px; My Media uses 78px to partially compensate for its absent text area.

Reach for rowStructureChanged() rather than this — it is the coalescing seam, and this is expensive enough (~85 ms early in a load, ~200 ms once every row is populated) that how many times it runs is the whole cost model.

Returns:
Type: 
void

(static) startLatestMediaLoads() → {void}

startLatestMediaLoads: Hands the eligible libraries (in row order) to the orchestrator task.

Skips outright while a run is in flight, matching the isLoading* rule updateHomeRows() already applies to the other persistent tasks: the running orchestrator is about to deliver fresh data, so restarting buys nothing and costs a cancel. It also keeps this off the one path nobody has measured — Home is SUSPENDED rather than destroyed on navigation (onScreenHidden stops no tasks), so a Home revisit lands here mid-run routinely, and restarting would mean setting control = "RUN" on a thread that may still be unwinding.

Returns:
Type: 
void

(static) startParallelLoads() → {void}

startParallelLoads: Fires off data tasks for sections that don't need library data. Library-dependent sections (library row, latestmedia) are handled in onLibrariesLoaded.

Returns:
Type: 
void

(static) updateActiveRecordingsItems() → {void}

Returns:
Type: 
void

(static) updateBackdropForFocusedItem() → {void}

Update backdrop to match currently focused item Handles all validation and edge cases Used by: onItemFocused observer and row update functions after replaceChild

Returns:
Type: 
void

(static) updateContinueWatchingItems() → {void}

Returns:
Type: 
void

(static) updateHomeRows() → {void}

updateHomeRows: Refresh data for all rows without tearing down the UI. Keeps existing row nodes in place to avoid focus disruption. As fresh data arrives, populateRowFromData updates row children in place.

Returns:
Type: 
void

(static) updateNextUpItems() → {void}

Returns:
Type: 
void

(static) updateOnNowItems() → {void}

Returns:
Type: 
void

(static) updateSize() → {void}

Returns:
Type: 
void