Live feeds: a feed is a driver on the set port

How to wire an external, real-time data source — a sensor, a WebSocket, a polled API — into a notebook. The short version: a feed is not a cell. A feed is a program that calls the notebook’s set port as data arrives, and the notebook is pure cells that react.


Why a feed can’t be a cell

Two facts of the design decide this, and they decide it cleanly:

  1. Cells are pure. A cell is a function of its inputs, recomputed each wave — that purity is what makes scrubbing reversible and the cache sound. A cell that reached out and fetched a URL would be neither.
  2. There are no timers. Tick-clocked folds are a deferred milestone; nothing in a cell fires on a schedule. A cell cannot poll a feed even if it wanted to.

So a feed lives outside the graph, which is exactly where the design already puts impurity. The design doc’s F3 rule: the transport owns the impure boundary; a cell is subscribed and pulled, never pushes. A live feed is the same rule read forwards — the impure edge (the socket, the HTTP call, the clock) belongs to a driver, and the notebook stays a pure function of a leaf whose value the driver writes.

The pattern

A feed leaf is an ordinary input leaf — the same kind a slider drives — except the hand on the knob is a program instead of a mouse:

// The latest reading from a live feed. Its default is 0; a driver process
// overrides it via the set port as data arrives.
//notebook:slider min=0 max=100 step=1
func reading() (v int) { return 0 }

// Everything downstream is pure and reacts to each new value.
func doubled(v int) (d int) { return v * 2 }

A driver connects to the source and writes each new value through the notebook’s one data-in port:

That is the whole contract. It is the one-port concept with a program on the other end: you set a leaf (data in) and subscribe to a cell (data out) — the same two calls whether the counterparty is a human at a slider or a market feed.

  ┌────────────┐   POST /set {leaf,value}    ┌──────────────────────┐
  │  driver    │ ──────────────────────────▶ │  notebook (pure cells)│
  │ (the feed) │                             │  reading → doubled → …│
  │  ws/http/  │ ◀────────────────────────── │                       │
  │  sensor    │        GET /events          └──────────────────────┘
  └────────────┘   (derived values stream)

Verified end-to-end: a driver POSTing v=42 to a served notebook recomputes the derived doubled cell to 84 on the event stream — leaf set from outside, pure cell reacts, result streams out.

Two honest notes

Worked examples

Four drivers, one pattern, in examples/:

Each ships the notebook (pure), the driver (driver/main.go, the only impure part), and a short README showing notebook run + starting the driver. The notebook alone is a static, portable artifact; the driver is what makes it live.