Subscriptions, invalidation hints, and coalescing.
2 and This is what makes a component pushable — able to update without the user doing anything.
A hint says "topic X changed, re-derive". It carries no payload, and there is deliberately no event handler: a hint triggers the same rebuild a reconnect uses. That is not a simplification, it is the correctness argument. A data-carrying event is wrong under every bus failure mode — dropped leaves the view permanently wrong, reordered applies stale data last, duplicated breaks any non-idempotent handler — and is correct only on an ordered exactly-once bus. Redis pub/sub is fire-and-forget; NATS core is at-most-once. Since "bring your own bus" is a goal, the engine has to be correct on the weakest plausible one.
With no handler, a drifting handler cannot be written, so the convergence invariant holds structurally rather than by discipline.
Because hints carry nothing, N hints for a topic are indistinguishable from one.
So they collapse into a single rebuild with no information lost — a property
data-carrying events could never have. flush-dirty! drains a dirty-topic set;
a burst of 100 hints on a hot topic produces one rebuild per affected context
rather than 100.
Measured on 1000 subscribed contexts: 100 hints without coalescing meant 100,000
:mount calls; with coalescing, 1,000. for the remaining amplification
(one query per viewer); remuda.cache explains why sharing that query has to
be opt-in.
PubSub is a two-method protocol. The engine never implements it — Redis, NATS,
core.async, or a bare atom for a single process are all the caller's choice. The
engine only needs "deliver this topic to this process"; ordering, delivery
guarantees and clustering are the bus's business, and the design above means none
of them affect correctness.
Subscriptions, invalidation hints, and coalescing. 2 and This is what makes a component *pushable* — able to update without the user doing anything. ## Hints, never data A hint says "topic X changed, re-derive". It carries no payload, and there is deliberately **no event handler**: a hint triggers the same rebuild a reconnect uses. That is not a simplification, it is the correctness argument. A data-carrying event is wrong under every bus failure mode — dropped leaves the view permanently wrong, reordered applies stale data last, duplicated breaks any non-idempotent handler — and is correct only on an ordered exactly-once bus. Redis pub/sub is fire-and-forget; NATS core is at-most-once. Since "bring your own bus" is a goal, the engine has to be correct on the weakest plausible one. With no handler, a drifting handler cannot be written, so the convergence invariant holds structurally rather than by discipline. ## Coalescing is lossless, and that is the point Because hints carry nothing, N hints for a topic are indistinguishable from one. So they collapse into a single rebuild with no information lost — a property data-carrying events could never have. `flush-dirty!` drains a dirty-topic set; a burst of 100 hints on a hot topic produces one rebuild per affected context rather than 100. Measured on 1000 subscribed contexts: 100 hints without coalescing meant 100,000 `:mount` calls; with coalescing, 1,000. for the remaining amplification (one query per viewer); `remuda.cache` explains why sharing that query has to be opt-in. ## The bus is yours `PubSub` is a two-method protocol. The engine never implements it — Redis, NATS, core.async, or a bare atom for a single process are all the caller's choice. The engine only needs "deliver this topic to this process"; ordering, delivery guarantees and clustering are the bus's business, and the design above means none of them affect correctness.
(contexts-for reg topics)Live contexts subscribed to any of topics.
Live contexts subscribed to any of `topics`.
(flush-dirty! reg)Atomically drains the dirty set and returns the contexts that must rebuild.
Draining and reading must be one operation: a hint arriving mid-flush has to
land in the next window rather than being dropped, or a lost hint means a
permanently stale view. swap-vals! gives that atomically.
Returns {:topics #{...} :contexts #{...}}. Callers rebuild each context once,
however many topics dirtied it — which is the coalescing win.
Atomically drains the dirty set and returns the contexts that must rebuild.
Draining and reading must be one operation: a hint arriving mid-flush has to
land in the *next* window rather than being dropped, or a lost hint means a
permanently stale view. `swap-vals!` gives that atomically.
Returns `{:topics #{...} :contexts #{...}}`. Callers rebuild each context once,
however many topics dirtied it — which is the coalescing win.(local-pubsub)An in-process PubSub, backed by an atom.
Sufficient for a single server, for tests, and for development. A multi-server deployment supplies a real bus instead — the engine cannot tell the difference, which is the point of the seam.
An in-process `PubSub`, backed by an atom. Sufficient for a single server, for tests, and for development. A multi-server deployment supplies a real bus instead — the engine cannot tell the difference, which is the point of the seam.
(publish-and-flush! reg bus topic)Convenience for tests and single-process use: publish then immediately flush.
Real deployments flush on a timer, so hints arriving close together coalesce. Flushing per publish defeats that, so this is deliberately not what the engine does in production.
Convenience for tests and single-process use: publish then immediately flush. Real deployments flush on a timer, so hints arriving close together coalesce. Flushing per publish defeats that, so this is deliberately not what the engine does in production.
A hint channel. Deliberately minimal: the engine needs no ordering, no delivery guarantee, and no replay, because a hint carries no information beyond its topic.
A hint channel. Deliberately minimal: the engine needs no ordering, no delivery guarantee, and no replay, because a hint carries no information beyond its topic.
(publish! this topic)Announce that topic changed. Fire-and-forget; may be lost.
Announce that `topic` changed. Fire-and-forget; may be lost.
(subscribe! this topic handler)Register handler, a 1-arg fn of topic, to be called on hints for topic.
Returns a 0-arg unsubscribe fn.
Register `handler`, a 1-arg fn of topic, to be called on hints for `topic`. Returns a 0-arg unsubscribe fn.
(subscribe-context! reg bus context-id topics)Subscribes context-id to topics, replacing any previous subscription set.
Diffs against the current set rather than resubscribing wholesale, because
:subscribe can depend on params: when params change, some topics persist and
should not be torn down and re-established. This is the same
add/remove-by-identity problem as keyed children and hot-reload preservation
— the third place it appears.
Returns the topics actually added and removed, which is what makes the diffing testable rather than assumed.
Subscribes `context-id` to `topics`, replacing any previous subscription set. Diffs against the current set rather than resubscribing wholesale, because `:subscribe` can depend on params: when params change, some topics persist and should not be torn down and re-established. This is the same add/remove-by-identity problem as keyed children and hot-reload preservation — the third place it appears. Returns the topics actually added and removed, which is what makes the diffing testable rather than assumed.
(unsubscribe-context! reg context-id)Removes every subscription for context-id. Called on disconnect.
Removes every subscription for `context-id`. Called on disconnect.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |