Technical Documentation

Pearl Architecture

A technical deep dive into how Pearl is structured — the module system, signal graph, runtime, and the decisions behind them.

Modules

Composable, self-contained units with declared inputs and outputs.

Signals

Reactive values that form a directed graph between modules.

Runtime

Executes modules, schedules updates, and manages the signal graph.

Graph

The dependency structure that enables incremental recomputation.

Overview

Pearl is built around a single idea: composition over configuration. The system is divided into three layers — modules, signals, and the runtime — each with a narrow responsibility and a clear contract with the layers around it.

This document describes each layer, how they interact, and the trade-offs that shaped the design.

Module System

A module is a pure function that receives its dependencies as arguments and returns a set of signals. Modules never access global state, which makes them trivially testable and deterministic.

type Module<I, O> = (inputs: I) => O
// I: inputs (signals)   O: outputs (signals)

Because modules are just functions, they compose naturally. The output of one module can be passed directly as the input to another, and the runtime handles the wiring.

Signal Graph

Signals form a directed acyclic graph (DAG). When a signal changes, the runtime walks the graph and recomputes only the nodes that depend on the changed value. This incremental evaluation is what makes Pearl efficient at scale.

Reactivity model

Pearl uses a push-pull reactivity model. Producers push invalidation marks downstream; consumers pull fresh values only when they are read. This avoids redundant computation while keeping latency low.

Runtime

The runtime is the orchestrator. It owns the signal graph, schedules updates, and provides the execution context for modules. The default runtime is single-threaded and optimized for low latency, but the interface is designed so alternative runtimes (e.g. worker-based) can be dropped in.

interface Runtime {
  register(module: Module): void
  run(): void
  dispose(): void
}

Data Flow

A typical request flows through the system as follows:

  • An external event updates a source signal.
  • The runtime marks all downstream signals as stale.
  • When a consumer reads a stale signal, the runtime lazily recomputes it.
  • Results are cached until the next invalidation.

Design Decisions

Why pure modules?

Purity guarantees that a module will always produce the same output for the same inputs. This makes modules cacheable, testable, and parallelizable — all without the developer thinking about it.

Why push-pull?

A pure push model over-computes; a pure pull model has high read latency. The hybrid approach gives Pearl the best of both: fast invalidation, lazy computation.

Why a pluggable runtime?

Different deployment targets have different constraints. A browser extension and a server process should not share the same scheduler. Pluggable runtimes let Pearl adapt without forcing the application code to change.