Skip to content

Re-derive subscriptions after every update instead of only at init#514

Merged
DROOdotFOO merged 3 commits into
DROOdotFOO:masterfrom
merklebonsai:pr/reactive-subscriptions
Jul 13, 2026
Merged

Re-derive subscriptions after every update instead of only at init#514
DROOdotFOO merged 3 commits into
DROOdotFOO:masterfrom
merklebonsai:pr/reactive-subscriptions

Conversation

@merklebonsai

Copy link
Copy Markdown
Collaborator

Dispatcher.setup_subscriptions/1 was called once, from init/1, against the initial model — and never again (two grep hits: the call site and the definition). Subscriptions were effectively static.

That breaks any app whose subscribe/1 depends on state, which is the normal TEA pattern: poll only while a job is running, tick only the currently-selected view. The app declares the subscription; the runtime never asks again; it is never started.

The visible case is the playground. Playground.App.subscribe/1 returns the selected demo's subscriptions, but at mount the selection is the first widget (which has no ticks). So every tick-driven demo — the scroll-anchor stream, Easing, Sparkline — was dead on arrival. Selecting them changed subscribe/1's result, but nothing re-read it.

Fix: subscriptions are a function of the model, so derive them from it. After every update, re-run subscribe/1, diff against the running set, start what's newly declared, stop what's no longer declared. A %Subscription{type, data} is its own identity, so an unchanged subscription keeps its existing timer rather than being torn down and restarted on every keystroke — no churn.

This is Elm's subscriptions semantics, which Raxol had declared but not implemented.

New tests (dispatcher_subscriptions_test.exs): a sub declared only by the updated model gets started; a sub no longer declared gets stopped; an unchanged sub keeps the same subscription id.

Full suite: 4691/4697. The 6 failures are pre-existing and environmental (5 × live-Postgres roundtrip needing a DB, 1 × terminal-type detection) — untouched by this change.

merklebonsai and others added 2 commits July 13, 2026 23:13
`setup_subscriptions/1` ran once, from `init/1`, against the initial model.
Subscriptions were therefore frozen: an app whose `subscribe/1` depends on
state -- poll only while a job runs, tick only the currently-selected view --
declared them, and the runtime never asked again, so they were never started.

The playground is the visible case. `Playground.App.subscribe/1` returns the
selected demo's subscriptions, but at mount the selection is the first widget
(no ticks), so every tick-driven demo -- the scroll-anchor stream, easing,
sparkline -- was dead on arrival.

Subscriptions are a function of the model, so derive them from it: after every
update, re-run `subscribe/1`, diff against what is running, start what is newly
declared and stop what is no longer declared. A `%Subscription{type, data}` is
its own identity, so an unchanged subscription keeps its existing timer rather
than being torn down and restarted on every keystroke.
@DROOdotFOO
DROOdotFOO merged commit 881dc65 into DROOdotFOO:master Jul 13, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants