macula (macula v11.4.0)

View Source

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

Close a V1 stream (both sides). Renamed from close/1 in 3.11.0 because close/1 now refers to the V2 pool surface.

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) =&lt; macula_manifest:default_chunk_size/0, 256 KiB) is sent as a single _content.put_block, and the MCID is &lt;&lt;2, 16#55, SHA-384(Bytes)&gt;&gt;. 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 (&lt;&lt;2, 16#56, _/binary&gt;&gt;), 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.

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

m_record/0

-type m_record() :: macula_record:m_record().

mcid/0

-type mcid() :: <<_:400>>.

pool/0

-type pool() :: macula_client:pool().

procedure/0

-type procedure() :: binary().

provider_io/0

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

realm/0

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

32-byte realm tag.

record_key/0

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

DHT storage key — macula_record:storage_key/1 output.

record_type/0

-type record_type() :: macula_record:type_tag().

stream/0

-type stream() :: pid().

stream_handler/0

-type stream_handler() :: fun((stream(), term()) -> any()).

stream_mode/0

-type stream_mode() :: server_stream | client_stream | bidi.

topic/0

-type topic() :: binary().

Functions

abort(Stream, Code, Message)

-spec abort(stream(), binary(), binary()) -> ok | {error, {text_too_long | invalid_text, code}}.

Abort the stream with an error frame.

advertise(Pool, Realm, Procedure, Handler, Opts)

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

await_reply(Stream)

-spec await_reply(stream()) -> {ok, term()} | {error, term()}.

Wait for the terminal reply (client-stream / bidi).

await_reply(Stream, Timeout)

-spec await_reply(stream(), timeout()) -> {ok, term()} | {error, term()}.

call(Pool, Realm, Procedure, Payload, TimeoutMs)

-spec call(pool(), realm(), procedure(), term(), 1..600000) -> {ok, term()} | {error, term()}.

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.

call_station(Pool, Station, Target, Realm, Procedure, Payload, TimeoutMs)

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

call_station(Pool, Station, Target, Realm, Procedure, Payload, TimeoutMs, Opts)

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

call_stream(Procedure, Args)

-spec call_stream(procedure(), term()) -> {ok, stream()} | {error, term()}.

Open a LOCAL in-process server-stream call. Used for unit tests and same-BEAM dispatch via macula_stream_local.

call_stream(Procedure, Args, Opts)

-spec call_stream(procedure(), term(), map()) -> {ok, stream()} | {error, term()}.

Open a LOCAL in-process server-stream call with options.

call_stream(Pool, Realm, Procedure, Args, Opts)

-spec call_stream(pool(), realm(), procedure(), term(), map()) -> {ok, stream()} | {error, term()}.

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.

call_stream_station(Pool, Station, Target, Realm, Procedure, Args, Opts)

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

child_spec(Id, Seeds, Opts)

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.

close(Pool)

-spec close(pool()) -> ok.

Stop a V2 pool. Every subscriber receives a final {macula_event_gone, SubRef, pool_closed} message.

close_send(Stream)

-spec close_send(stream()) -> ok.

Half-close the write side; recv still drains.

close_stream(Stream)

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

connect(Seeds, Opts)

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

dist_relay_client()

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

ensure_distributed()

-spec ensure_distributed() -> ok | {error, term()}.

Ensure this node is running in distributed mode.

field(Name, Map)

-spec field(atom() | binary(), map()) -> term().

A field of a map a peer supplied, or undefined when it is absent. See field/3.

field(Name, Map, Default)

-spec field(atom() | binary(), map(), term()) -> term().

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.

find_content_providers(Pool, MCID)

-spec find_content_providers(pool(), mcid()) -> {ok, [map()]} | {error, term()}.

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, []}.

find_record(Pool, Key)

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

find_record(Pool, Key, TimeoutMs)

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

find_records(Pool, Key)

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

find_records(Pool, Key, TimeoutMs)

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

find_records_by_type(Pool, Type)

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

get_content(Pool, MCID)

-spec get_content(pool(), mcid()) -> {ok, binary()} | {error, not_found | invalid_mcid | term()}.

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.

get_content_station(Pool, Station, MCID, TimeoutMs)

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

get_content_station(Pool, Station, MCID, TimeoutMs, Opts)

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

join_dist_relay(Opts)

-spec join_dist_relay(map()) -> ok | {error, term()}.

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): &lt;&lt;"quic://relay.example.com:4434"&gt;&gt;

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_mesh(Opts)

-spec join_mesh(map()) -> ok | {error, term()}.

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 with host, port and expected_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).

links(Pool)

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

monitor_nodes()

