Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rivenmedia/riven-ts/llms.txt

Use this file to discover all available pages before exploring further.

Riven’s entire application lifecycle is driven by XState v5 state machines. Rather than a tangled web of callbacks or ad-hoc async initialisation, every state transition is explicit, typed, and recoverable. Three machines cooperate to take Riven from cold start to a fully running media pipeline: the Bootstrap machine, the Plugin Registrar machine (invoked by Bootstrap), and the Main Runner machine.

Bootstrap Machine

Machine ID: "Bootstrap" The Bootstrap machine runs once at startup and proceeds through a strictly ordered sequence of states. Each state invokes a promise-based actor; a failure in any state transitions to the terminal Errored state and throws to the process supervisor.
// Bootstrap machine states in order
"Bootstrapping database connection"
"Clearing previous instance state"
"Applying mock scenario"        // only when mockScenario is set
"Bootstrapping plugins"
"Initialising services"         // parallel: GraphQL server
"Bootstrapping VFS"
"Success"                       // final — outputs server, vfs, plugins, queues
1

Bootstrapping database connection

Invokes the initialiseDatabaseConnection actor. Establishes the MikroORM connection pool to PostgreSQL. On failure, transitions immediately to Errored.
2

Clearing previous instance state

Invokes clearPreviousInstanceState with flags from settings: unsafeWipeDatabaseOnStartup and unsafeWipeRedisOnStartup. Resets any stale in-progress job state from a previous crash. After success, branches to Applying mock scenario if a mock scenario was provided, otherwise goes straight to Bootstrapping plugins.
3

Applying mock scenario (conditional)

Invokes applyMockScenario with the scenario object. Used exclusively in integration tests to seed a known database state before the runner starts. Skipped entirely in production.
4

Bootstrapping plugins

Invokes the pluginRegistrarMachine child machine (see below). On completion, the handlePluginValidationResponse action populates validPlugins, invalidPlugins, pluginQueues, pluginWorkers, and publishableEvents on the context. If any plugins failed validation, Riven logs a warning but continues.
5

Initialising services (parallel state)

A parallel state with a single region: Bootstrapping GraphQL Server. Invokes startGqlServer with the validated plugin map and plugin settings. On success, the Apollo server instance is stored in context via assignGqlServer. Transitions to Bootstrapping VFS when all parallel regions reach final.
6

Bootstrapping VFS

Invokes initialiseVfs with settings.vfsMountPath. On success, stores the Fuse instance in context via assignVfs. Transitions to Success.
7

Success (final)

The machine outputs { server, vfs, plugins, pluginQueues, pluginWorkers, publishableEvents } to the parent Program machine, which then sends START to the Main Runner.

Bootstrap Context and Output

// What Bootstrap carries in its context
interface BootstrapMachineContext {
  mainRunnerRef: AnyActorRef;
  rootRef: AnyActorRef;
  validPlugins: ValidPluginMap;          // plugins that passed validation
  invalidPlugins: InvalidPluginMap;      // plugins that failed — logged, not thrown
  pluginQueues: PluginQueueMap;          // BullMQ Queue per plugin event
  pluginWorkers: PluginWorkerMap;        // BullMQ Worker per plugin event
  publishableEvents: Set<RivenEvent["type"]>; // events with at least one hook
  server?: ApolloServer;
  vfs?: Fuse;
}

// What Bootstrap returns to the parent on success
interface BootstrapMachineOutput {
  server: ApolloServer;
  vfs: Fuse;
  plugins: ValidPluginMap;
  pluginQueues: PluginQueueMap;
  pluginWorkers: PluginWorkerMap;
  publishableEvents: Set<RivenEvent["type"]>;
}
Bootstrap throws if it completes without a server or VFS instance. These are considered fatal preconditions — the machine output type asserts their presence at runtime.

Main Runner Machine

Machine ID: "Riven program main runner" The Main Runner is the long-lived orchestrator. It starts in Idle, transitions to Running when it receives a START event from the Program machine, and only ever leaves Running if an unrecoverable error forces it into the terminal Errored state.
Idle  →(START)→  Running  →(fatal error)→  Errored (final)

Startup Sequence

When the START event arrives, a single assign action performs several things atomically:
  1. Stores the validated plugin map, queues, workers, and publishable events on context.
  2. Creates all flow workers (standard BullMQ workers for the core pipeline queues).
  3. Creates all sandboxed workers (isolated worker threads for CPU-intensive tasks).
// Flow workers created on START
"process-item-request"          // ItemRequest → index
"process-media-item"            // media item pipeline orchestrator
"request-content-services"      // polls content sources (Overseerr, MDBList…)
"scrape-item"                   // scraper fan-out (custom backoff strategy)
"download-item"                 // debrid download orchestration
"download-item.find-valid-torrent"  // torrent selection
"download-item.rank-streams"    // stream ranking
"request-stream-link"           // stream URL acquisition + health-check loop
"request-subtitles"             // post-process subtitle fetching
"post-process-media-item"       // post-processing orchestrator

// Sandboxed workers (useWorkerThreads: true)
"scrape-item.parse-scrape-results"       // concurrency = floor(parallelism × 0.25)
"download-item.map-items-to-files"       // concurrency = floor(parallelism × 0.75)
"download-item.validate-torrent-files"   // concurrency = floor(parallelism × 0.25)
After assignment, the entry actions of Running:
  • Raise riven.core.started (notifies plugin hooks that Riven is live).
  • Raise riven-internal.request-content-services (immediately polls content sources).
  • Raise riven-internal.retry-library (re-queues any incomplete items from a previous run).

Event Handling in Running

The Running state handles every RivenEvent type. The jobEnqueuer actor is invoked as a persistent service that receives forwarded events; it enqueues them to the appropriate plugin queues. The shouldQueueEvent guard prevents events from accumulating if no plugins have registered hooks for them.
// riven.item-request.create.success
// → spawnChild "processItemRequest" actor
// → enqueues process-item-request flow job

// riven.item-request.update.success
// → same as create.success; handles metadata refreshes

// riven.item-request.create.error.conflict
// → logs at verbose; item already exists, no action needed

Error Recovery and Graceful Shutdown

Job-Level Retry

Flow workers use BullMQ’s built-in retry with exponential backoff. The scrape worker uses a custom backoffStrategy based on settings.scrapeCooldownHours — 30 min → hours-based tiers at attempt 2, 5, and 10.

Library Retry

On startup and on a schedule, riven-internal.retry-library re-queues any ItemRequest or MediaItem that is stuck in an incomplete state from a previous run or crash.

Graceful Shutdown

The handleGracefulShutdown action sends riven.core.shutdown to the parent Program machine, which coordinates draining BullMQ workers and unmounting the VFS before the process exits.

Errored (final)

If the Main Runner enters Errored, it logs a fatal message and halts. This state is only reachable from an unrecoverable internal error — normal event handling errors are swallowed and logged per-event.

Integration with the Event System

State machines and the event bus are tightly coupled by design:
  1. The Main Runner’s send method (self.send) is passed directly to every flow worker as sendEvent, allowing job processors to fire events back into the machine.
  2. The jobEnqueuer actor receives forwarded events from the Running state’s always transition and fans them out to plugin queues.
  3. The publishableEvents set (built during Bootstrap) acts as a filter — events with no registered plugin hooks are never enqueued, preventing unbounded Redis growth.
// How a flow worker emits an event back to the machine:
sendEvent({
  type: "riven.media-item.index.success",
  item: indexedMediaItem,
});

// The Main Runner's Running state picks it up and routes it:
"riven.media-item.index.success": [
  { guard: "isUnreleasedItem", actions: ["scheduleReindex", "log"] },
  { guard: "isOngoingItem",    actions: ["scheduleReindex", "processMediaItem", "log"] },
  { guard: "isEntirelyReleasedItem", actions: ["processMediaItem", "log"] },
  { actions: "log" }, // fallback
]

Build docs developers (and LLMs) love