hecate_om_pubsub (hecate_om v0.19.0)
View SourcePublish onto the mesh via macula's supervised macula_publisher, instead of every service hand-rolling catch macula:publish(...).
Confirmed by a 2026-08-24 workspace-wide survey (see hecate-om/plans/PLAN_HECATE_OM_MESH_WRAPPERS.md, piece C): ~20 hecate-services/* repos independently wrote the same
case {hecate_om:macula_client(), hecate_om_identity:realm()} of {{ok, Pool}, {ok, Realm}} -> catch macula:publish(...), ok; _ -> ok end
idiom, in three different flavors of what to do with the outcome (silently discard it, log it, or return it to the caller). Building this against macula_publisher rather than a raw synchronous call gets every caller two things none of the hand-rolled sites had: cancellable in-flight publishes, and free pubsub.publish_started_v1 / pubsub.publish_completed_v1 mesh facts around every publish.
{error, mesh_unavailable} when this service isn't attached to a pool or has no realm configured yet — same contract as hecate_om:mesh_handles/0, so a caller that already checks for that shape elsewhere in its own code doesn't need a second case to learn.
Summary
Functions
Declare the desired subscription set: one supervised macula_subscriber per {Topic, HandlerMod, Args} not already running (HandlerMod implementing the macula_subscriber behaviour); any running one no longer in the set is stopped. Safe to call repeatedly whenever the desired set changes at runtime — e.g. a federation_inbox-shaped service adding one topic per newly- registered entity — diffs against what's currently running and touches only the delta. Self-heals on a 30s reconcile tick independent of any caller: a topic that couldn't start because the mesh wasn't attached yet, or whose child exhausted its own restart budget after a full pool replacement, is retried automatically. See hecate_om_pubsub_subscriptions for the mechanism.
Publish Payload on Topic using this service's own mesh handle and realm. Default mode is async_silent: starts the publish and returns ok immediately without waiting for it to land, matching how most real call sites in the fleet already behave — nobody blocks on a fact publish today.
As publish/2, with Opts
Publish Payload on every topic in Topics. Convenience for a one-fact-fans-to-N-topics service (hecate-news publishes to a firehose topic plus one sub-topic per non-empty axis). Every topic is attempted regardless of an earlier one's outcome; returns ok only if every publish returned ok.
The outcome-handling mode a publish actually uses: Opts's mode, defaulting to async_silent.
The realm a publish actually uses: Opts's realm override when given, otherwise this service's own default realm.
Start a supervised macula_publisher with a caller-supplied callback Module, using this service's own mesh handle and realm — the escape hatch for outcome handling publish/2,3's three fixed modes don't cover (e.g. retry-with-backoff on failure, fully decoupled from any caller waiting synchronously — a real need that just hasn't shown up in a surveyed repo yet, unlike the three modes, which were each derived from one). Resolves hecate_om:mesh_handles/0 the same way publish/2,3 does, so reaching for this instead of publish/2,3 doesn't mean duplicating that boilerplate — that resolution is the one thing worth getting from hecate_om_pubsub either way. Module must implement -behaviour(macula_publisher) itself; hecate_om_pubsub has no say over what its handle_published/2 does.
As start_publisher/3, with Args passed to Module:init/1.
As start_publisher/4, with Opts (realm only — see above).
Types
-type mode() :: async_silent | async_log | sync.
-type publish_opts() :: #{realm => binary(), mode => mode(), timeout => pos_integer()}.
Functions
Declare the desired subscription set: one supervised macula_subscriber per {Topic, HandlerMod, Args} not already running (HandlerMod implementing the macula_subscriber behaviour); any running one no longer in the set is stopped. Safe to call repeatedly whenever the desired set changes at runtime — e.g. a federation_inbox-shaped service adding one topic per newly- registered entity — diffs against what's currently running and touches only the delta. Self-heals on a 30s reconcile tick independent of any caller: a topic that couldn't start because the mesh wasn't attached yet, or whose child exhausted its own restart budget after a full pool replacement, is retried automatically. See hecate_om_pubsub_subscriptions for the mechanism.
Publish Payload on Topic using this service's own mesh handle and realm. Default mode is async_silent: starts the publish and returns ok immediately without waiting for it to land, matching how most real call sites in the fleet already behave — nobody blocks on a fact publish today.
-spec publish(binary(), term(), publish_opts()) -> ok | {error, term()}.
As publish/2, with Opts:
realm — publish on a realm other than this service's own. A real, deployed need (see hecate-dronex, hecate-robo-rumbler, hecate-biotope/hecate-society in the survey): a fleet realm for hecate_om's own plumbing and a separate business/public realm for the facts themselves, off the same pool. mode — async_silent (default): fire-and-forget, outcome discarded. async_log: fire-and-forget, but a failed publish is logged (hecate-victron/hecate-warden/hecate-sentinel all re-added this after an earlier silent-swallow version ate refused frames unnoticed). sync: blocks until the publish resolves and returns its outcome (hecate-biotope/hecate-society/ hecate-mpong-bot all already block their own caller today; this just moves that blocking here). timeout — sync mode only. Milliseconds to wait for the outcome before returning {error, timeout}. Default 5000.
Publish Payload on every topic in Topics. Convenience for a one-fact-fans-to-N-topics service (hecate-news publishes to a firehose topic plus one sub-topic per non-empty axis). Every topic is attempted regardless of an earlier one's outcome; returns ok only if every publish returned ok.
-spec publish_many([binary()], term(), publish_opts()) -> ok | {error, term()}.
-spec resolve_mode(publish_opts()) -> mode().
The outcome-handling mode a publish actually uses: Opts's mode, defaulting to async_silent.
-spec resolve_realm(publish_opts(), binary()) -> binary().
The realm a publish actually uses: Opts's realm override when given, otherwise this service's own default realm.
Start a supervised macula_publisher with a caller-supplied callback Module, using this service's own mesh handle and realm — the escape hatch for outcome handling publish/2,3's three fixed modes don't cover (e.g. retry-with-backoff on failure, fully decoupled from any caller waiting synchronously — a real need that just hasn't shown up in a surveyed repo yet, unlike the three modes, which were each derived from one). Resolves hecate_om:mesh_handles/0 the same way publish/2,3 does, so reaching for this instead of publish/2,3 doesn't mean duplicating that boilerplate — that resolution is the one thing worth getting from hecate_om_pubsub either way. Module must implement -behaviour(macula_publisher) itself; hecate_om_pubsub has no say over what its handle_published/2 does.
Args is passed to Module:init/1, same as macula_publisher:start_link/6 itself; default undefined when omitted. Opts accepts realm only (see publish/3) — mode and timeout don't apply, since the caller's own Module owns the outcome entirely.
As start_publisher/3, with Args passed to Module:init/1.
-spec start_publisher(module(), binary(), term(), term(), #{realm => binary()}) -> {ok, pid()} | {error, term()}.
As start_publisher/4, with Opts (realm only — see above).