hecate_pubsub (macula v10.20.2)

View Source

Realm-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 subscribesubscribe/3 adds the subscriber to the topic's set. The wrapper builds a SUBSCRIBE frame for upstream propagation if needed.
  • Local publishbuild_event/3 takes 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 EVENTdeliver_event/2 returns the list of local subscribers whose subscription matches the event's topic + realm. The wrapper notifies each via the application channel.
  • Receive SUBSCRIBE / UNSUBSCRIBEprocess/3 updates 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

state/0

-type state() ::
          #{realm := <<_:256>>,
            subscriptions := #{topic() => sets:set(subscriber())},
            patterns := #{topic() => sets:set(subscriber())}}.

subscriber/0

-type subscriber() :: macula_identity:pubkey().

topic/0

-type topic() :: binary().

Functions

build_event(_, _, Identity)

deliver_event(State, 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.

is_subscribed(_, Topic, Sub)

-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.

new(Realm)

-spec new(<<_:256>>) -> state().

patterns(_)

-spec patterns(state()) -> [topic()].

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.

process(State, From, F)

-spec process(state(), macula_identity:pubkey(), macula_frame:frame()) -> {state(), [subscriber()]}.

purge_subscriber(State, Sub)

-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.

realm(_)

subscribe(State, Topic, Sub)

-spec subscribe(state(), topic(), subscriber()) -> state().

subscriber_count(_)

-spec subscriber_count(state()) -> non_neg_integer().

subscribers(_, Topic)

-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.

topic_count(_)

-spec topic_count(state()) -> non_neg_integer().

topics(_)

-spec topics(state()) -> [topic()].

unsubscribe(State, Topic, Sub)

-spec unsubscribe(state(), topic(), subscriber()) -> state().