hecate_pubsub (macula v11.5.0)

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 publish: build_event/2 makes the EVENT for a PUBLISH, carrying its publication bytes unchanged, and the wrapper hands it to Plumtree for fan-out. The publisher signs the publication once and no hop re-signs it, so every subscriber verifies it end to end (D17).
  • Receive EVENT: deliver_event/2 verifies the EVENT's publication under the state's crypto profile and returns the local subscribers whose subscription matches its realm and topic; a publication that does not verify reaches no one. 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

The EVENT for a PUBLISH: its publication bytes, unchanged, and how the EVENT was delivered.

Match an incoming EVENT frame to local subscribers. Its publication is verified first, under the state's crypto profile and the clock; the result is empty when it does not verify, when its realm is not this state's (defensive: the transport should already route by realm), or when 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.

The PubSub state for a realm, verifying publications under Profile.

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>>,
            profile := macula_crypto_profile:profile(),
            subscriptions := #{topic() => sets:set(subscriber())},
            patterns := #{topic() => sets:set(subscriber())}}.

subscriber/0

-type subscriber() :: <<_:256>>.

topic/0

-type topic() :: binary().

Functions

build_event(_, Via)

-spec build_event(macula_frame:frame(), plumtree | direct) -> macula_frame:frame().

The EVENT for a PUBLISH: its publication bytes, unchanged, and how the EVENT was delivered.

deliver_event(State, Frame)

-spec deliver_event(state(), macula_frame:frame()) -> [subscriber()].

Match an incoming EVENT frame to local subscribers. Its publication is verified first, under the state's crypto profile and the clock; the result is empty when it does not verify, when its realm is not this state's (defensive: the transport should already route by realm), or when 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, Profile)

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

The PubSub state for a realm, verifying publications under Profile.

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(), <<_:256>>, 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().