Menu
Nuvio Web docs

The addon pipeline

From "this profile has some addon URLs" to "this page has rows".

Registry

addons/registry.ts turns the profile's addon rows into an AddonRegistry: enabled addons only, sorted by the order you arranged them, each with its fetched manifest.

The registry is the thing that answers "who serves this?". providersFor( resource, type, id) filters addons by what their manifest claims — a resource, the content types it covers, and its id prefixes — so a tt-prefixed IMDb id never goes to an addon that only serves kitsu: ids.

A manifest that fails to load produces an AddonLoadError rather than taking the registry down. The addon is left out; the others work.

Caching

CacheKeyTTL
Manifestaddon base URL30 minutes
Registryaccount + profile60s, or 5s while any addon is failing

The registry cache is process-wide, so its key has to identify the account and not just the profile. profileId is the profile index, 1..6 within one Nuvio account, so on an instance with more than one account — which /admin exists to support — keying on it alone served account B account A's addons, catalogs and streams for the length of the TTL. Invalidation is per-entry for the same reason: one person editing their addons should not re-fan-out everyone else's next page.

The shorter retry TTL is what makes a transient addon outage recover in seconds rather than up to a minute, and it is why the Addons page can usefully re-poll.

Client

addons/client.ts is the request layer. It takes the registry and a fetch, and every outbound request goes through safeFetch — see Security.

Fan-out is capped at six simultaneous upstream requests. Addon responses are normalized defensively: an entry missing what installing it needs (a URL), or what displaying it needs (an id and a name), is dropped rather than rendered.

Queries

addons/catalog-queries.ts holds the orchestration — home rows, search across catalogs, "more like this", a catalog page, a title's metadata — as pure functions taking an injected client. They reach for no request context, so they are unit-tested against a fake.

Home fetches up to eight catalogs (sixteen once the user has arranged them in Settings → Home), four at a time: fetching all of them at once is a burst at whichever addons serve them.

Request-scoped wrappers

addons/server.ts is the thin layer the loads actually call. Each wrapper grabs this request's client and delegates:

export async function homeCatalogRows(): Promise<HomeRow[]> {
  const { client, registry } = await getAddonClient();
  return queries.homeCatalogRows(client, registry);
}

Loads call these directly and stream the result — never awaited in the load itself — so addon fetches start server-side instead of after the page has shipped, hydrated, and made a second round trip.

Why pooling rather than Promise.all

A profile with a dozen stream providers would otherwise open a dozen simultaneous upstream connections on every page load. pooledMap from #lib/core/pool.ts caps the width while preserving input order:

const rows = await pooledMap(catalogs, 4, async (catalog) => fetchRow(catalog));

pool.ts also carries settleAll (every task must succeed, throws an AggregateError otherwise) and settleSome (returns results and errors, for best-effort fan-out where some providers are expected not to answer).

Addon fan-out is settleSome territory. A batch write where a partial result would leave inconsistent state is settleAll territory.

Where to add things

You want to...Touch
Add a new kind of addon querycatalog-queries.ts, pure, with a test
Expose it to a loadA wrapper in server.ts
Change how addons are ranked or filteredregistry.ts
Change transport, retries, normalizationclient.ts
Change what a manifest is allowed to bemanifest.ts

This guide lives in the project repo: edit it there, and this page follows within a day.