hecate_pubsub (macula v10.13.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.

Deliberately NOT propagated cross-station: topics/1 returns subscriptions's keys only. 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 — a Bloom filter tests exact-string membership, so gossiping a raw *-bearing string would be meaningless (it can only ever match itself, never the concrete topics it was meant to stand in for) and would pollute the gossip layer for no benefit. A wildcard subscriber therefore only ever receives a publish that reaches THIS realm instance directly — same station as the publisher, or already fanned here via the ordinary (exact-topic) gossip/relay path. Mesh-wide wildcard subscription (matching cross-station, not just locally) is a separate, bigger piece of work — 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.

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

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