-spec monitor_nodes() -> ok.

Subscribe to node up/down events.

open_stream(Procedure, Args, Opts)

-spec open_stream(procedure(), term(), map()) -> {ok, stream()} | {error, term()}.

Open a LOCAL in-process client-stream or bidi call. Used for unit tests and same-BEAM dispatch via macula_stream_local.

open_stream(Procedure, Args, Opts, Mode)

-spec open_stream(procedure(), term(), map(), stream_mode()) -> {ok, stream()} | {error, term()}.

Open a LOCAL in-process stream with explicit mode.

parse_stations(Value)

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

provider_authorization(Pool, Realm, Procedure)

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

provider_authorization(Pool, Realm, Procedure, Opts)

-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(Pool, Realm, Topic, Payload)

-spec publish(pool(), realm(), topic(), term()) -> ok | {error, term()}.

Publish to (Realm, Topic) on Pool. Equivalent to publish/5 with empty opts.

publish(Pool, Realm, Topic, Payload, Opts)

-spec publish(pool(), realm(), topic(), term(), map()) -> ok | {error, term()}.

Publish to (Realm, Topic) on Pool with options. See macula_pubsub:publish/5 for honored opts.

put_content(Pool, Bytes)

-spec put_content(pool(), binary()) -> {ok, mcid()} | {error, term()}.

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) =&lt; macula_manifest:default_chunk_size/0, 256 KiB) is sent as a single _content.put_block, and the MCID is &lt;&lt;2, 16#55, SHA-384(Bytes)&gt;&gt;. 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 (&lt;&lt;2, 16#56, _/binary&gt;&gt;), 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.

put_content_station(Pool, Station, Bytes, TimeoutMs)

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

put_content_station(Pool, Station, Bytes, TimeoutMs, Opts)

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

put_record(Pool, Signed)

-spec put_record(pool(), m_record() | binary()) -> ok | {error, term()}.

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.

recv(Stream)

-spec recv(stream()) -> {chunk, binary()} | {data, term()} | eof | {error, term()}.

Receive the next chunk (blocks).

recv(Stream, Timeout)

-spec recv(stream(), timeout()) -> {chunk, binary()} | {data, term()} | eof | {error, term()}.

send(Stream, Bin)

-spec send(stream(), binary()) -> ok | {error, term()}.

Send a binary chunk on the stream.

send(Stream, Body, Encoding)

-spec send(stream(), binary() | term(), raw | msgpack) -> ok | {error, term()}.

Send a chunk with explicit encoding.

set_reply(Stream, Result)

-spec set_reply(stream(), term()) -> ok.

Server-side: emit the terminal reply value.

sign_domain_record(Pool, Record)

-spec sign_domain_record(pool(), m_record()) -> {ok, m_record()} | {error, term()}.

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_node_record(Pool, Record)

-spec sign_node_record(pool(), m_record()) -> {ok, m_record()} | {error, term()}.

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.

sign_node_record(Pool, Record, Opts)

-spec sign_node_record(pool(), m_record(), #{not_after := integer()}) ->
                          {ok, m_record()} | {error, term()}.

status(Pool)

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

subscribe(Pool, Realm, Topic, Subscriber)

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

subscribe(Pool, Realm, Topic, Subscriber, 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 after order_timeout_ms (a connect/2 option, default 250ms). A new publisher's first facts are held for up to order_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.

subscribe_callback(Pool, Realm, Topic, Callback)

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

subscribe_records(Pool, Type, Callback)

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

text(Bin)

-spec text({text, binary()} | binary()) -> binary().

The binary of a text value a peer supplied (D26): the binary of {text, Bin}, a binary unchanged, and badarg for anything else.

unadvertise(Pool, Realm, Procedure)

-spec unadvertise(pool(), realm(), procedure()) -> ok.

Stop advertising a procedure on a V2 pool.

unadvertise_stream(Procedure)

-spec unadvertise_stream(procedure()) -> ok.

Stop advertising a LOCAL streaming procedure.

unadvertise_stream(Pool, Realm, Procedure)

-spec unadvertise_stream(pool(), realm(), procedure()) -> ok.

Stop advertising a streaming procedure on a V2 pool.

unmonitor_nodes()

-spec unmonitor_nodes() -> ok.

Unsubscribe from node up/down events.

unsubscribe(Pool, SubRef)

-spec unsubscribe(pool(), reference()) -> ok.

Drop a pool subscription. Idempotent.

unsubscribe_records(Pool, Ref)

-spec unsubscribe_records(pool(), reference()) -> ok.

Cancel a subscribe_records/3 subscription.

withdraw_node_record(Pool, Withdrawn, Reason)

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