{"id":50312441,"url":"https://github.com/aurimasniekis/cpp-conduit","last_synced_at":"2026-05-28T22:01:34.362Z","repository":{"id":360008640,"uuid":"1248315867","full_name":"aurimasniekis/cpp-conduit","owner":"aurimasniekis","description":"Modern C++23 header-only event-dispatching / event-transport library","archived":false,"fork":false,"pushed_at":"2026-05-24T14:53:00.000Z","size":116,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T16:21:58.353Z","etag":null,"topics":["amqp","cpp","cpp23","dispatcher","event","event-dispatcher","event-driven","events","mqtt","nats","rabbitmq","redis","zeromq"],"latest_commit_sha":null,"homepage":"https://aurimasniekis.github.io/cpp-conduit/","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aurimasniekis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-24T13:36:57.000Z","updated_at":"2026-05-24T14:53:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aurimasniekis/cpp-conduit","commit_stats":null,"previous_names":["aurimasniekis/cpp-conduit"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/aurimasniekis/cpp-conduit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-conduit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-conduit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-conduit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-conduit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aurimasniekis","download_url":"https://codeload.github.com/aurimasniekis/cpp-conduit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-conduit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33627941,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["amqp","cpp","cpp23","dispatcher","event","event-dispatcher","event-driven","events","mqtt","nats","rabbitmq","redis","zeromq"],"created_at":"2026-05-28T22:01:33.430Z","updated_at":"2026-05-28T22:01:34.353Z","avatar_url":"https://github.com/aurimasniekis.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Conduit\n\n[![CI](https://github.com/aurimasniekis/cpp-conduit/actions/workflows/ci.yml/badge.svg)](https://github.com/aurimasniekis/cpp-conduit/actions/workflows/ci.yml)\n[![Docs](https://github.com/aurimasniekis/cpp-conduit/actions/workflows/docs.yml/badge.svg)](https://aurimasniekis.github.io/cpp-conduit/)\n\n`conduit` is a C++23 event bus and event-transport library. You declare events as C++ types with compile-time names, hand them to a `Bus`, and the bus delivers them — synchronously, on a thread pool, or out over MQTT / AMQP / NATS / Redis / ZeroMQ. The same event object also serializes itself to JSON or CBOR so it can leave the process and come back.\n\n## Why use this library?\n\n- Good for **decoupling producers from consumers** inside one process — and then later extending the same code to publish off-machine without rewriting the producer site.\n- Good for **typed in-process pub/sub** where you want compile-time event names and listener handlers that take the payload type directly (`[](const OrderCreated\u0026 o){...}`).\n- Good for **bridging a bus to an external system** — the `relay` transport hands matching envelopes to a user callback; point it at your websocket / HTTP webhook / log sink.\n- Useful when you want **glob-pattern listeners** (`bus.listen(\"order.**\", ...)`) without rolling your own matcher.\n- Useful when you want **middleware around every dispatch** — tracing, metrics, deny-listing, structured logging.\n- **Not ideal for** hard-real-time work: dispatch goes through `std::function`, `std::mutex`, and (for broker transports) heap-allocated wire buffers. It is fire-and-forget by default — failures are reported through middleware, not thrown at the publisher.\n- **Not ideal for** guaranteed delivery on its own. The core ferries `Durable` / `Persistent` / `RequireAck` flags through, but actual durability is the broker adapter's job (MQTT QoS, AMQP `confirm.select`, etc.).\n\n## Quick example\n\n```cpp\n#include \u003cconduit/conduit.hpp\u003e\n\n#include \u003ciostream\u003e\n#include \u003cstring\u003e\n\n#include \u003cparcel/parcel.h\u003e\n\n// An event type. The fixed-string `\"greeted\"` is the wire name used by\n// listeners and any remote transport. The static `event_field_descriptors`\n// hook declares the fields that get serialized.\nstruct Greeted : conduit::Event\u003cGreeted, \"greeted\"\u003e {\n    std::string who;\n\n    Greeted() = default;\n    explicit Greeted(std::string s) : who(std::move(s)) {}\n\n    static auto\u0026 event_field_descriptors(parcel::FieldsBuilder\u003cGreeted\u003e\u0026 b) {\n        return b.field\u003c\u0026Greeted::who\u003e(\"who\");\n    }\n};\n\nint main() {\n    conduit::Bus bus;\n    bus.use_transport\u003cconduit::local::Transport\u003e();   // in-process delivery\n\n    // listen\u003cT\u003e(handler) — handler may take `const T\u0026` or `const EventEnvelope\u0026`.\n    // `sub` keeps the subscription alive; destroy it to unsubscribe.\n    auto sub = bus.listen\u003cGreeted\u003e([](const Greeted\u0026 g) {\n        std::cout \u003c\u003c \"hello, \" \u003c\u003c g.who \u003c\u003c '\\n';\n    });\n\n    bus.publish(conduit::event(Greeted{\"world\"}).build());\n}\n```\n\nA few things to notice in this example:\n\n- `Greeted` derives from `conduit::Event\u003cGreeted, \"greeted\"\u003e`. The CRTP parameter is the event class itself; the fixed-string is the event's wire-stable name.\n- `event_field_descriptors` is required (even for empty events — return `b;`). It is how `parcel` learns to encode the event for transport.\n- `bus.publish(...)` is `void` and does not throw on listener errors; exceptions are routed to middleware (see *Error handling*).\n- The `sub` handle is RAII: when it is destroyed the listener is unregistered.\n\n## Installation\n\n`conduit` is a CMake project. The core library is header-only; each broker adapter is an opt-in static library gated by `CONDUIT_TRANSPORT_\u003cNAME\u003e=ON`.\n\n### CMake FetchContent\n\n```cmake\ninclude(FetchContent)\nFetchContent_Declare(\n    conduit\n    URL      https://github.com/aurimasniekis/cpp-conduit/archive/refs/tags/v0.5.0.tar.gz\n    URL_HASH SHA256=4dd3722131d4bb47e2d0333d7084d4b7e930acfa7a692eab22f4d2982be8be5d\n    DOWNLOAD_EXTRACT_TIMESTAMP TRUE\n)\nFetchContent_MakeAvailable(conduit)\n\ntarget_link_libraries(my_app PRIVATE conduit::conduit)\n```\n\nTo opt into a transport adapter — for example MQTT — set the option *before* fetching:\n\n```cmake\nset(CONDUIT_TRANSPORT_MQTT ON CACHE BOOL \"\" FORCE)\nFetchContent_MakeAvailable(conduit)\n\ntarget_link_libraries(my_app PRIVATE\n    conduit::conduit\n    conduit::transport_mqtt)\n```\n\n### `find_package` after install\n\n`conduit` generates an install rule when it is built top-level and none of its dependencies were pulled via `FetchContent`. After a regular `cmake --install`, downstream projects can:\n\n```cmake\nfind_package(conduit REQUIRED)\ntarget_link_libraries(my_app PRIVATE conduit::conduit)\n```\n\n### `add_subdirectory`\n\nDrop the repo into a `third_party/` folder and `add_subdirectory(third_party/conduit)`. The library exports `conduit::conduit` and one `conduit::transport_\u003cname\u003e` target per enabled adapter.\n\n### Minimal consumer `CMakeLists.txt`\n\n```cmake\ncmake_minimum_required(VERSION 3.25)\nproject(my_app LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 23)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\ninclude(FetchContent)\nFetchContent_Declare(\n    conduit\n    URL      https://github.com/aurimasniekis/cpp-conduit/archive/refs/tags/v0.5.0.tar.gz\n    URL_HASH SHA256=4dd3722131d4bb47e2d0333d7084d4b7e930acfa7a692eab22f4d2982be8be5d\n    DOWNLOAD_EXTRACT_TIMESTAMP TRUE\n)\nFetchContent_MakeAvailable(conduit)\n\nadd_executable(my_app main.cpp)\ntarget_link_libraries(my_app PRIVATE conduit::conduit)\n```\n\n## Requirements\n\n- C++23 compiler.\n- CMake ≥ 3.25.\n- Always-fetched dependencies:\n  - `nlohmann/json` 3.12.0\n  - `cpp-ulid` 1.0.0\n  - `cpp-parcel` 0.2.0\n  - `cpp-metadata` 0.2.0\n  - `cpp-commons` 0.1.3\n- Tests use GoogleTest 1.15.2 (only when `CONDUIT_BUILD_TESTS=ON`, default on top-level builds).\n- Each transport adapter brings its own dependencies, only when its option is enabled:\n\n  | Option                    | Pulls in                                                                |\n  |---------------------------|-------------------------------------------------------------------------|\n  | `CONDUIT_TRANSPORT_MQTT`  | Paho MQTT C++ + Paho MQTT C                                             |\n  | `CONDUIT_TRANSPORT_AMQP`  | AMQP-CPP (optional OpenSSL with `CONDUIT_TRANSPORT_AMQP_TLS`)           |\n  | `CONDUIT_TRANSPORT_NATS`  | nats.c (optional OpenSSL with `CONDUIT_TRANSPORT_NATS_TLS`)             |\n  | `CONDUIT_TRANSPORT_REDIS` | redis-plus-plus + hiredis (optional TLS)                                |\n  | `CONDUIT_TRANSPORT_ZMQ`   | libzmq + cppzmq (optional libsodium with `CONDUIT_TRANSPORT_ZMQ_CURVE`) |\n\n- Threads — `find_package(Threads REQUIRED)` is unconditional.\n\n## Core concepts\n\n### `conduit::Event\u003cSelf, Name\u003e`\n\nUser events derive from this CRTP base. `Self` is the event class itself; `Name` is the wire-stable name as a `parcel::fixed_string`. Each event must provide a static `event_field_descriptors` hook that declares its fields — `parcel` uses it for JSON/CBOR encode and decode.\n\n```cpp\nstruct OrderCreated : conduit::Event\u003cOrderCreated, \"order.created\"\u003e {\n    std::string order_id;\n    double total = 0.0;\n\n    OrderCreated() = default;\n    OrderCreated(std::string id, double t)\n        : order_id(std::move(id)), total(t) {}\n\n    static auto\u0026 event_field_descriptors(parcel::FieldsBuilder\u003cOrderCreated\u003e\u0026 b) {\n        return b.field\u003c\u0026OrderCreated::order_id\u003e(\"order_id\")\n                .field\u003c\u0026OrderCreated::total\u003e(\"total\");\n    }\n};\n```\n\nAn event with no fields still needs the hook — return the builder unchanged:\n\n```cpp\nstruct Tick : conduit::Event\u003cTick, \"tick\"\u003e {\n    static auto\u0026 event_field_descriptors(parcel::FieldsBuilder\u003cTick\u003e\u0026 b) {\n        return b;\n    }\n};\n```\n\nEvents must be default-constructible: the registry decodes by calling `std::make_shared\u003cT\u003e()` and then populating fields.\n\n### `conduit::EventEnvelope`\n\nThe polymorphic wrapper that flows through the bus. It carries:\n\n- A ULID `id()`.\n- `flags()` — see *Flags*.\n- `metadata()` — a JSON-shaped key/value tree backed by [`md::Metadata`](https://github.com/aurimasniekis/cpp-metadata). Values may be strings, booleans, integers, floats, arrays, or nested objects; access via `require_string(\"k\")` / `get_string_if(\"k\")` / `at(\"k\").as_int()` etc., or path-style with `require_path(\"device.firmware.major\")`.\n- `timestamps()` — `created_at`, `published_at`, `received_at`, `delivered_at`, `failed_at`.\n- Optional `correlation_id()` and `causation_id()` (ULIDs).\n- The typed payload, accessed with `payload_as\u003cT\u003e()` returning `std::shared_ptr\u003cconst T\u003e` (or `nullptr` if the payload is not a `T`).\n\n`EventEnvelopeView` is a source-compatibility alias for `EventEnvelope`. The bus stores envelopes by `shared_ptr` internally, so copying an envelope is cheap and the core is shared — mutations through a non-const accessor on a copy show up on every other copy.\n\n### `EventBuilder\u003cT\u003e` and `conduit::event(T)`\n\nA fluent builder. Returned by `conduit::event(payload)`; finalized with `.build()` or by implicit conversion to `EventEnvelope` (used by `bus.publish(builder)`):\n\n```cpp\nauto env = conduit::event(OrderCreated{\"O-9\", 49.99})\n    .metadata(\"tenant\", \"acme\")\n    .correlation_id(parent_id)\n    .flag\u003cconduit::flags::Direct\u003e()\n    .build();\nbus.publish(env);\n\n// Or pass the payload straight to publish() — the bus wraps it with defaults.\nbus.publish(OrderCreated{\"O-9\", 49.99});\n```\n\n### `conduit::Bus`\n\nOwns transports, middleware, and listeners. Constructible on the stack or via `shared_ptr` — the bus tracks subscriptions through a self-aliasing pointer so both work. `Bus` is non-copyable and non-movable; pass it by reference.\n\nThe bus is destroyed last: its destructor calls `shutdown()`, which flushes and detaches every transport. `shutdown()` is idempotent and noexcept.\n\n### `Transport`\n\nAn abstract base for anything that carries envelopes. The library ships with:\n\n- `conduit::local::Transport` — in-process. Three modes: `Direct` (same thread), `Queue` (single worker), `ThreadPool` (N workers).\n- `conduit::relay::Transport` — hands envelopes whose name matches a glob to a user callback.\n- `conduit::FilteredTransport` — wraps another transport with outbound/inbound predicates.\n- `conduit::mqtt::Transport`, `amqp::Transport`, `nats::Transport`, `redis::Transport`, `zmq::Transport` — opt-in broker adapters.\n\nA transport returns `TransportScope::Local` or `TransportScope::Remote` from `scope()`. The bus uses that together with the `LocalOnly` / `RemoteOnly` flags on an envelope to decide where it goes.\n\n### `Middleware`\n\nHooks invoked around every publish:\n\n```cpp\nclass Middleware {\npublic:\n    virtual bool before_dispatch(EventEnvelopeView\u0026 v);\n    virtual void after_dispatch(EventEnvelopeView\u0026 v);\n    virtual void on_error(EventEnvelopeView\u0026 v, const std::exception_ptr\u0026 ep);\n    virtual void on_transport_error(std::string_view transport,\n                                    const std::exception_ptr\u0026 ep);\n};\n```\n\n- `before_dispatch` can return `false` to drop the envelope.\n- `on_error` fires when a listener throws (or when an invariant like `LocalOnly + RemoteOnly` is violated).\n- `on_transport_error` fires when a transport adapter fails to decode an inbound message — there is no envelope at that point, so the hook receives the transport's short name and the exception.\n\n### Subscriptions\n\n`Bus::listen(...)` returns a `Subscription` (a move-only RAII handle). Drop it to unregister. `Subscription::detach()` and `Subscription::release()` exist; prefer letting the handle's destructor do the work.\n\n```cpp\n{\n    auto sub = bus.listen\u003cGreeted\u003e([](const Greeted\u0026) {});\n    bus.publish(Greeted{\"world\"});            // delivered\n} // sub goes out of scope here\nbus.publish(Greeted{\"world\"});                // not delivered\n```\n\n`EventSubscriber` is a base class that owns several `Subscription`s in one place — convenient for projection / aggregator objects that listen to many events.\n\n## Common usage patterns\n\n### Listener styles\n\n```cpp\n// Take the payload directly.\nauto a = bus.listen\u003cOrderCreated\u003e([](const OrderCreated\u0026 o) { /* ... */ });\n\n// Take the envelope — gives you id, flags, metadata, timestamps.\nauto b = bus.listen\u003cOrderCreated\u003e([](const conduit::EventEnvelope\u0026 env) {\n    auto p = env.payload_as\u003cOrderCreated\u003e();\n    std::cout \u003c\u003c env.id().string() \u003c\u003c ' ' \u003c\u003c p-\u003eorder_id \u003c\u003c '\\n';\n});\n\n// Exact name string (no wildcards).\nauto c = bus.listen(\"order.created\", [](const conduit::EventEnvelopeView\u0026) {});\n\n// Glob pattern (`*` = within segment, `**` = across segments).\nauto d = bus.listen(\"order.**\", [](const conduit::EventEnvelopeView\u0026 v) {\n    std::cout \u003c\u003c v.name() \u003c\u003c '\\n';\n});\n\n// Class-based listener.\nclass MyListener : public conduit::EventListener\u003cOrderCreated\u003e {\npublic:\n    void on_event(const conduit::EventEnvelope\u003cOrderCreated\u003e\u0026) override {}\n};\nauto e = bus.listen\u003cOrderCreated\u003e(std::make_shared\u003cMyListener\u003e());\n```\n\n`bus.listen\u003cT\u003e(handler)` registers the event type with the bus's registry automatically, so a remote transport on the same bus can decode incoming `T` envelopes too.\n\n### Wiring up several listeners with `EventSubscriber`\n\n```cpp\nclass OrderProjection : public conduit::EventSubscriber {\npublic:\n    void register_to(conduit::Bus\u0026 bus) override {\n        on\u003cOrderCreated\u003e(bus, [](const OrderCreated\u0026 o) { /* ... */ });\n        on\u003cOrderShipped\u003e(bus, [](const OrderShipped\u0026 o) { /* ... */ });\n        on(bus, \"audit.*\", [](const conduit::EventEnvelopeView\u0026) {});\n    }\n};\n\nOrderProjection projection;\nbus.register_subscriber(projection);\n```\n\nWhen `projection` is destroyed, every subscription it owns is cleaned up.\n\n### Picking a local execution mode\n\n```cpp\n// Direct (default): each publish() delivers in the caller's thread.\nbus.use_transport\u003cconduit::local::Transport\u003e();\n\n// Queue: one worker thread drains in FIFO order. publish() returns immediately.\nbus.use_transport\u003cconduit::local::Transport\u003e(conduit::local::Execution::Queue);\n\n// ThreadPool: N workers. Use ThreadPoolConfig::queue_capacity for backpressure.\nbus.use_transport\u003cconduit::local::Transport\u003e(\n    conduit::local::Execution::ThreadPool,\n    conduit::local::ThreadPoolConfig{.threads = 4, .queue_capacity = 256});\n```\n\n`queue_capacity = 0` is unbounded — convenient for tests, hazardous in production if a consumer falls behind. Set a positive cap so `submit()` blocks the producer when the queue is full.\n\n`flags::Direct` on an envelope overrides the mode and forces inline delivery even when the local transport is queued or pooled — useful for \"this must happen synchronously\" events like config reload:\n\n```cpp\nbus.publish(conduit::event(Beep{}).flag\u003cconduit::flags::Direct\u003e().build());\n```\n\n### Relaying selected events outside the bus\n\n```cpp\nbus.use_transport\u003cconduit::relay::Transport\u003e(\"order.*\",\n    [](const conduit::EventEnvelopeView\u0026 v) {\n        const auto j = conduit::serialization::encode_json(v);\n        std::cout \u003c\u003c \"relayed: \" \u003c\u003c j.dump() \u003c\u003c '\\n';\n    });\n\nbus.publish(conduit::event(OrderCreated{\"O-9\", 49.99}).build()); // relayed\nbus.publish(conduit::event(AuditRecorded{}).build());            // not relayed\n```\n\n`relay::Transport` accepts a glob; you can `add_route` / `remove_route` at runtime. Callbacks run on whatever thread `dispatch()` happens to be called from — usually the publisher's, unless a `local::Transport` in Queue/ThreadPool mode is in front of you. Exceptions thrown from a relay callback are swallowed so the bus's fire-and-forget contract holds.\n\n### Filtering a transport bidirectionally\n\n`FilteredTransport` wraps an inner transport with up to two predicates. Outbound gating decides what `dispatch()` actually sends; inbound gating decides what the bus sees from arrivals.\n\n```cpp\nauto inner = std::make_shared\u003cconduit::mqtt::Transport\u003e(cfg);\nbus.use_transport\u003cconduit::FilteredTransport\u003e(\n    inner,\n    /*outbound=*/[](const auto\u0026 v){ return conduit::Glob::match(\"order.*\", v.name()); },\n    /*inbound=*/ [](const auto\u0026 v){ return !v.flags().template has\u003cconduit::flags::LocalOnly\u003e(); });\n```\n\nEither predicate may be empty (`{}`) — meaning \"pass everything\".\n\n### Middleware\n\n```cpp\nclass TraceMW : public conduit::Middleware {\n    bool before_dispatch(conduit::EventEnvelopeView\u0026 v) override {\n        v.metadata().insert_or_assign(\"trace_id\", make_trace_id());\n        return true;\n    }\n    void on_error(conduit::EventEnvelopeView\u0026 v, const std::exception_ptr\u0026) override {\n        log_error(v.name());\n    }\n    void on_transport_error(std::string_view transport,\n                            const std::exception_ptr\u0026 ep) override {\n        try { std::rethrow_exception(ep); }\n        catch (const std::exception\u0026 e) {\n            log_error(\"transport \" + std::string{transport} + \" failed: \" + e.what());\n        }\n    }\n};\n\nbus.use_middleware\u003cTraceMW\u003e();\n```\n\nSkip the whole pipeline for one envelope with `flags::NoMiddleware`:\n\n```cpp\nbus.publish(conduit::event(Spammy{}).flag\u003cconduit::flags::NoMiddleware\u003e().build());\n```\n\n### Scope-aware dispatch and default flags\n\n```cpp\nstruct AppConfigReloadEvent\n    : conduit::Event\u003cAppConfigReloadEvent, \"app.config.reload\"\u003e,\n      conduit::DefaultFlags\u003cconduit::flags::LocalOnly\u003e { /* ... */ };\n```\n\nEvery published `AppConfigReloadEvent` automatically has `LocalOnly` set; the bus will route it through local listeners but skip any `TransportScope::Remote` transport. If you can't modify the event type, specialize the trait:\n\n```cpp\nnamespace conduit {\ntemplate \u003c\u003e\nstruct event_traits\u003cThirdPartyEvent\u003e {\n    static flags::FlagSet default_flags() {\n        return flags::FlagSet::of\u003cflags::LocalOnly\u003e();\n    }\n};\n}\n```\n\nRouting matrix (from `Bus::publish_impl`):\n\n| envelope flags | transport scope | dispatched?                    |\n|----------------|-----------------|--------------------------------|\n| `LocalOnly`    | `Local`         | yes                            |\n| `LocalOnly`    | `Remote`        | no — silently skipped          |\n| `RemoteOnly`   | `Local`         | no                             |\n| `RemoteOnly`   | `Remote`        | yes                            |\n| both set       | any             | dropped + routed to `on_error` |\n| neither set    | any             | yes                            |\n\nCustom flags follow the same `Flag\u003c\"name\"\u003e` pattern:\n\n```cpp\nstruct MyAuditFlag : conduit::flags::Flag\u003c\"my.audit\"\u003e {};\nbus.publish(conduit::event(X{}).flag\u003cMyAuditFlag\u003e());\n```\n\nThe fixed-string name is the flag's stable identity on the wire — no registration step.\n\n### Serialization\n\n```cpp\nauto env = conduit::event(Telemetry{\"t-1\", 0.5})\n               .metadata(\"source\", \"sensor-3\").build();\n\nauto j     = conduit::encode_json(env);   // nlohmann::json\nauto bytes = conduit::encode_cbor(env);   // std::vector\u003cstd::uint8_t\u003e\n\nconduit::EventRegistry reg;\nreg.add\u003cTelemetry\u003e();\n\nauto env2 = reg.decode_json(j);\nauto env3 = reg.decode_cbor(std::span\u003cconst std::uint8_t\u003e{bytes});\nauto pay  = env2.payload_as\u003cTelemetry\u003e();\n```\n\nThe wire shape is stable:\n\n```json\n{\n  \"id\": \"01H...\",\n  \"name\": \"telemetry\",\n  \"flags\": [\"direct\", \"durable\"],\n  \"metadata\": { \"source\": \"sensor-3\" },\n  \"timestamps\": { \"created_at\": 1779378065775 },\n  \"correlation_id\": \"01H...\",\n  \"causation_id\":   \"01H...\",\n  \"payload\": { \"id\": \"t-1\", \"value\": 0.5 }\n}\n```\n\nEach flag's wire name is the literal passed to `Flag\u003c\"...\"\u003e`, so flag identity survives across processes and language boundaries without registration. `serialization::encode_json` / `encode_cbor` / `EventRegistry` are also exposed under the `conduit::serialization::` namespace for backwards compatibility.\n\n### Event type registry\n\nSeparate from the per-`Bus` decode `EventRegistry` above, conduit keeps a process-wide **type catalog** for introspection: for each event type its `name`, its `shape`, and its optional `display_info` — plus a JSON schema derived from the underlying parcel descriptor. It decodes nothing, and it is **not** fed by the `Bus`: register a type explicitly with `add\u003cT\u003e()` or self-register it at static-init with `CONDUIT_REGISTER_EVENT(T)`.\n\n```cpp\nstruct OrderCreated : conduit::Event\u003cOrderCreated, \"order.created\"\u003e {\n    std::string order_id;\n    double total = 0.0;\n    static auto\u0026 event_field_descriptors(parcel::FieldsBuilder\u003cOrderCreated\u003e\u0026 b) {\n        return b.field\u003c\u0026OrderCreated::order_id\u003e(\"order_id\").field\u003c\u0026OrderCreated::total\u003e(\"total\");\n    }\n};\nCONDUIT_REGISTER_EVENT(OrderCreated);   // at namespace scope\n\n// ... anywhere, before/without any Bus:\nauto\u0026 reg = conduit::global_event_types();\nreg.contains(\"order.created\");                       // true (bare name)\nreg.contains(\"conduit:event:order.created\");         // true (full kind)\nauto info = reg.find(\"order.created\");               // std::optional\u003cEventTypeInfo\u003e\nauto all  = conduit::registered_event_types();       // every registered type\nauto schema = reg.schema(\"order.created\");           // nlohmann::json (throws if unknown)\n```\n\n`EventTypeRegistry` is also usable standalone (`conduit::EventTypeRegistry local; local.add\u003cOrderCreated\u003e();`), independent of the global instance. `schema(name)` is the per-type descriptor schema:\n\n```json\n{\n  \"kind\": \"conduit:event:order.created\",\n  \"display_info\": { \"name\": \"Catalog Order\", \"description\": \"An order placed in the catalog.\" },\n  \"category\": \"struct\",\n  \"fields\": [\n    { \"key\": \"order_id\", \"kind\": \"string\", \"display_info\": {}, \"required\": true },\n    { \"key\": \"total\",    \"kind\": \"f64\",    \"display_info\": {}, \"required\": true }\n  ]\n}\n```\n\n### Talking to a real broker (MQTT example)\n\n```cpp\n#include \u003cconduit/mqtt/transport.hpp\u003e\n\nconduit::mqtt::Config cfg;\ncfg.url       = \"tcp://localhost:1883\";\ncfg.client_id = \"my-app\";\ncfg.qos       = 1;\ncfg.topic     = \"conduit/orders\";   // required, non-empty\n\nbus.use_transport\u003cconduit::mqtt::Transport\u003e(cfg);\n```\n\nOne `mqtt::Transport` instance binds to a single topic and carries traffic in both directions. To route different events onto different topics, attach a second instance with its own `Config::topic`, optionally wrapped in `FilteredTransport`. Other broker adapters (AMQP, NATS, Redis, ZMQ) follow the same shape — one transport instance per logical channel; see the per-adapter examples under `transports/\u003cname\u003e/examples/`.\n\n## Error handling\n\n`conduit` is fire-and-forget. `Bus::publish` returns `void` and does not throw under normal operation. Failures are surfaced through middleware:\n\n| Failure                                                        | Path                                                                                       |\n|----------------------------------------------------------------|--------------------------------------------------------------------------------------------|\n| Listener throws                                                | `Middleware::on_error(envelope, exception_ptr)`                                            |\n| `LocalOnly + RemoteOnly` set on the same envelope              | `on_error` with `std::runtime_error(\"LocalOnly + RemoteOnly conflict — envelope dropped\")` |\n| Transport's `dispatch` throws                                  | `on_error` with the offending envelope                                                     |\n| Transport fails to decode an inbound message (no envelope yet) | `Middleware::on_transport_error(transport_name, exception_ptr)`                            |\n| Relay callback throws                                          | swallowed — the bus contract is fire-and-forget                                            |\n| Middleware itself throws                                       | swallowed inside the pipeline                                                              |\n\nConfiguration-time failures **do** throw — and every exception conduit raises derives from `conduit::Exception` (defined in `conduit/exception.hpp`), so a single `catch (const conduit::Exception\u0026)` handles them all:\n\n```\nstd::runtime_error\n  conduit::Exception\n    conduit::ConfigError              // transport Config validation failed\n      conduit::TlsNotSupportedError   // TLS requested but feature flag off at build time\n    conduit::TransportError           // operational/runtime transport failure\n      conduit::amqp::AmqpError, conduit::mqtt::MqttError,\n      conduit::nats::NatsError, conduit::redis::RedisError,\n      conduit::zmq::ZmqError          // per-transport subclasses\n    conduit::SerializationError       // envelope/cell decode failure\n    conduit::UnknownEventTypeError    // EventTypeRegistry lookup miss\n```\n\n- `mqtt::Transport`, `nats::Transport`, `amqp::Transport`, `redis::Transport`, `zmq::Transport` throw `conduit::ConfigError` from their constructor if required fields are empty (topic / subject / channel / etc.), or `conduit::TlsNotSupportedError` when TLS is requested in a build that disabled it.\n- `attach()` throws the relevant per-transport `*Error` (e.g. `conduit::mqtt::MqttError`) — all `conduit::TransportError` subtypes — if it cannot connect to the broker.\n\nA typical pattern:\n\n```cpp\nclass LogErrors : public conduit::Middleware {\n    void on_error(conduit::EventEnvelopeView\u0026 v, const std::exception_ptr\u0026 ep) override {\n        try { std::rethrow_exception(ep); }\n        catch (const std::exception\u0026 e) {\n            std::cerr \u003c\u003c \"dispatch error on \" \u003c\u003c v.name() \u003c\u003c \": \" \u003c\u003c e.what() \u003c\u003c '\\n';\n        }\n    }\n    void on_transport_error(std::string_view t, const std::exception_ptr\u0026 ep) override {\n        try { std::rethrow_exception(ep); }\n        catch (const std::exception\u0026 e) {\n            std::cerr \u003c\u003c \"transport \" \u003c\u003c t \u003c\u003c \" error: \" \u003c\u003c e.what() \u003c\u003c '\\n';\n        }\n    }\n};\n\nbus.use_middleware\u003cLogErrors\u003e();\n```\n\n## Edge cases and pitfalls\n\n- **A `Subscription` you forget to keep alive cancels immediately.** `bus.listen(...)` returns a `[[nodiscard]]` handle whose destructor unsubscribes. `bus.listen\u003cT\u003e([]{ ... });` (no assignment) is almost always a bug.\n- **`bus.publish(...)` with no transport attached** still delivers to local listeners — there is a fallback that fans out inline. This is convenient for tests; in production, attach at least one transport so the routing matrix runs.\n- **`local::Transport` in Queue / ThreadPool mode** runs listeners on a different thread. Capture-by-reference into `bus.listen` requires that the captured object outlive the bus. `bus.drain()` waits for the current backlog; `bus.shutdown()` is implicit in the destructor and is idempotent.\n- **`queue_capacity = 0` is unbounded.** A slow consumer + fast producer + unbounded queue is the classic memory-leak shape. Pick a real number.\n- **Events must be default-constructible.** The registry deserializes by `std::make_shared\u003cT\u003e()` and then populates fields. A missing default constructor is a compile error inside `parcel`'s machinery — the message is long; the fix is short.\n- **`payload_as\u003cT\u003e()` returns `nullptr` when the envelope's payload is some other type.** This happens when a pattern listener (`\"order.*\"`) fires for an event whose C++ type the listener does not know — always null-check before dereferencing.\n- **Inbound decode errors are swallowed by the transport** and reported via `Middleware::on_transport_error`. If you do not install a middleware that handles it, malformed wire data is silently dropped.\n- **`LocalOnly` and `RemoteOnly` set on the same envelope** is treated as an invariant violation: the envelope is dropped and `on_error` fires. The builder will happily let you do it, so prefer setting one or the other.\n- **`Bus` cannot be moved or copied.** Use a `shared_ptr\u003cBus\u003e` if you need shared ownership; the bus keeps an internal self-alias so `shared_from_this()` works either way.\n- **Mutations on an envelope copy mutate the original.** Envelopes share their core via `shared_ptr`. This is intentional — it is how middleware can stamp `trace_id` into metadata and have it reach the listeners. It also means `local.timestamps().received_at = ...` in a transport is visible everywhere.\n- **`flags::NoMiddleware` skips the whole middleware pipeline**, including your audit logging. Use it carefully — it is meant for noisy traffic that is already accounted for, not as a generic opt-out.\n- **Relay callbacks share the publisher's thread** when no thread-pool transport is in front of them. If your callback blocks, the publisher blocks.\n- **Broker connections happen during `attach`.** `bus.use_transport\u003cconduit::mqtt::Transport\u003e(cfg)` will block until it connects or fails — be prepared for `conduit::TransportError` (or the per-transport subclass) at startup.\n- **Pattern listeners use globs, not regex.** `*` matches within one segment (`order.*` matches `order.created` but not `order.line.added`); `**` crosses segments. Anything else matches literally.\n\n## API overview\n\n| API                                                       | Lives in                            | Purpose                                                                   |\n|-----------------------------------------------------------|-------------------------------------|---------------------------------------------------------------------------|\n| `conduit::Event\u003cSelf, \"name\"\u003e`                            | `conduit/event.hpp`                 | CRTP base for user events.                                                |\n| `conduit::DefaultFlags\u003c...\u003e` / `event_traits\u003cT\u003e`          | `conduit/event.hpp`                 | Attach default flags to an event type.                                    |\n| `conduit::EventEnvelope` (alias `EventEnvelopeView`)      | `conduit/envelope.hpp`              | The envelope passed around the bus.                                       |\n| `conduit::EventBuilder\u003cT\u003e` / `conduit::event(T)`          | `conduit/builder.hpp`               | Fluent builder for envelopes.                                             |\n| `conduit::Bus`                                            | `conduit/bus.hpp`                   | Dispatch root; owns transports, middleware, listeners.                    |\n| `conduit::Transport`                                      | `conduit/transport.hpp`             | Abstract transport base; returns `Local` / `Remote` scope.                |\n| `conduit::local::Transport`                               | `conduit/local/transport.hpp`       | In-process delivery (`Direct` / `Queue` / `ThreadPool`).                  |\n| `conduit::relay::Transport`                               | `conduit/relay/transport.hpp`       | Callback transport, glob-routed.                                          |\n| `conduit::FilteredTransport`                              | `conduit/filtered_transport.hpp`    | Bidirectional outbound/inbound filter wrapper.                            |\n| `conduit::mqtt::Transport` (etc.)                         | `conduit/mqtt/transport.hpp` (etc.) | Broker adapters — opt-in via CMake flags.                                 |\n| `conduit::Middleware`                                     | `conduit/middleware.hpp`            | `before_dispatch` / `after_dispatch` / `on_error` / `on_transport_error`. |\n| `conduit::EventListener\u003cT\u003e` / `EventSubscriber`           | `conduit/listener.hpp`              | Class-based listener / multi-event subscriber.                            |\n| `conduit::Subscription`                                   | `conduit/listener.hpp`              | RAII unsubscribe handle.                                                  |\n| `conduit::Glob`                                           | `conduit/glob.hpp`                  | `*` (within segment) / `**` (across) matcher.                             |\n| `conduit::flags::Flag\u003c\"name\"\u003e`, `FlagSet`, built-in flags | `conduit/flags.hpp`                 | Type-tag-based flag bitset.                                               |\n| `conduit::EventRegistry`                                  | `conduit/serialization.hpp`         | Registers event types for wire decode.                                    |\n| `conduit::encode_json` / `encode_cbor`                    | `conduit/serialization.hpp`         | Encode an envelope to the wire.                                           |\n| `conduit::Exception` + `ConfigError`, `TransportError`, … | `conduit/exception.hpp`             | Root exception hierarchy thrown by conduit (catch one type, not many).    |\n| `conduit::EventTypeRegistry` / `global_event_types()`     | `conduit/event_type_registry.hpp`   | Process-wide event *type* catalog (introspection + JSON schema).          |\n| `CONDUIT_REGISTER_EVENT(T)` / `registered_event_types()`  | `conduit/event_type_registry.hpp`   | Self-register a type into the catalog / snapshot all registered types.    |\n| `conduit::Metadata` (= `md::Metadata`), `Timestamps`      | `conduit/metadata.hpp`              | Envelope metadata (typed JSON-shaped tree) + timestamp struct.            |\n\nBuilt-in flags: `Direct`, `Durable`, `Persistent`, `NoMiddleware`, `RequireAck`, `Broadcast`, `LocalOnly`, `RemoteOnly`.\n\n## Examples\n\nThe `examples/` directory contains compact programs that map to specific concepts. Each builds as `conduit_\u003cname\u003e` when `CONDUIT_BUILD_EXAMPLES=ON` (the default at top level).\n\n| Example                                | Demonstrates                                                            |\n|----------------------------------------|-------------------------------------------------------------------------|\n| `examples/hello.cpp`                   | Minimal: event, listen, publish.                                        |\n| `examples/typed_listener.cpp`          | Listener receives the envelope instead of the payload.                  |\n| `examples/subscriber.cpp`              | `EventSubscriber` wires up several listeners as one unit.               |\n| `examples/pattern_listener.cpp`        | `bus.listen(\"order.*\", ...)` glob subscription.                         |\n| `examples/middleware_logging.cpp`      | A logging middleware with `before_dispatch` / `after_dispatch`.         |\n| `examples/threadpool_local.cpp`        | ThreadPool execution + `bus.drain()`.                                   |\n| `examples/flags_direct.cpp`            | `flags::Direct` forces inline delivery even in Queue mode.              |\n| `examples/local_only_event.cpp`        | `DefaultFlags\u003cLocalOnly\u003e` keeps an event off remote transports.         |\n| `examples/relay_to_callback.cpp`       | `relay::Transport` routes matching events to a callback.                |\n| `examples/filtered_transport.cpp`      | Per-leg `FilteredTransport` predicates.                                 |\n| `examples/serialization_roundtrip.cpp` | JSON + CBOR encode/decode via `EventRegistry`.                          |\n| `transports/\u003cname\u003e/examples/*.cpp`     | Broker-specific recipes — publish/subscribe, multi-topic, queue groups. |\n\n## Testing\n\n```bash\ncmake -S . -B build\ncmake --build build -j\nctest --test-dir build --output-on-failure\n```\n\nThe repository ships a `Makefile` that wraps the common workflows:\n\n```bash\nmake test         # configure + build + ctest in build/\nmake sanitize     # ASan + UBSan in build-san/\nmake tidy         # clang-tidy via build-tidy/\nmake release      # Release build + tests in build-release/\nmake coverage     # Clang source-based coverage + HTML report\nmake docs         # Doxygen HTML in build-docs/docs/html/\nmake mqtt         # Configure + build + test with CONDUIT_TRANSPORT_MQTT=ON\nmake amqp         # ...AMQP\nmake nats         # ...NATS\nmake redis        # ...Redis\nmake zmq          # ...ZMQ\nmake ci           # Pre-push gate: format-check + tidy + test + sanitize + release + every transport\nmake format       # clang-format -i over headers/sources/tests/examples\n```\n\nEach broker smoke test (`MqttSmoke`, `AmqpSmoke`, `NatsSmoke`, `RedisSmoke`, `ZmqSmoke`) is skipped if its environment variable is not set:\n\n| Env var                     | Example                              |\n|-----------------------------|--------------------------------------|\n| `CONDUIT_MQTT_TEST_BROKER`  | `tcp://localhost:1883`               |\n| `CONDUIT_AMQP_TEST_BROKER`  | `amqp://guest:guest@localhost:5672/` |\n| `CONDUIT_NATS_TEST_BROKER`  | `nats://localhost:4222`              |\n| `CONDUIT_REDIS_TEST_BROKER` | `tcp://localhost:6379`               |\n| `CONDUIT_ZMQ_TEST_ENDPOINT` | `tcp://127.0.0.1:25557`              |\n\nCI brings each broker up as a service container and runs the corresponding smoke test against it.\n\n## CI\n\nGitHub Actions runs:\n\n- `build` — Ubuntu + macOS, Debug + Release, GCC 14 / Clang 20.\n- `sanitizers` — ASan + UBSan on Ubuntu / GCC 14.\n- `clang-tidy` — macOS / Homebrew LLVM.\n- `format` — `clang-format-22` dry-run.\n- `mqtt`, `amqp`, `nats`, `redis`, `zmq` — each boots its broker as a service container (or, for ZMQ, uses a loopback endpoint) and runs the adapter's smoke test.\n- `docs` — Doxygen build, deployed to GitHub Pages.\n\n## FAQ\n\n**Is the core header-only?** Yes. The `conduit::conduit` CMake target is `INTERFACE`. Each broker adapter is a separate static library because it pulls in heavy C dependencies (paho, nats.c, libzmq, etc.).\n\n**Does the bus take ownership of listeners?** It stores the handler. The returned `Subscription` is the owner of the registration — destroy it to unregister. Capture-by-reference into a handler is fine as long as the captured objects outlive the bus.\n\n**Can I use it from multiple threads?** Yes. `Bus::publish`, `Bus::listen`, and `Bus::use_transport` / `use_middleware` all take an internal mutex. Listeners themselves may run on any thread depending on the local-transport mode; treat handler bodies as multi-threaded code unless you are in `Direct` mode.\n\n**What is `parcel`?** A separate serialization library (`cpp-parcel`) used to encode the typed payload. Events use its `FieldsBuilder` to declare schema; `conduit` builds the envelope JSON/CBOR around it.\n\n**Can I use it without any transport?** Yes — if no transport is attached the bus performs an inline local fan-out as a fallback. This is mostly useful for tests; for real applications attach at least `local::Transport` so the routing matrix and scope filtering run.\n\n**How do I send the same event to two different MQTT topics?** Attach two `mqtt::Transport` instances, each with its own `Config::topic`. Wrap each in a `FilteredTransport` if you only want certain envelope names to go down each path.\n\n**What happens when the broker disconnects mid-publish?** The dispatch attempt is caught and surfaced through `Middleware::on_error`. The reconnect policy belongs to the underlying broker client (paho, nats.c, etc.); see their docs.\n\n**Where does retention / durability live?** In the broker adapter, not the core. `Durable` / `Persistent` / `RequireAck` are carried as flags so adapters can honor them — for MQTT that means QoS and retain; for AMQP it means `delivery_mode=2` and `confirm.select`; etc. The core promises only to *carry* the flags.\n\n**Can I add my own transport?** Yes. Inherit from `conduit::Transport`, implement `scope()` and `dispatch(const EventEnvelopeView\u0026)`, and optionally override `attach_with_sink`, `detach`, and `flush`. For inbound delivery, call `deliver_inbound(envelope)` from your read path; for decode failures, call `bus()-\u003ereport_transport_error(\"my_transport\", std::current_exception())`.\n\n## Contributing\n\nContributions to the library are welcome! If you encounter any issues or have suggestions for\nimprovements,\nplease feel free to submit a pull request or open an issue on the project's repository.\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-conduit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faurimasniekis%2Fcpp-conduit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-conduit/lists"}