hecate_pubsub (macula v10.14.4)
View SourceRealm-scoped PubSub state + dispatch (Part 6 §6).
Holds the topic-to-subscriber index for one realm and converts incoming SUBSCRIBE / UNSUBSCRIBE / EVENT frames into local state mutations + delivery instructions. The wire layer that actually transmits frames lives elsewhere — typically hecate_plumtree for intra-realm fan-out.
Pipeline
- Local subscribe —
subscribe/3adds the subscriber to the topic's set. The wrapper builds a SUBSCRIBE frame for upstream propagation if needed. - Local publish —
build_event/3takes a PUBLISH spec and produces a signed EVENT frame the wrapper hands to Plumtree for fan-out. The publisher signs the EVENT once; intermediate hops do NOT re-sign, so every subscriber can verify authenticity end-to-end (Part 6 §6.4). - Receive EVENT —
deliver_event/2returns the list of local subscribers whose subscription matches the event's topic + realm. The wrapper notifies each via the application channel. - Receive SUBSCRIBE / UNSUBSCRIBE —
process/3updates local state.
State is per realm: an instance handles one realm's topics only. Cross-realm leakage is impossible — the realm is baked into the state and every dispatch checks it.
Wildcard subscriptions (2026-08-29, station-local only)
subscribe/3 with a topic containing a literal * segment (see macula_topic_pattern:matches/2) registers a PATTERN instead of an exact topic — kept in a SEPARATE patterns map, not mixed into subscriptions, so the common case (no wildcard subscribers in this realm) pays zero extra cost on delivery: subscribers/2 only scans patterns when map_size(patterns) > 0.
topics/1 returns subscriptions's keys only, still deliberately — macula_station_peering_router treats every entry in topics/1 as local interest worth re-subscribing on every peer and folding into the Bloom-gossip summary, and a Bloom filter tests exact-string membership: gossiping a raw *-bearing string INTO THE BLOOM would be meaningless (it can only ever match itself, never the concrete topics it was meant to stand in for).
patterns/1 (2026-08-29) is the separate, purpose-built export for mesh-wide wildcard propagation: macula_station_bloom_exchange gossips the raw pattern SET on its own _mesh.patterns topic (patterns are expected to be few — no Bloom-summarization needed, unlike the exact-topic set) and matches a concrete publish against every peer's gossiped patterns directly via macula_topic_pattern:matches/2 at fan-out time, entirely separate from the Bloom path. See macula-station/plans/PLAN_ORG_SCOPED_DISPATCH_AND_WILDCARD_DISCOVERY.md, slice 5.
Reference: plans/PLAN_MACULA_V2_PART6_PROTOCOL.md §6; plans/PLAN_PHASE_5_BREAKDOWN.md Session 5.5.
Summary
Functions
Match an incoming EVENT frame to local subscribers. Returns an empty list if the realm doesn't match (defensive — the transport should already route by realm) or no-one is subscribed.
Whether Sub is registered under the LITERAL string Topic — exact or pattern, whichever map it actually lives in. Distinct from "would Sub receive a publish to Topic'" (that question is subscribers/2): a subscriber registered under a pattern is not is_subscribed for one of the concrete topics that pattern matches, only for the pattern string itself.
Every wildcard pattern registered in this realm — the keys of the separate patterns map (never mixed with topics/1's exact keys). Feeds macula_station_bloom_exchange's own, separate _mesh.patterns gossip — see moduledoc.
Remove Sub from every topic in this realm, dropping any topic whose subscriber set becomes empty as a result — the same drop_or_keep/3 rule unsubscribe/3 applies to one topic, fanned out across all of them in one pass.
Every subscriber that would receive a publish to Topic — exact subscribers plus, when this realm has any registered, every wildcard pattern that matches Topic. Topic itself is always concrete here (a publish never carries a wildcard); a caller passing a *-bearing string gets whatever literal entry (if any) happens to exist under that exact string in subscriptions — patterns match AGAINST concrete topics, not against each other.
Types
-type state() :: #{realm := <<_:256>>, subscriptions := #{topic() => sets:set(subscriber())}, patterns := #{topic() => sets:set(subscriber())}}.
-type subscriber() :: macula_identity:pubkey().
-type topic() :: binary().
Functions
-spec build_event(state(), macula_frame:publish_spec(), macula_identity:key_pair()) -> macula_frame:frame().
-spec deliver_event(state(), macula_frame:frame()) -> [subscriber()].
Match an incoming EVENT frame to local subscribers. Returns an empty list if the realm doesn't match (defensive — the transport should already route by realm) or no-one is subscribed.
-spec is_subscribed(state(), topic(), subscriber()) -> boolean().
Whether Sub is registered under the LITERAL string Topic — exact or pattern, whichever map it actually lives in. Distinct from "would Sub receive a publish to Topic'" (that question is subscribers/2): a subscriber registered under a pattern is not is_subscribed for one of the concrete topics that pattern matches, only for the pattern string itself.
-spec new(<<_:256>>) -> state().
Every wildcard pattern registered in this realm — the keys of the separate patterns map (never mixed with topics/1's exact keys). Feeds macula_station_bloom_exchange's own, separate _mesh.patterns gossip — see moduledoc.
-spec process(state(), macula_identity:pubkey(), macula_frame:frame()) -> {state(), [subscriber()]}.
-spec purge_subscriber(state(), subscriber()) -> state().
Remove Sub from every topic in this realm, dropping any topic whose subscriber set becomes empty as a result — the same drop_or_keep/3 rule unsubscribe/3 applies to one topic, fanned out across all of them in one pass.
For a peer or daemon that disconnects without sending UNSUBSCRIBE for everything it held: without this, a topic whose only subscriber was that departed connection never empties, so it never leaves topics/1 — and macula_station_peering_router (which treats every entry in topics/1 as local interest worth re-subscribing on every peer, regardless of whether the original subscriber was a peer-sourced entry) keeps re-propagating it mesh-wide forever. See macula-station/plans/DESIGN_SUBSCRIPTION_LIFECYCLE_GC.md.
-spec subscribe(state(), topic(), subscriber()) -> state().
-spec subscriber_count(state()) -> non_neg_integer().
-spec subscribers(state(), topic()) -> [subscriber()].
Every subscriber that would receive a publish to Topic — exact subscribers plus, when this realm has any registered, every wildcard pattern that matches Topic. Topic itself is always concrete here (a publish never carries a wildcard); a caller passing a *-bearing string gets whatever literal entry (if any) happens to exist under that exact string in subscriptions — patterns match AGAINST concrete topics, not against each other.
-spec topic_count(state()) -> non_neg_integer().
-spec unsubscribe(state(), topic(), subscriber()) -> state().