macula (macula v11.4.0)
View SourceMacula SDK — Public API for mesh applications.
This is the main entry point for applications using the Macula SDK.
Apps connect via connect/2, which returns a macula_client pool that internally wraps N peering links to N stations. publish/4,5, subscribe/4,5, unsubscribe/2, call/5, advertise/5, unadvertise/3, call_stream/5, advertise_stream/5, and unadvertise_stream/3 route through the pool with realm-per-call semantics. See macula_pubsub for the slice module of the publish/subscribe surface.
LOCAL streaming (call_stream/2,3, open_stream/3,4, advertise_stream/2,3, unadvertise_stream/1) dispatches in-process via macula_stream_local — for unit tests and same-BEAM pairs.
Erlang distribution over the mesh ships via join_mesh/1 (V2 pool carrier) or join_dist_relay/1 (dedicated dist relay). See macula_dist_pool / macula_dist_relay_client.
Summary
Functions
Abort the stream with an error frame.
Register a procedure handler on a V2 pool and advertise it: the pool resolves its own D25 provider authorization — the realm-signed org_directory and the org-signed procedure_delegation that names the pool's node id, both fetched from the DHT — signs the advertisement, and sends it as an ADVERTISE frame to every link (replayed on link respawn). The procedure must carry an org namespace, and the pool must run a provisioned identity whose delegation the org has published, and pin the realm's key (realm_trust at connect); a missing piece fails fast with {error, {provider_authorization, _}}.
Advertise a LOCAL in-process streaming procedure (default: server_stream).
Advertise a LOCAL in-process streaming procedure with mode.
Register a streaming procedure handler on a V2 pool. Fans out to every healthy link and stores in pool state for replay on link respawn. A caller reaches this provider only through a procedure_advertisement record that names it; registering the handler publishes none. See macula_client:advertise_stream/5.
As advertise_stream/5, with Opts. auth sets the streaming procedure's policy, the same set advertise/5 takes: open (default), {ucan_required, Issuer} or {realm_member_required, RealmDid, RequiredCan}. A consumer presents its token with call_stream/5's ucan_token opt.
Wait for the terminal reply (client-stream / bidi).
Call Procedure in Realm at the provider that serves it: resolve the procedure's verified advertisements, authorized against the realm key the pool pinned for Realm, reach the station a candidate names directly, and call its provider there. See macula_direct_dial:call/5. A procedure the pool's linked stations serve themselves, such as _dht.*, goes through macula_client:call_linked_station/5.
Issue a CALL to Target, a provider's node_id, at ONE specific station, dialing it directly even if it is not in the pool's seed set. Station is a seed URL (e.g. <<"quic://[::1]:4433">>). The pool reuses an existing link or dials and monitors a new one, waits for the handshake, and calls there. This is the direct-dial data path: resolve a provider's serving_station and its endpoint, then reach it in one hop. See macula_client:call_station/7.
As call_station/7, presenting a capability token to a gated provider via Opts (#{ucan_token => Token}). Empty/absent = none. Slice 7b dual-trust. Opts also carries the per-call TLS trust override for this dial: verify, expected_node_id, and pin_tls_cert (see macula_client:call_station/9), and may set dial_timeout_ms, how much of TimeoutMs the wait for a fresh link's handshake may take (default: all of it).
Open a LOCAL in-process server-stream call. Used for unit tests and same-BEAM dispatch via macula_stream_local.
Open a LOCAL in-process server-stream call with options.
Open a streaming RPC to Procedure's provider in Realm: resolve the provider through its procedure_advertisement and open the stream at its serving station, naming the provider as the target, as call/5 does for a single-reply call. Same as macula_direct_dial:call_stream/5. The returned stream is bound to that station's link (errors with peer_down if the link dies; caller re-opens). Optsucan_token presents a UCAN to a streaming procedure advertised with an auth policy (see advertise_stream/6). An open whose signed STREAM_OPEN would be longer than max_stream_open_bytes (1 MiB by default) returns {error, {open_too_large, Limit}} without sending anything.
Open a streaming RPC to Target, a provider's node_id, by DIALING a specific station directly (direct-dial): the streaming analogue of call_station/7. Compose it with DHT resolution (find_records -> read_procedure_advertisement -> station_endpoint) to reach a stream provider in one hop, exactly as a unary caller does. Opts may set dial_timeout_ms (default 10_000) and a mode. Opts also carries the per-call TLS trust override for this dial: verify, expected_node_id, pin_tls_cert (see macula_client:call_station/8).
OTP child spec to drop a V2 pool into a caller's supervision tree. Give node_identity as a loader {Module, Function, Args} that returns {ok, Key}. Its Args say where the key is and never hold the key, because a supervisor logs them when a start fails. See macula_client:child_spec/3.
Stop a V2 pool. Every subscriber receives a final {macula_event_gone, SubRef, pool_closed} message.
Half-close the write side; recv still drains.
Connect to the Macula relay mesh and return a pool handle.
The dist relay client that join_dist_relay/1 started, if it is running.
Ensure this node is running in distributed mode.
A field of a map a peer supplied, or undefined when it is absent. See field/3.
A field of a map a peer supplied (D26), or Default when it is absent. A map from the codec carries its text keys as {text, Bin}; a map handed over in process may carry atom or binary keys. The lookup tries {text, Name}, then the atom, then the binary, so a handler reads both kinds of map the same way. Looking up a binary name never creates an atom.
Resolve every host currently announcing an MCID: hosts that stored a chunked put (_content.put_manifest) and got content_announcementd automatically by the station on receipt (macula_content_announcer). get_content/2 already reaches a copy via the connected station's own 1-hop peer relay, so this is for a caller that wants to know WHO holds an MCID, or to dial a specific one directly with get_content_station/4,5 — e.g. when the connected station's relay hop budget does not reach the host (a partial-mesh pair with no mutual peer), or to route around a specific host deliberately.
Fetch a record from the mesh DHT by its macula_record:storage_key/1.
As find_record/2, waiting at most TimeoutMs for the reply, for a caller that bounds its work by a deadline of its own.
Fetch EVERY record stored at Key — the full multi-value set, e.g. every procedure_advertisement under one procedure's storage key. Where find_record/2 returns the first record (or not_found), this returns the whole list, empty when none.
As find_records/2, waiting at most TimeoutMs for the reply, for a caller that bounds its work by a deadline of its own.
Return every record of a given type currently visible from the pool's connected stations, each verified under the node's crypto profile; a record that does not verify is dropped.
Fetch the bytes for a previously-stored MCID. Returns {error, not_found} if no provider in the pool's reach holds a copy (for chunked content, if any single chunk is unreachable). Dispatches on the MCID's codec byte: 16#55 (raw/single-block) fetches one block, BLAKE3-verified by the station before it leaves the store; 16#56 (manifest) fetches the manifest, then every chunk in order, reassembles, and verifies the whole against the manifest's size and Merkle root before returning.
As get_content/2, dialing Station directly (reusing a live link or dialing + waiting up to TimeoutMs for one) instead of picking from the pool's existing links — the content-transfer counterpart to call_station/7. Station and TimeoutMs mean exactly what they do there; the underlying block/manifest transfer has its own internal timeouts regardless of TimeoutMs, which bounds only the connect wait. See find_content_providers/2 to resolve a station to dial, or macula_direct_dial:get_content/3 to resolve-and-fetch in one call.
As get_content_station/4, with a per-call TLS trust override for this dial — verify, expected_node_id, pin_tls_cert (see call_station/8). See get_content/2 on why a malformed MCID is rejected here rather than reaching macula_content_transfer.
Enable Erlang distribution over a dedicated dist relay (macula-io/macula-dist-relay).
Join the Macula relay mesh with Erlang distribution.
Per-link snapshot of a V2 pool — one entry per spawned link with its peer station node_id (pubkey), dial host, pid, and connected flag. Use this to resolve a specific station (by pubkey or hostname) to its link for targeted, per-station operations. See macula_client:links/1 for the link_info() shape.
Subscribe to node up/down events.
Open a LOCAL in-process client-stream or bidi call. Used for unit tests and same-BEAM dispatch via macula_stream_local.
Open a LOCAL in-process stream with explicit mode.
Parse a MACULA_STATIONS seed list: comma-separated <node id>@<host>:<port> entries, the node id as 64 lowercase hex characters, the host a DNS name, an IPv4 address or a bracketed IPv6 address, the port 1 to 65535. Returns the seeds in order, each pinned to its node id. A refusal names the entry's position and what is wrong with it, and carries neither the value, nor a node id, nor a host. See macula_stations:parse/1.
Resolve this pool's own D25 provider authorization for an org-namespaced Procedure under Realm — the realm-signed org_directory and the org-signed procedure_delegation naming the pool's node id, both fetched from the DHT and verified against the realm key the pool pinned for Realm at connect — as the authorization opt macula_direct_dial:publish_advertisement/5 and macula_response:advertise_direct/6,7 take: a map of the two records' encoded wire forms. advertise/5 resolves this same chain itself for the wire frame; a caller that publishes the direct-dial record needs it explicitly, since the station refuses an org-namespaced record without one (no_authorization).
As provider_authorization/3, reading the resolution's DHT calls from the provider_io/0 seam entries in Opts (defaults to the facade's own functions) — the seam advertise/5 accepts too.
Publish to (Realm, Topic) on Pool. Equivalent to publish/5 with empty opts.
Publish to (Realm, Topic) on Pool with options. See macula_pubsub:publish/5 for honored opts.
Store Bytes in the mesh's content store and return its MCID (Macula Content ID, 50 bytes: tag 2 for SHA-384, codec, then the 48-byte hash). Content that fits in one block (byte_size(Bytes) =< macula_manifest:default_chunk_size/0, 256 KiB) is sent as a single _content.put_block, and the MCID is <<2, 16#55, SHA-384(Bytes)>>. Larger content is split into chunks (macula_manifest:create/1), each chunk sent via its own _content.put_block, then a content_manifest via _content.put_manifest; the returned MCID is the manifest's (<<2, 16#56, _/binary>>), Merkle-rooted over every chunk. Either way the station verifies each block's hash before accepting it.
As put_content/2, dialing Station directly (reusing a live link or dialing + waiting up to TimeoutMs for one) instead of picking from the pool's existing links — the content-transfer counterpart to call_station/7. Station and TimeoutMs mean exactly what they do there; the underlying block/manifest transfer has its own internal timeouts regardless of TimeoutMs, which bounds only the connect wait. See macula_direct_dial:put_content/4 to resolve a station by identity and put in one call.
As put_content_station/4, with a per-call TLS trust override for this dial — verify, expected_node_id, pin_tls_cert (see call_station/8).
Store a signed record in the mesh DHT via a V2 pool.
Receive the next chunk (blocks).
Send a binary chunk on the stream.
Send a chunk with explicit encoding.
Server-side: emit the terminal reply value.
Sign a domain record (tags 0x20 to 0xFF) as this node, with the pool's node identity key, in the pool's own process. See macula_client:sign_domain_record/2.
Sign a record this node signs about itself with the pool's node identity key, in the pool's own process: a node record, a procedure advertisement or a content announcement that names this node. See macula_client:sign_node_record/2 for the record checks and refusals. Optsnot_after bounds the signed record's expiry: the pool judges it on its own clock, refuses a bound already passed as {error, not_after_passed}, ends the record at the bound when the bound comes before the record's lifetime runs out, and keeps the built lifetime otherwise.
Aggregate health snapshot of a V2 pool. Suitable for /health or /status endpoints; not for hot-loop polling. See macula_client:status/1 for the full shape.
Subscribe Subscriber to (Realm, Topic) on Pool. Equivalent to subscribe/5 with empty opts.
Subscribe Subscriber to (Realm, Topic) on Pool with options. The delivery option chooses how a single publisher's out-of-order arrivals are handled
Subscribe with a callback function. The SDK spawns a small receiver process internally and invokes the callback once per inbound event. See macula_pubsub:subscribe_callback/4.
Subscribe to live record-stored events filtered by type.
The binary of a text value a peer supplied (D26): the binary of {text, Bin}, a binary unchanged, and badarg for anything else.
Stop advertising a procedure on a V2 pool.
Stop advertising a LOCAL streaming procedure.
Stop advertising a streaming procedure on a V2 pool.
Unsubscribe from node up/down events.
Drop a pool subscription. Idempotent.
Cancel a subscribe_records/3 subscription.
Withdraw a record this node signed — a node record, a procedure advertisement, a content announcement or a domain record — with a tombstone signed by the pool's node identity key, in the pool's own process. See macula_client:withdraw_node_record/3.
Types
-type m_record() :: macula_record:m_record().
-type mcid() :: <<_:400>>.
-type pool() :: macula_client:pool().
-type procedure() :: binary().
-type provider_io() :: #{status => fun((pool()) -> {ok, macula_client:status()} | {error, term()}), find_record => fun((pool(), record_key()) -> {ok, m_record()} | {error, term()}), sign_node_record => fun((pool(), m_record()) -> {ok, m_record()} | {error, term()}), realm_key => fun((pool(), realm()) -> {ok, binary()} | none)}.
-type realm() :: <<_:256>>.
32-byte realm tag.
-type record_key() :: <<_:256>>.
DHT storage key — macula_record:storage_key/1 output.
-type record_type() :: macula_record:type_tag().
-type stream() :: pid().
-type stream_mode() :: server_stream | client_stream | bidi.
-type topic() :: binary().
Functions
Abort the stream with an error frame.
-spec advertise(pool(), realm(), procedure(), macula_client:handler(), map()) -> ok | {error, term()}.
Register a procedure handler on a V2 pool and advertise it: the pool resolves its own D25 provider authorization — the realm-signed org_directory and the org-signed procedure_delegation that names the pool's node id, both fetched from the DHT — signs the advertisement, and sends it as an ADVERTISE frame to every link (replayed on link respawn). The procedure must carry an org namespace, and the pool must run a provisioned identity whose delegation the org has published, and pin the realm's key (realm_trust at connect); a missing piece fails fast with {error, {provider_authorization, _}}.
Optsauth sets the procedure's policy: open (default, serve any identified caller), {ucan_required, Issuer} (gated to one known identity, Slice 7b), or {realm_member_required, RealmDid, RequiredCan} (gated to realm membership at a specific tier) -- see macula_client:auth_policy() for the full set. Opts may also carry advertise, an arity-6 override for the pool fan-out (see macula_response:advertise_opts()), and the provider_io/0 seam entries the resolution reads its DHT calls from.
-spec advertise_stream(procedure(), stream_handler()) -> ok | {error, term()}.
Advertise a LOCAL in-process streaming procedure (default: server_stream).
-spec advertise_stream(procedure(), stream_mode(), stream_handler()) -> ok | {error, term()}.
Advertise a LOCAL in-process streaming procedure with mode.
-spec advertise_stream(pool(), realm(), procedure(), stream_mode(), stream_handler()) -> ok | {error, term()}.
Register a streaming procedure handler on a V2 pool. Fans out to every healthy link and stores in pool state for replay on link respawn. A caller reaches this provider only through a procedure_advertisement record that names it; registering the handler publishes none. See macula_client:advertise_stream/5.
-spec advertise_stream(pool(), realm(), procedure(), stream_mode(), stream_handler(), map()) -> ok | {error, term()}.
As advertise_stream/5, with Opts. auth sets the streaming procedure's policy, the same set advertise/5 takes: open (default), {ucan_required, Issuer} or {realm_member_required, RealmDid, RequiredCan}. A consumer presents its token with call_stream/5's ucan_token opt.
Wait for the terminal reply (client-stream / bidi).
Call Procedure in Realm at the provider that serves it: resolve the procedure's verified advertisements, authorized against the realm key the pool pinned for Realm, reach the station a candidate names directly, and call its provider there. See macula_direct_dial:call/5. A procedure the pool's linked stations serve themselves, such as _dht.*, goes through macula_client:call_linked_station/5.
-spec call_station(pool(), macula_client:seed(), <<_:256>>, realm(), procedure(), term(), 1..600000) -> {ok, term()} | {error, term()}.
Issue a CALL to Target, a provider's node_id, at ONE specific station, dialing it directly even if it is not in the pool's seed set. Station is a seed URL (e.g. <<"quic://[::1]:4433">>). The pool reuses an existing link or dials and monitors a new one, waits for the handshake, and calls there. This is the direct-dial data path: resolve a provider's serving_station and its endpoint, then reach it in one hop. See macula_client:call_station/7.
-spec call_station(pool(), macula_client:seed(), <<_:256>>, realm(), procedure(), term(), 1..600000, map()) -> {ok, term()} | {error, term()}.
As call_station/7, presenting a capability token to a gated provider via Opts (#{ucan_token => Token}). Empty/absent = none. Slice 7b dual-trust. Opts also carries the per-call TLS trust override for this dial: verify, expected_node_id, and pin_tls_cert (see macula_client:call_station/9), and may set dial_timeout_ms, how much of TimeoutMs the wait for a fresh link's handshake may take (default: all of it).
Open a LOCAL in-process server-stream call. Used for unit tests and same-BEAM dispatch via macula_stream_local.
Open a LOCAL in-process server-stream call with options.
Open a streaming RPC to Procedure's provider in Realm: resolve the provider through its procedure_advertisement and open the stream at its serving station, naming the provider as the target, as call/5 does for a single-reply call. Same as macula_direct_dial:call_stream/5. The returned stream is bound to that station's link (errors with peer_down if the link dies; caller re-opens). Optsucan_token presents a UCAN to a streaming procedure advertised with an auth policy (see advertise_stream/6). An open whose signed STREAM_OPEN would be longer than max_stream_open_bytes (1 MiB by default) returns {error, {open_too_large, Limit}} without sending anything.
-spec call_stream_station(pool(), macula_client:seed(), <<_:256>>, realm(), procedure(), term(), map()) -> {ok, stream()} | {error, term()}.
Open a streaming RPC to Target, a provider's node_id, by DIALING a specific station directly (direct-dial): the streaming analogue of call_station/7. Compose it with DHT resolution (find_records -> read_procedure_advertisement -> station_endpoint) to reach a stream provider in one hop, exactly as a unary caller does. Opts may set dial_timeout_ms (default 10_000) and a mode. Opts also carries the per-call TLS trust override for this dial: verify, expected_node_id, pin_tls_cert (see macula_client:call_station/8).
-spec child_spec(term(), [macula_client:seed()], macula_client:opts()) -> supervisor:child_spec().
OTP child spec to drop a V2 pool into a caller's supervision tree. Give node_identity as a loader {Module, Function, Args} that returns {ok, Key}. Its Args say where the key is and never hold the key, because a supervisor logs them when a start fails. See macula_client:child_spec/3.
-spec close(pool()) -> ok.
Stop a V2 pool. Every subscriber receives a final {macula_event_gone, SubRef, pool_closed} message.
-spec close_send(stream()) -> ok.
Half-close the write side; recv still drains.
-spec close_stream(stream()) -> ok.
Close a V1 stream (both sides). Renamed from close/1 in 3.11.0 because close/1 now refers to the V2 pool surface.
-spec connect([macula_client:seed()], macula_client:opts()) -> {ok, pool()} | {error, term()}.
Connect to the Macula relay mesh and return a pool handle.
Seeds is a list of station endpoints: #{host, port, expected_node_id} maps, or URL binaries or strings. Every seed names the node_id it expects, in the seed or in the expected_node_id option; otherwise no pool starts and {error, {seeds, expected_node_id_required}} is returned. The pool spawns one peering link per seed and routes ops with replication, replay, and event dedup. Returns immediately; link handshakes complete asynchronously.
Honored opts (full reference: macula_client:opts()):
node_identity: the pool's node identity key, in the node's crypto profile; generated with a puzzle-solved node_id if absent.realm_trust: the realm keys the pool pins, one per realm id, as#{RealmId => RealmKey}, each realm's public key as carried. A call trusts an org namespaced advertisement only through the key pinned for its realm. Refused as{error, {realm_trust, invalid}}unless every id is 32 bytes and every key is well formed for the node's crypto profile, and as{error, {realm_trust, profile_mismatch}}for a key of the other profile.replication_factor— links per PUBLISH (default 2, since 10.19.0).capabilities— per-link bitfield (default 0).alpn— QUIC ALPN list (default[<<"macula">>]).connect_timeout_ms— per-link CONNECT/HELLO deadline (default 30_000).dedup_sweep_ms: how often the inbound publication dedup table is swept.
Legacy opts silently dropped (with a one-shot logger:notice): relays (use the Seeds positional argument), realm (V2 is realm-per-call), site (no V2 analog), connections (one link per seed; add more seeds to grow the pool).
See macula_client for the canonical pool implementation and macula_pubsub for the slice module.
-spec dist_relay_client() -> {ok, pid()} | {error, not_joined}.
The dist relay client that join_dist_relay/1 started, if it is running.
The client exits with {relay_closed, Reason} when the relay closes the connection and is not restarted. Monitor the returned pid and call join_dist_relay/1 again after it goes down.
-spec ensure_distributed() -> ok | {error, term()}.
Ensure this node is running in distributed mode.
A field of a map a peer supplied, or undefined when it is absent. See field/3.
A field of a map a peer supplied (D26), or Default when it is absent. A map from the codec carries its text keys as {text, Bin}; a map handed over in process may carry atom or binary keys. The lookup tries {text, Name}, then the atom, then the binary, so a handler reads both kinds of map the same way. Looking up a binary name never creates an atom.
Resolve every host currently announcing an MCID: hosts that stored a chunked put (_content.put_manifest) and got content_announcementd automatically by the station on receipt (macula_content_announcer). get_content/2 already reaches a copy via the connected station's own 1-hop peer relay, so this is for a caller that wants to know WHO holds an MCID, or to dial a specific one directly with get_content_station/4,5 — e.g. when the connected station's relay hop budget does not reach the host (a partial-mesh pair with no mutual peer), or to route around a specific host deliberately.
Each entry is verified under the node's crypto profile before its endpoint is trusted: its signature, and that its signer is the announcer_node it names, since macula_record:verify/2 refuses a record whose payload names another signer. Unverifiable records and records of another type are dropped, not surfaced as errors. Single-block content (put via _content.put_block alone) is not announced: resolving its MCID returns {ok, []}.
-spec find_record(pool(), record_key()) -> {ok, m_record()} | {error, not_found | term()}.
Fetch a record from the mesh DHT by its macula_record:storage_key/1.
Returns {error, not_found} when no record exists at the key. The record is verified under the node's crypto profile before it is returned; one that does not verify returns its refusal from macula_record:verify/2, such as {error, expired}.
-spec find_record(pool(), record_key(), pos_integer()) -> {ok, m_record()} | {error, not_found | term()}.
As find_record/2, waiting at most TimeoutMs for the reply, for a caller that bounds its work by a deadline of its own.
-spec find_records(pool(), record_key()) -> {ok, [m_record()]} | {error, term()}.
Fetch EVERY record stored at Key — the full multi-value set, e.g. every procedure_advertisement under one procedure's storage key. Where find_record/2 returns the first record (or not_found), this returns the whole list, empty when none.
The relay's local store is a signer-deduped multiset: one record per signing key at a storage key, so N providers of one procedure return N records. Each record is verified under the node's crypto profile, and a record that does not verify is dropped.
-spec find_records(pool(), record_key(), pos_integer()) -> {ok, [m_record()]} | {error, term()}.
As find_records/2, waiting at most TimeoutMs for the reply, for a caller that bounds its work by a deadline of its own.
-spec find_records_by_type(pool(), record_type()) -> {ok, [m_record()]} | {error, term()}.
Return every record of a given type currently visible from the pool's connected stations, each verified under the node's crypto profile; a record that does not verify is dropped.
Coverage depends on each station's view of the DHT — a single station sees its local replicas plus whatever its peers have gossiped. Aggregating across the full mesh requires querying multiple stations and deduplicating by record key.
Fetch the bytes for a previously-stored MCID. Returns {error, not_found} if no provider in the pool's reach holds a copy (for chunked content, if any single chunk is unreachable). Dispatches on the MCID's codec byte: 16#55 (raw/single-block) fetches one block, BLAKE3-verified by the station before it leaves the store; 16#56 (manifest) fetches the manifest, then every chunk in order, reassembles, and verifies the whole against the manifest's size and Merkle root before returning.
A thin blocking wrapper over macula_content_transfer:start_get/2 + await/1 — see the note on put_content/2. MCID must carry one of the two codec bytes put_content/2 ever mints (16#55 single-block, 16#56 chunked manifest) — anything else can't have come from this SDK's own put path (a corrupted record, a caller's encoding bug, or hostile input on a path that turns user-controlled bytes into an MCID) and is rejected here rather than reaching macula_content_transfer's internal dispatch, whose is_chunked/2 clauses assume this shape and previously crashed the calling process's linked worker on anything else.
-spec get_content_station(pool(), macula_client:seed(), mcid(), pos_integer()) -> {ok, binary()} | {error, not_found | term()}.
As get_content/2, dialing Station directly (reusing a live link or dialing + waiting up to TimeoutMs for one) instead of picking from the pool's existing links — the content-transfer counterpart to call_station/7. Station and TimeoutMs mean exactly what they do there; the underlying block/manifest transfer has its own internal timeouts regardless of TimeoutMs, which bounds only the connect wait. See find_content_providers/2 to resolve a station to dial, or macula_direct_dial:get_content/3 to resolve-and-fetch in one call.
-spec get_content_station(pool(), macula_client:seed(), mcid(), pos_integer(), map()) -> {ok, binary()} | {error, not_found | invalid_mcid | term()}.
As get_content_station/4, with a per-call TLS trust override for this dial — verify, expected_node_id, pin_tls_cert (see call_station/8). See get_content/2 on why a malformed MCID is rejected here rather than reaching macula_content_transfer.
Enable Erlang distribution over a dedicated dist relay (macula-io/macula-dist-relay).
Different from join_mesh/1: - Connects to a dist relay (port 4434, ALPN macula-dist), NOT the pub/sub station mesh - No mesh_client, no pub/sub subscriptions — only dist traffic - Uses raw QUIC stream routing with no MessagePack overhead
Options: - url (required): <<"quic://relay.example.com:4434">>
After this returns ok, standard OTP distribution (rpc:call/4, gen_server:call/3 across nodes, pg groups, etc.) works across firewalls via the dist relay.
The relay client runs as a temporary child of the macula application supervisor. It does not reconnect: when the relay closes the connection the client ends and is not restarted. Monitor the pid from dist_relay_client/0 to learn that, then call this function again. Returns {error, macula_not_started} when the macula application is not running.
Join the Macula relay mesh with Erlang distribution.
After calling this, standard OTP distribution works across firewalls. Opts takes:
relays(required): the V2 pool's seeds, each a map withhost,portandexpected_node_id, the relay's 32-byte node_id, which every dial checks (D16). A relay without one refuses the join with{error, {relays, expected_node_id_required}}before any pool starts.node_identity: the V2 pool's node identity key,macula_node_keys:node_key(). Default: generated.
Internally builds a V2 macula_client:pool() and registers it with macula_dist_pool as the carrier for _dist.tunnel.* traffic. Dist tunnel frames travel under the all-zeros realm (protocol-internal infrastructure, not bound to any user realm).
-spec links(pool()) -> {ok, [macula_client:link_info()]}.
Per-link snapshot of a V2 pool — one entry per spawned link with its peer station node_id (pubkey), dial host, pid, and connected flag. Use this to resolve a specific station (by pubkey or hostname) to its link for targeted, per-station operations. See macula_client:links/1 for the link_info() shape.
-spec monitor_nodes() -> ok.
Subscribe to node up/down events.
Open a LOCAL in-process client-stream or bidi call. Used for unit tests and same-BEAM dispatch via macula_stream_local.
Open a LOCAL in-process stream with explicit mode.
-spec parse_stations(binary()) -> {ok, [macula_stations:seed()]} | {error, term()}.
Parse a MACULA_STATIONS seed list: comma-separated <node id>@<host>:<port> entries, the node id as 64 lowercase hex characters, the host a DNS name, an IPv4 address or a bracketed IPv6 address, the port 1 to 65535. Returns the seeds in order, each pinned to its node id. A refusal names the entry's position and what is wrong with it, and carries neither the value, nor a node id, nor a host. See macula_stations:parse/1.
-spec provider_authorization(pool(), realm(), procedure()) -> {ok, #{org_directory := binary(), procedure_delegation := binary()}} | {error, term()}.
Resolve this pool's own D25 provider authorization for an org-namespaced Procedure under Realm — the realm-signed org_directory and the org-signed procedure_delegation naming the pool's node id, both fetched from the DHT and verified against the realm key the pool pinned for Realm at connect — as the authorization opt macula_direct_dial:publish_advertisement/5 and macula_response:advertise_direct/6,7 take: a map of the two records' encoded wire forms. advertise/5 resolves this same chain itself for the wire frame; a caller that publishes the direct-dial record needs it explicitly, since the station refuses an org-namespaced record without one (no_authorization).
Fails fast with {error, {provider_authorization, _}} when the procedure has no org namespace, a chain piece is missing from the DHT, the pool pinned no key for Realm, or the chain does not verify against that key — the same failures advertise/5 reports.
-spec provider_authorization(pool(), realm(), procedure(), provider_io()) -> {ok, #{org_directory := binary(), procedure_delegation := binary()}} | {error, term()}.
As provider_authorization/3, reading the resolution's DHT calls from the provider_io/0 seam entries in Opts (defaults to the facade's own functions) — the seam advertise/5 accepts too.
Publish to (Realm, Topic) on Pool. Equivalent to publish/5 with empty opts.
Publish to (Realm, Topic) on Pool with options. See macula_pubsub:publish/5 for honored opts.
Store Bytes in the mesh's content store and return its MCID (Macula Content ID, 50 bytes: tag 2 for SHA-384, codec, then the 48-byte hash). Content that fits in one block (byte_size(Bytes) =< macula_manifest:default_chunk_size/0, 256 KiB) is sent as a single _content.put_block, and the MCID is <<2, 16#55, SHA-384(Bytes)>>. Larger content is split into chunks (macula_manifest:create/1), each chunk sent via its own _content.put_block, then a content_manifest via _content.put_manifest; the returned MCID is the manifest's (<<2, 16#56, _/binary>>), Merkle-rooted over every chunk. Either way the station verifies each block's hash before accepting it.
The whole transfer — every block call plus the manifest call for chunked content — rides one dedicated QUIC stream on one pinned pool link (see PLAN_PER_STREAM_QUIC_ISOLATION.md Phase 2), so a large blob transfer no longer head-of-line-blocks other RPC/PubSub traffic on the same connection.
A thin blocking wrapper over macula_content_transfer:start_put/2 + await/1 — see that module for the addressable form (a live pid, real cancel with a peer-visible abort, pause/resume/multi-stream as later phases land). PLAN_PUSH_UPLOAD.md Phase 1.
-spec put_content_station(pool(), macula_client:seed(), binary(), pos_integer()) -> {ok, mcid()} | {error, term()}.
As put_content/2, dialing Station directly (reusing a live link or dialing + waiting up to TimeoutMs for one) instead of picking from the pool's existing links — the content-transfer counterpart to call_station/7. Station and TimeoutMs mean exactly what they do there; the underlying block/manifest transfer has its own internal timeouts regardless of TimeoutMs, which bounds only the connect wait. See macula_direct_dial:put_content/4 to resolve a station by identity and put in one call.
-spec put_content_station(pool(), macula_client:seed(), binary(), pos_integer(), map()) -> {ok, mcid()} | {error, term()}.
As put_content_station/4, with a per-call TLS trust override for this dial — verify, expected_node_id, pin_tls_cert (see call_station/8).
Store a signed record in the mesh DHT via a V2 pool.
Build the record via the typed constructors in macula_record (node_record/3,4, content_announcement/3,4, tombstone/2,3, realm_directory/3,4, procedure_advertisement/4,5, etc.) and sign it. A record this node signs about itself is signed by its pool, which holds the node identity key: through macula_client:sign_node_record/2, a domain record (tags 0x20 to 0xFF) through macula_client:sign_domain_record/2, and the tombstone of either through macula_client:withdraw_node_record/3. A realm-, org- or foundation-signed record is signed with macula_record:sign/2 and its signer's key. Pass the signed record or its wire form (macula_record:encode/1). The record travels as its wire form; the station verifies it on receipt and stores it under macula_record:storage_key/1, propagating to the K-nearest peers.
Receive the next chunk (blocks).
Send a binary chunk on the stream.
Send a chunk with explicit encoding.
Server-side: emit the terminal reply value.
Sign a domain record (tags 0x20 to 0xFF) as this node, with the pool's node identity key, in the pool's own process. See macula_client:sign_domain_record/2.
Sign a record this node signs about itself with the pool's node identity key, in the pool's own process: a node record, a procedure advertisement or a content announcement that names this node. See macula_client:sign_node_record/2 for the record checks and refusals. Optsnot_after bounds the signed record's expiry: the pool judges it on its own clock, refuses a bound already passed as {error, not_after_passed}, ends the record at the bound when the bound comes before the record's lifetime runs out, and keeps the built lifetime otherwise.
-spec status(pool()) -> {ok, macula_client:status()}.
Aggregate health snapshot of a V2 pool. Suitable for /health or /status endpoints; not for hot-loop polling. See macula_client:status/1 for the full shape.
-spec subscribe(pool(), realm(), topic(), pid()) -> {ok, reference()} | {error, {text_too_long | invalid_text, topic}}.
Subscribe Subscriber to (Realm, Topic) on Pool. Equivalent to subscribe/5 with empty opts.
-spec subscribe(pool(), realm(), topic(), pid(), map()) -> {ok, reference()} | {error, {text_too_long | invalid_text, topic}}.
Subscribe Subscriber to (Realm, Topic) on Pool with options. The delivery option chooses how a single publisher's out-of-order arrivals are handled:
ordered(default) — per-publisher FIFO by seq; out-of-order arrivals are buffered and released in order, a genuinely missing seq skipped afterorder_timeout_ms(aconnect/2option, default 250ms). A new publisher's first facts are held for up toorder_timeout_ms, so its order starts at the lowest seq seen.latest_only— deliver only seqs newer than the highest seen for that publisher (drop stale); no buffering, no delay.as_arrives— deliver in raw arrival order; the consumer orders it itself.
Ordering state is kept per publisher, and apart for EVENTs whose publisher signature did not verify. See macula_pubsub:subscribe/5.
-spec subscribe_callback(pool(), realm(), topic(), macula_pubsub:callback()) -> {ok, reference()} | {error, term()}.
Subscribe with a callback function. The SDK spawns a small receiver process internally and invokes the callback once per inbound event. See macula_pubsub:subscribe_callback/4.
-spec subscribe_records(pool(), record_type(), fun((m_record()) -> any())) -> {ok, reference()} | {error, term()}.
Subscribe to live record-stored events filtered by type.
The callback receives each newly-stored record of the given type that verifies under the node's crypto profile. Returns a subscription reference for unsubscribe_records/2. Topic shape is _dht.records.<type>.stored, rendered with the type tag as a decimal integer for log friendliness.
The binary of a text value a peer supplied (D26): the binary of {text, Bin}, a binary unchanged, and badarg for anything else.
Stop advertising a procedure on a V2 pool.
-spec unadvertise_stream(procedure()) -> ok.
Stop advertising a LOCAL streaming procedure.
Stop advertising a streaming procedure on a V2 pool.
-spec unmonitor_nodes() -> ok.
Unsubscribe from node up/down events.
Drop a pool subscription. Idempotent.
Cancel a subscribe_records/3 subscription.
-spec withdraw_node_record(pool(), m_record() | binary(), macula_record:reason()) -> {ok, m_record()} | {error, term()}.
Withdraw a record this node signed — a node record, a procedure advertisement, a content announcement or a domain record — with a tombstone signed by the pool's node identity key, in the pool's own process. See macula_client:withdraw_node_record/3.