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.
<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:
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/.emitare untyped, ordinary method calls — never structurally type-checked against the declared<Type>. - ○ Calling
.subscribe(...)from inside aderivedexpression or a template binding is rejected at compile time — it would re-subscribe and replay on every recompute. - ○ Deliberately excluded from the
derived/watchdependency graph — a stream read never triggers a reactive recompute on its own.
Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.