📡

Streams
Per-instance pub-sub between objects inside one component.

stream <name>: <Type> is a BehaviorSubject-like value for imperative, reactive communication between different objects living inside the same component — a .thr component's own script, and any .flsh class instances it holds. It's never for node-to-node communication across components — that stays field/bind: (see Reactive state and Template & binding).

stream dataLoaded: string

.emit / .subscribe

.emit(value) sends a new value; .subscribe(callback) reacts to every future value plus an immediate replay of the most recent one, synchronously, before .subscribe itself returns — a subscriber that arrives after at least one .emit() sees the current value right away, no separate "read the current value" call needed.

StreamDemo.thr (excerpt)
<script>
stream dataLoaded: string

public function setup() {
  dataLoaded.subscribe(function (value: string) {
    state dataLoadedText = "dataLoaded: " + value
  })
}

private function emitDataLoaded(key: string, press: boolean) {
  if (press) {
    dataLoaded.emit("payload-ready")
  }
}
</script>

Declaring a stream on a class

A stream can also be a class field — reachable from whoever holds the instance, exactly like reading any other public member:

Publisher.flsh
class Publisher {
  public stream onChanged: string

  public function change(value: string) {
    m.onChanged.emit(value)
  }
}

someInstance.onChanged.subscribe(...) then works with zero special mechanism, from either a component's own script or another class's method.

Bound-method subscribe sugar

Passing a bare stored function value across this boundary is unreliable for a class instance (a .flsh instance has no SceneGraph identity, and its m binding does not survive being stored and invoked later) — so .subscribe(<target>.<methodName>) is recognized structurally and lowered to a safe { target, action } descriptor automatically:

' Sugar: pass a bound method reference directly...
dataLoaded.subscribe(m.onPublisherChanged)

' ...instead of the hand-written descriptor it lowers to:
dataLoaded.subscribe({ target: m, action: "onPublisherChanged" })

Reference implementation — apps/streams-demo

Every mechanism on this page has a router-mounted, scaled chapter in apps/streams-demo — 3 chapters (/emit-subscribe, /class-stream, /bound-method-sugar), reachable with REWIND/FAST-FORWARD once compiled and sideloaded. /emit-subscribe contrasts a late subscriber's immediate replay against the idiomatic setup()-subscribe bridge-into-state pattern, firing multiple emissions in a row to prove the callback fires every time. /class-stream subscribes to the same class-declared stream field from both a component's own script and a second class instance at once. /bound-method-sugar wires the same emission to two independent listeners side by side — one using the raw { target, action } descriptor, one using the .subscribe(m.methodName) sugar — so a live run confirms both forms actually fire identically. See findings/streams-demo-app.md for what each chapter covers and findings/demo-app-conventions.md for the app-structure convention it follows.

⚠️ Not (yet) supported

  • ○ No unsubscribe API — a stream's subscriber list lives as long as whatever owns the stream does.
  • ○ .subscribe/.emit are untyped, ordinary method calls — never structurally type-checked against the declared <Type>.
  • ○ Calling .subscribe(...) from inside a derived expression or a template binding is rejected at compile time — it would re-subscribe and replay on every recompute.
  • ○ Deliberately excluded from the derived/watch dependency graph — a stream read never triggers a reactive recompute on its own.

Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.