All notable changes to nim-brokers are documented here. The project follows Semantic Versioning. Dates are ISO-8601.
3.3.0 — 2026-07-14
Handler body sugar across all lanes: listenIt / onSignalIt / provideIt /
reprovideIt, plus a new broker tutorial presentation. An additive release —
no runtime behavior changes, no breaking changes. Each sugar macro wraps the
existing registration proc, letting a handler be written as an inline block
instead of a separately-declared proc.
TypeName.listenIt[(ctx)]: body(EventBroker, all lanes) registers a listener whose block is the real listener proc body, with the event value injected asit(nothing injected forvoidevent types). Returnslisten'sResult[<T>Listener, string];awaitis allowed andraises: []is enforced as for a hand-written listener.TypeName.onSignalIt[(ctx)]: body(SignalBroker, all lanes) installs the single handler with the signal value injected asit(nothing forvoidpayloads); duplicate-guard and return value areonSignal's.TypeName.provideIt[(ctx)]: body/reprovideIt(RequestBroker, all lanes) register a provider whose block is the real provider proc body with the declared signature arg names injected.provideIt→setProvider(keeps the "already set" guard);reprovideIt→replaceProvider(replace-or-insert). Dual-slot brokers also getprovideItNoArgs/reprovideItNoArgs. In sync mode the body cannotawait.TypeName.provideIton MultiRequestBroker — body sugar over the additivesetProvider, returning itsResult[<T>ProviderHandle, string](keep the handle forremoveProvider). NoreprovideIt(providers are additive, no replace verb); eachprovideItadds a fresh provider.- Compile-time fall-through guard (
providerBody): aprovideIt/reprovideItbody must produce a value on every path (return ok(...)/err(...),result = ..., or a trailingResultexpression) — otherwise it is a compile error rather than a silenterr(""). Ablockcontainingbreakand noreturn calls (quit,raiseAssert) are conservatively treated as non-terminal. - New tests:
test_handler_sugar,test_multi_thread_handler_sugar, and fivereject/reject_provideit_*compile-failure guards. Cookbook recipes added indoc/COOKBOOK.md; design indoc/design/BROKER_HANDLER_SUGAR_PLAN.md.
doc/BrokerTutorialPrezi.html— a standalone slide-deck tutorial walking through the broker primitives and their usage.
3.2.0 — 2026-07-09
New SignalBroker primitive (fire-and-forget, single-handler, no-reply) across
all lanes, an asynchronous FFI request path (<lib>_callAsync), bind/rebind
provider sugar for class-method providers, and the multi-thread teardown-UAF fix
that greens --mm:refc on Windows. This is a feature release: it adds a fourth
broker shape, a non-blocking FFI call gate with idiomatic async wrappers in all
four foreign languages, and closes the last Windows/refc sanitizer gap. No
breaking changes to the existing single-thread, multi-thread, or FFI surfaces.
- A new one-way, single-handler broker fusing EventBroker's dispatch shape
(sync call site +
asyncSpawn, no reply path) with RequestBroker's single-registry ownership and aResult[void, string]acceptance check. Declared with anobject/ref/ POD /voidpayload. One handler is installed viaTypeName.onSignal(handler)(duplicate-guarded, mirrorssetProvider); producers callTypeName.signal(...)— a plain non-async proc returningResult[void, string]. Value form, inline-field overloads (likeemit), and avoidpulse (TypeName.signal()) are all generated.ok()means accepted (best-effort snapshot: a handler exists and the queue had room), never "handled".erris definitely-not-delivered:"no signal handler installed"or"queue full". Handler exceptions are swallowed with a chronicles warn — for delivery confirmation use a RequestBroker with avoidresponse.dropSignalHandler()is asyncFuture[void](uniform withdropListener, suspension-free body). Mock/replace trio:replaceSignalHandler/getCurrentSignalHandler/withMockSignalHandler(MT variants are owning-thread only).
- Single-thread lane (
brokers/signal_broker.nim) and multi-thread lane (brokers/internal/mt_signal_broker.nim): the MT lane reuses EventBroker(mt)'s global refcounted slab + per-bucket Vyukov ring transport and RequestBroker(mt)'s single-registry ownership. Same-threadsignal→ directasyncSpawn; cross-thread → marshal payload → enqueue → wake. Lock-freesignal()fast-fail via a per-typeAtomic[int]handler-present counter (signalHandlerPresent). The owning thread allocs the ring;dropSignalHandlercloses it and the poll fn hands it to the per-thread pending-free registry, freed atteardownBrokerThread. - FFI
SignalBroker(API)lane (brokers/internal/api_signal_broker_cbor.nim) rides<lib>_callon a slot-free, one-way path: the payload is enqueued onto the courier ring with a sentinel (no response slot, no round trip) and the call returns immediately. Both failure modes resolve on the caller's thread — a lock-freesignalHandlerPresentload →ApiStatusProviderErr(-10) when no handler, or a courier-ring-full →ApiStatusAgain(-6); accepted →ApiStatusOk(0).<lib>_callAsyncrejects a signal name withApiStatusOneWay(-13). A CBOR decode failure in the adapter is logged, not returned. - Discovery + wrappers. Signals surface as a top-level
signalslist (ApiSignalInfo, the inverse ofevents) in_listApis/_getSchema/ the CDDL and in the generated C header. Each foreign wrapper emits a typed one-way method with no async variant — C++Result<void>, Python-> None(raising), RustResult<(), String>, Goerror. Full four-language parity is gated by the typemappingtestlib signal matrix. BrokerInterface/BrokerImplementsupport forSignalBroker(API)on both the main interface and sub-interfaces, including sub-interface FFI signal routing.- New tests:
test_signal_broker,test_multi_thread_signal_broker,test_api_signal_broker,test_broker_interface_signal,perf_test_multi_thread_signal_broker, fourreject/reject_signal_*guards (bad mode / bad shape / no handler / duplicate handler), plus ASan/TSan tasks for the MT and FFI signal lanes.
- A non-blocking request gate alongside the blocking
<lib>_call._callAsyncsubmits a request and returns immediately with anApiStatus; the response is delivered later through the async response callback. Backed by a configurable async window (in-flight cap) with an EAGAIN-friendly contract —ApiStatusAgain(-6) when the window is saturated — plus a per-request timeout. - Idiomatic async wrappers in every foreign language, each mapping to the
host's native async idiom rather than a raw callback:
- C++ — a
std::future-returning sibling for each async method. - Rust — a typed
AsyncError+std::result::Result, notokiodependency, with a per-call timeout exposed on the generated_asyncmethod. - Go —
context.Context-aware methods with semaphore backpressure. - Python —
async defmethods over the callback bridge.
- C++ — a
- Response status codes are single-sourced, and dispatch-result encoding is shared
across the courier handlers. New
test_api_callAsyncexercises the full path.
- Class-method providers can now be wired with
bind/rebindsugar instead of hand-writing the closure that capturesself. Simplifies registering aBrokerImplementmethod as a RequestBroker provider.
- Ordered per-thread teardown + close-safe shared signals eliminate the
Windows/
--mm:refcteardown UAF (a race between broker-signal teardown, setProvider registration seqs, and refcdeallocOsPages). The fix pairs aBrokerSignalSharedrefcount withteardownBrokerThreadordering and is regression-gated by the newnimble testAllocRacetask on the Windows CI cells. - The
skipRefcOnWindowscarve-out is removed entirely — Windows + refc is now CI-green across the parity matrix. Seedoc/LIMITATION.md§2.2 anddoc/design/TEARDOWN_UAF_FINDINGS.mdfor the mechanism and the remaining app-level caveat. - Sanitizer matrix (ASan/LSan/TSan) is green on Windows and Linux; residual
benign reports (ORC
registerCycleroots buffer,setProviderregistration seqs, event-poll bookkeeping seq) are suppressed viatools/sanitizers/lsan.supp.
- The default-context RequestBroker forwarder returns the inner
Futuredirectly instead of allocating a wrapper Future — one fewer allocation per default-context request. - New in-process and FFI-overhead perf harnesses (
test/ffibench/perf_inproc.nim,perf_overhead.nim,perf_phases.nim) plus aperftestInproctask measure broker dispatch overhead vs. a direct call and serial-vs-pipelined request throughput.
- New broker cookbook and USAGEGUIDE, README refreshed with a SignalBroker
section and
bindsugar, FFI prezi updated to CBOR-only, and the torpedoDESIGN.mdrefreshed.
3.1.4 — 2026-06-19
BrokerImplement constructors gain result-shape parity with new and adopt
the ambient global broker context. The generated create /
createUnderContext wrappers now mirror whatever shape the implementation's
new returns, and create connects its instance to the current global scope
instead of an isolated per-class context.
create/createUnderContextmirror thenewconstructor's return shape across all four forms:new(...): T→create(): T,new(...): Future[T] {.async.}→create(): Future[T] {.async.},new(...): Result[T, string]→create(): Result[T, string], andnew(...): Future[Result[T, string]]→Future[Result[T, string]] {.async.}. The wrapper unwrapsnew's result (await/valueOr) into a bareself: T, bindsbrokerCtx, runs the optionalPostInithook, then wires providers via the unchanged<Impl>SetupProviders(which never sees the Future/Result wrapper). The sync-bare form is byte-identical to the legacy codegen, so existing implementations are unaffected. Also fixes a chronos/parseStmtinteraction — an{.async.}proc carrying atypedescparam is implicitly generic, and synthetic line info crashed the async transform; the async wrappers are now re-stamped with a real source location (copyLineInfoRec).createadopts the ambient globalclassCtx. It previously minted a per-classclassCtx(<Impl>BrokerClassCtxvianewClassCtx()), disconnected from the thread-global scope. It now allocates its per-instance context withnewInstanceCtx(globalBrokerContext())— theclassCtxis read from the currentglobalBrokerContext()at call time, so acreate'd instance shares the global scope of the bare brokers (or the locked test context), whileinstanceCtxstays unique vianewInstanceCtx's process-global counter. The per-class<Impl>BrokerClassCtx/<Impl>BrokerInstCountermodule state is dropped.createUnderContext(ctx, …)is unchanged for explicit/foreign scopes.- New
test/test_broker_ctor_shapes.nimcovers all four constructor shapes pluscreateUnderContextand the locked-scope adoption behavior; registered in thetesttask.
3.1.3 — 2026-06-18
Uniform EventBroker call shape across all lanes, plus a batch of FFI
wrapper type-mapping fixes (alias / seq[byte] / proc-sugar payloads) and
courier-ring drop visibility. emit is now sync void everywhere and
dropListener / dropAllListeners are async Future[void] everywhere, so the
public shape of an EventBroker no longer changes when its tag flips between
(none) / (mt) / (API) and the same source compiles in any lane. The FFI
fixes were surfaced while generating wrappers for the Logos Delivery library;
they preserve meaningful alias names and fix several proc-sugar payload shapes
across all four foreign wrappers.
emitis now syncvoidin the multi-thread and API lanes (it already was in the single-thread lane). The MTemitImplis a plain{.gcsafe, raises: [].}proc; the publicemitand inline field-constructor overloads call it directly. The cross-thread marshal + ring enqueue still happen synchronously beforeemitreturns — no work is deferred. Migration: dropawait/waitForfrom every(mt)/(API)emit call site (await X.emit(a)→X.emit(a)).dropListener/dropAllListenersare now asyncFuture[void]in the multi-thread and API lanes (they already were in the single-thread lane), for cross-lane shape parity. The MT/API drop bodies stay suspension-free, so the returned Future completes eagerly — a discarded Future (e.g. from sync FFI teardown) still clears listeners and fires the cleanup hook. Non-discardable in every lane. Migration:awaityour(mt)/(API)drop calls (ordiscard/waitForin sync /{.thread.}contexts).clearProvider/clearProvidersare unchanged — sync in every lane.- Guiding principle: the public shape follows whether the op actually
suspends.
emitnever awaits listener completion in any lane → sync. Drop on the single-thread lane genuinely awaits in-flight listener cancellation → async, and the other lanes adopt that shape. - No FFI ABI or foreign-wrapper change — neither
emitnordrop*is exposed on the C ABI (verified against the C++/Python/Rust/Go parity matrices).
- Registered type aliases / distincts now keep their name throughout every
wrapper. Object and event-payload fields and unpacked event-callback params
typed with a registered alias (
channelId: ChannelId,timestamp: Timestamp) were flattened to the resolved primitive (std::string,int64_t); the field and param mappers (nimTypeToCppType/…PyHint/…RustHint/…GoCborHint) now return the alias name when its underlying maps, so C++, Python, Rust, and Go all seeChannelId/Timestamprather than the bare primitive. The C++ event-callback param mapper got the same fix (Rust/Go already went through the general field mapper). - Pure type aliases are now resolved in wrapper type discovery. Aliases like
type ContentTopic = string/Timestamp = int64were dropped because registration keyed offgetTypeInst(which echoes a pure alias's own name); discovery now usesgetTypeImplto chain through alias-of-alias to the base symbol. Also fixes alias-typed fields insideOption[T]/seq[T]and structurally-identicalarray[N, byte]aliases (captured as-written soWakuMessageHashandCurve25519Keyno longer collide), and stopsresolveActualSymmis-unwrapping a non-typedescarray[…]bracket. seq[byte]maps to an ergonomicBytesvector in the C++ wrapper — a per-librarystruct Bytes : std::vector<uint8_t>whosejson_type_traitsforce a CBOR byte string (major type 2), matching how Nim'scbor_serializationdecodes it. Previously C++ emitted a jsoncons-specificbyte_string(correct on the wire, unergonomic in the public type).- proc-sugar request brokers now emit the right response payload type in
every wrapper. A verb-named broker registers its verb as a synthetic
distinct alias of the payload; codegen used to surface that synthetic name
(
Result<Send>,Result<StartDiscv5>) instead of the real payload. Fixed for scalar alias/distinct payloads (Result<RequestId>), bare primitives (Result<bool>),seq[...]/array[N,T]containers, and standalone registered objects (whose return type is now scanned and registered, not dropped as "not emittable").
registerBrokerLibraryversion:accepts aconstidentifier, not just a string literal — so a build-time version (-d:git_version=…strdefine) works. The generated<lib>_version()binds the version expression to a string const in the caller's module and returns its.cstring; foreign wrappers expose version only via the runtime_version()call and are unchanged.- Outbound events dropped on a full courier ring are now logged. The CBOR
event courier is a bounded fire-and-forget ring (256 → 1024); on overflow
tryEnqueuedrops by design, but the drop was silent. A throttledwarnnow fires at the generated emit drop site — first drop always logs, then a geometric count backoff (10, 100, 1000 …) plus a 5 s periodic floor so a steady stall keeps reporting without flooding the producer hot path.
3.1.2 — 2026-06-10
Table[K, V] associative-container support across the full FFI surface with
complete scalar-key coverage in all four wrappers, broker method tunneling +
first-class provider mocking, natural proc new constructor ergonomics, and a
chronos dispatcher fd-leak fix for the createContext/shutdown cycle.
Table[K, V]/OrderedTable[K, V]support through the CBOR FFI surface, with nocbor_serializationdependency change. The Table codec lives in the brokers package (brokers/internal/api_cbor_tables.nim) and rides the library's public reader/writer API rather than the patchedcbor_serialization/std/tablesbinding, so the dependency pin stays at its upstream release. Keys travel on the wire as CBOR text strings (the codec writer only emits string map keys); each wrapper converts text ⇄ typed key in generated code. Recognition is wired through the compile-time type schema and the external-type resolver (api_schema.nim,api_type_resolver.nim):ApiFieldDefgainsisTable/tableKeyType/tableValueType, bracket- depth-awareK, Vsplitting keeps a composite value type's own commas intact, andvalidateTableKeyrejects unsupported keys (plain platform-widthint/uint,bool,float, composites) at compile time with a clear error.nimTypeToCddlemits{* tstr => <V>}for the discovery/CDDL schema.- Full scalar Table-key coverage in all four wrappers —
string,int8..64,char,enum(ordinal on the wire), anddistinct-of-scalar keys round-trip through Python, C++, Rust, and Go:- Python (
cbor2) — full coverage viapyKeyClasshelpers inpyEncodeExpr/pyDecodeExpr(int(_k),Priority(int(_k)), string passthrough) →Dict[K, V]. - Rust (
ciborium/serde) — a genericcbor_strkey_mapserde adapter (emitted only when a struct has a non-string-keyed Table field) converts the text-keyed wire map ⇄ typed-keyHashMapviaDisplay/FromStron generated enums; string/char keys map toStringand skip the adapter. - Go (
fxamacker/cbor) — generatedMarshalCBOR/UnmarshalCBORround-trip through a string-keyed wire struct, converting keys viastrconv(int/enum/distinct-of-int throughint64) →map[K]V. - C++ (
jsoncons) — a string-keyed<Name>__wiremirror plus a customjson_type_traits<Json, Name>that delegates field (de)serialisation to the wire struct and converts only the map keys (charvia first byte, int/enum/distinct viastd::stoll/std::to_string) →std::unordered_map<K, V>. Verified orc + refc against the unpatched dependency:runTypeMapTestLib{Py,Cpp,Rust,Go}133/133 each;test/test_api_table_codec.nimround-trips 9 Table shapes and is wired into thetestApicodec list.
- Python (
- Interface request methods now tunnel through the broker. The public
request entry point generated by
BrokerInterfaceis a plain proc that delegates to<Broker>.request(self.brokerCtx, …)instead of a{.base.}pure-virtual method.BrokerImplementemits the user's method body as a private<verb>Implproc and points the per-instance provider closure at it. Effect: bothinstance.method(…)andIFace(instance).method(…)route through the broker dispatch path — so a provider swap (mock) is honored on direct calls (previously bypassed), MT cross-thread calls go through the channel tunnel with thread affinity preserved, and the raw body runs only inside the provider closure on the owning thread. - First-class provider introspection + mocking on
RequestBroker, both single-thread and MT, per context and per slot (request_broker.nim,mt_request_broker.nim):getCurrentProvider(T, ctx)/getCurrentProviderNoArgs(T, ctx)→Option[provider](distinct names because return-type-only overloads are illegal; both slots can coexist on one broker).replaceProvider(T, ctx, handler)— replace-or-insert; never errors on an existing entry, unlikesetProvider.withMockProvider(T, ctx, mock): body— scoped, exception-safe save/restore template. MT variants read the per-thread threadvar slot and are therefore owning-thread only (documented); cross-thread introspection is unsupported (the shared bucket holds a ring, not the closure).
- Natural
proc newconstructor ergonomics. The magic-selfproc init(...)constructor is replaced by a user-authoredproc new(T: typedesc[Impl], …): Implthat builds and returns a bare instance. The macro wraps it with two decorators:Impl.create(args…)(allocates a fresh instance context, then decorates) andImpl.createUnderContext(ctx, args…)(adopts an external context; renames the oldbindToContext; used by the FFI lane and sub-instance factories). An optionalproc init(self: Impl)post-context hook runs afterbrokerCtxis bound and before providers are wired, for ctor logic deriving per-instance state fromself.brokerCtx. A zero-arg defaultnewis synthesized when omitted.
- chronos dispatcher selector fd leaked per createContext/shutdown cycle.
chronos 4.2.2 has no
PDispatcherteardown and itsSelectorImplhas no=destroy, so each broker thread's selector OS fd (kqueue on macOS, epoll on Linux) was never closed. The API library spawns a processing + delivery thread per_createContextand joins them per_shutdown, so every context lifecycle leaked 2 fds regardless of--mm:refcvs--mm:orc(a missing destructor, not a GC-mode issue). NewcloseThreadDispatcherSelector()inmt_broker_common.nim(chronos public surface only) is called as the last action of both generated broker thread procs, after the dispatch loop has exited. Made portable to the Windows IOCP branch (closeHandleon the IOCPHANDLE, forward-declared to avoid achronos/osdefsimport that destabilised the Windows + Nim 2.2.4 + refc + release MT build). Regression testtest/test_api_library_init.nimruns 20 create/shutdown cycles and asserts no fd growth (verified delta=40 without the fix, delta=0 with it, on orc + refc). The Nim 2.2.4 + Windows leg skips the assertion — a known Nim-runtimejoinThreadHANDLE leak (2/cycle) fixed in Nim 2.2.10. eventCourierleaked on the delivery-thread-not-ready_createContextfailure path. That cleanup branch freed the call courier but never the event courier (unlike the two sibling failure paths), leaking aCborEventCourier+ its 256-slot ring + aLockon the shared heap. Added the missingdrainAndFree(arg.eventCourier).- C++ sub-instance constructor
noexceptmismatch. The CBOR C++ wrapper emitted the sub-instance constructor declaration withoutnoexceptwhile both definitions used it, causing an "exception specification … does not match" error on anyBrokerInterfacereturning a sub-instance. All three sites now agree.
test/test_api_table_codec.nim— 9-shape Table CBOR round-trip against the upstream (unpatched) dependency.test/test_broker_method_tunneling.nim— mock honored on direct + base-typed calls, per-ctx isolation, capture/replace/restore,withMockProvider, cross-thread instance-method tunneling.test/test_mt_provider_mock.nim— MT provider mocking on the owning thread.runTypeMapTestLib{Py,Cpp,Rust,Go}gaintest_map_result_all_key_flavors(int32/char/enum/distinct keys),map_param_roundtrip, andmap_event.
- Design/plan docs relocated under
doc/design/(ASSOC_CONTAINERS_PLAN.md,ASSOC_CONTAINERS_IMPL_PLAN.md) alongside the existingHIERARCHICAL_*/PLAN_*/CBOR_*docs.doc/OOP_Brokers.mdupdated for tunneling, theproc new+create/createUnderContextsurface, and a new "Testing — mocking providers" section.
3.1.1 — 2026-06-02
Flexible MT payload sizing, dynamic CBOR courier growth, void-"payload"
for RequestBroker's proc-syntax-sugar, pure-Nim persistence example.
- Flexible MT payload sizing —
uint32size fields + automatic heap-spill (mt_queue.nim,mt_codec.nim,mt_config.nim,mt_event_broker.nim,mt_request_broker.nim).CellHeader.payloadSizeandResponseSlotHeader.payloadSizewidened fromuint16touint32(the previous 64 KiB ceiling silently wrapped — thelargePayloadpreset storedpayloadSize = 0). When a marshaled payload exceeds the fixed slab cell, the producer spills the bytes to anallocShared0buffer referenced via newCellHeader.overflow/overflowLenfields (symmetric onResponseSlotHeader). The MPSC ring still carries theuint32cell index unchanged. Spill is always-on, gated only by a newmaxDynamicPayloadByteskwarg (defaulthigh(uint32)) as an OOM/DoS backstop. NewmtMarshalSizeValue+ per-type<T>MtMarshalSizecompanions size the spill buffer in one pass. The common fits-the-cell path is unchanged and allocator-free. Verified viatest/test_mt_large_payload.nim(1.5 MiB inline + spill for event/request/response + over-ceiling drop) green on orc/refc × debug/ release; ASAN clean.
voidpayload in proc-sugar form across single-thread, MT, and API brokers.RequestBroker: proc f(...): Future[Result[void, string]](and itssyncvariant) now lowers correctly through every dispatch path: the MT binary response codec guards the four payload-value touches (marshalResp/unmarshalResp/marshalRespSize/ same-thread nil-check) withwhen (payloadType is void), and the API/CBOR adapter bridges thevoidResult to aCborUnitenvelope (bit-for-bit identical to the legacytype X = voidwire form).
- Pure-Nim persistence example (
examples/persistence/nim_example/). Replicates the three C++ scenarios (two contexts, mixed backends, concurrent load) using broker interfaces directly — single-thread chronos async, no FFI, no CBOR. Demonstrates the factory pattern:PersistenceFacaderegisters a zero-argprovideFactoryat module init; the example usesIPersistence.create()and never imports any concrete impl type. Newnimble runPersistenceExampleNimtask (honoursMM=orc|refc).
- Dynamic growth for
CborCallRing/CborEventRingvia append-only slot segments (api_cbor_courier.nim,api_cbor_event_courier.nim,api_request_broker_cbor.nim). Resolves #21. On response-slot-pool exhaustion the pool now grows by appending a new segment rather than reallocating one array — relocating a slot whoseLock/Conda blocked_callis waiting on would be a use-after-free. Append-only segments keep every published slot address stable. Growth coordinates the slot pool and the ring together underring.lock, doubling up to a4 * origSlotCountceiling. A global slot index maps to its owning segment viaslotAt; claims scan the published segments lock-free. Covered by newtest/test_api_courier_growth.nim. - Hardened CBOR courier growth paths against
allocShared0OOM.growRingLockednow returnsbooland leaves the ring untouched on alloc failure.claimSlotnil-checks the new slot segment (release lock + return-1, the refusal contract) and grows the ring before committing any pool state — on ring OOM it rolls back the fresh segment (deinit locks/conds,deallocShared) soslotCount/segs/nSegsstay consistent. Event-courier path nil-checks the grown ring buffer and falls back to the drop contract on OOM instead of nil-deref. Stalemt_queuecell-layout comment corrected — payload now starts atsizeof(CellHeader), not offset 16.
- Single-thread
synckeyed-overload ordering bug uncovered by the proc-sugar work (request_broker.nim). Pre-existing — surfaced when exercising sync sugar viatest/test_request_broker_sync_void.nim.
- Design docs relocated to
doc/design/(Event_Dispatch_Options.md,HIERARCHICAL_BROKERS_PLAN.md,HIERARCHICAL_BROKERS_DEFERRED.md,HIERARCHICAL_BROKERS_REDUCED_A_PLAN.md,PLAN_global_subscount_teardown_fix.md). Newdoc/FLEXIBLE_MT_DISPATCH_PLAN.mddocuments the spill design.
test/test_mt_large_payload.nim— 1.5 MiB inline + spill matrix for event/request/response + over-ceiling drop.test/test_api_courier_growth.nim— pool + ring growth coverage.test/test_request_broker_sync_void.nim— syncvoid-payload proc-sugar regression suite.- New
void-payload cases intest/test_request_broker.nimandtest/test_multi_thread_request_broker.nim.
3.1.0 — 2026-05-28
Hierarchical / OOP brokers — BrokerInterface, BrokerImplement,
RequestBroker proc-sugar, multi-interface FFI with sub-instance routing.
BrokerInterfacemacro (brokers/broker_interface.nim). Declares an abstract interface (ref object of RootObj) with typed request methods (abstract{.base, async:(raises:[]), gcsafe.}methods) and event facades (emit/listen/dropListenertemplates forwarding viaself.brokerCtx). Two forms:BrokerInterface IFace:(in-process only) andBrokerInterface(API, IFace):(generates FFI-capable(API)sub-brokers).BrokerImplementmacro (brokers/broker_implement.nim). Attaches concrete behavior to an interface:BrokerImplement Impl of IFace:with optionalproc init(...)and rawmethodoverrides. GeneratesImpl.new(),setupProviders(closure-capturedselfper request), idempotentclose()(clears providers + drops all event listeners, breaking refc cycles).bindToContext(ctx, ...)adopts an externalBrokerContext(used by FFI processing thread).- Factory / dependency-injection on every
BrokerInterface: typedIFace.provideFactory(f)(last-wins) +IFace.create(...). Consumer depends only on the interface module; implementer callsprovideFactory; composition root wires both. RequestBroker.isProvided— query whether a provider is registered for a given context.
- Proc-style request declaration across single-thread, MT, and API
brokers. Lowercase verb procs (
proc getHealth(...)) pair with a capitalized broker name (GetHealth). POD form (bare scalar/string return) and object form (explicittype GetHealth = object) both supported. Two-slot naming: single signature → bare apiName; both zero-arg + arg-based → bare +<name>Arg. Legacysignature*syntax unchanged.
mainClass: IMaininregisterBrokerLibrarydesignates the library's primary interface. Only the main class exposes_createContext/_shutdown. AdditionalBrokerInterface(API)interfaces become sub-interface wrapper classes.<lib>_releaseInstance(ctx)C-ABI entry point for sub-instance teardown. Clears providers and drops all listeners for the sub-instance context on the processing thread.- Sub-instance routing via
BrokerContextlayout: low 16 bits = classCtx (library identity), high 16 bits = instanceCtx._call(subCtx)recovers the library courier by classCtx masking; the full ctx routes to the sub-instance's provider.newInstanceCtx(parentCtx)allocates a fresh instanceCtx sharing the parent's classCtx. - Sub-instance events: emit-side courier lookup is classCtx-masked so
sub-instance
EventBroker(API)events reach foreign subscribers. Per-classCtx listener installer registry ensures new sub-instances get event-courier wiring automatically. - Per-interface wrapper classes in all four foreign-language codegens:
- C++: sub-interface class with typed methods,
close()→releaseInstance,~Sub()destructor. Event-bearing subs returned asResult<std::unique_ptr<Sub>>(non-movable due to EventDispatcher); event-free subs returned by value. - Rust:
pub struct Sub { ctx }+impl Drop→releaseInstance, typed methods,close(). - Go:
type Sub struct { ctx, mu }+Close()+runtime.SetFinalizer→releaseInstance, typed methods. - Python: sub-class with
(ctx)ctor, typed methods, context-manager support,close()→releaseInstance.
- C++: sub-interface class with typed methods,
BrokerContextlayout split (broker_context.nim): classCtx (low 16)- instanceCtx (high 16).
DefaultBrokerContext = makeBrokerContext(1, 0).NewBrokerContext()allocates a fresh classCtx with instanceCtx 0. Backward-compatible — flat brokers operate with instanceCtx = 0.
- instanceCtx (high 16).
close()clears event listeners via compile-time interface → events registry (broker_utils). EachBrokerImplementclose now callsEventType.dropAllListeners(self.brokerCtx)for every event declared in the interface.
- FFI event subs-count teardown: decrement shared per-event atomic counter on context teardown instead of resetting to zero (could zero out other contexts' subscriptions).
- Sanitizer coverage expanded: UBSan-in-ASan, separate TSan, Linux ASan+LSan modes added to CI.
hierlibandpersistenceOOP examples added to CI matrix (runHierExample{Py,Cpp,Rust,Go},runPersistenceExampleCpp).- New test files:
test_request_broker_sugar.nim,test_broker_oop.nim,test_broker_lifecycle.nim(wired intonimble test);test_broker_interface_api.nim,test_broker_interface_mt.nim(wired intonimble testApi);test/reject/*.nimcompile-fail tests (wired intonimble testSugarRejects). - Broker lifecycle test (
test_broker_lifecycle.nim): skip broadened to cover Nim 2.2.4 + refc + release on Linux/Windows, and refc + Windows debug.
examples/ffiapi/hierlib— single-interface OOP FFI example (BrokerInterface(API)+BrokerImplement+bindToContext+registerBrokerLibrary) with C++, Python, Rust, and Go consumers.examples/persistence— multi-interface example (IPersistencemain +IBackendsub-interface) demonstrating sub-instance creation, per-instance event routing, concurrent multi-context scenarios, and targeted sub-instance teardown. C++ consumer with Rust/Python/Go sub-instance events deferred.
doc/HIERARCHICAL_BROKERS_PLAN.md— full design document for the OOP broker layer.- Updated README and AGENTS.md with OOP broker documentation.
3.0.0 — 2026-05-22
Native C-ABI FFI strategy retired — CBOR is now the only transport. The Nim ↔ FFI wire path is rebuilt on per-context CBOR couriers.
🚨 Breaking vs. 2.x. The
nativetyped-C export ABI is gone. Libraries that compiled against the native per-type C structs (*CItem/*CResult, per-result free helpers, pointer+count batch layout) no longer build. There is no migration shim — port to the CBOR surface (which has been the recommended strategy since 1.0.0). The public wrapper surface for C++ / Rust / Go / Python is unchanged: those wrappers already rode the CBOR ABI, so consumer code that used the typed wrappers needs no source changes — only a rebuild.
- Native C-ABI codegen deleted.
api_codegen_{c,cpp,python,rust,go,nim}.nim,api_event_broker.nim,api_request_broker.nim,api_type.nim, andapi_ffi_mode.nimare removed.brokers/api_library.nimshrinks by ~1080 lines; the branch nets ~12.9k deletions across 104 files. - FFI build flags collapsed to a single switch. Only
-d:BrokerFfiApiremains and it always selects CBOR.-d:BrokerFfiApiNativeis now a hard{.fatal.}compile error;-d:BrokerFfiApiCBORwas a transitional alias and has been removed (bare-d:BrokerFfiApicovers it). - The
RequestBroker(API)/EventBroker(API)macro dispatchers no longer emit a native branch; the deprecatedApiTypeannotation and its dead array-size-const pipework (gArraySizeConsts,registerArraySizeConst) are gone — plain Nimobjecttypes are auto-resolved.
- Request path: the stale
AsyncChannelis replaced by a per-context CBOR request courier (api_cbor_courier.nim) — a POD MPSC ring plus a per-call response slot guarded byLock+Cond. The cross-thread cost of an FFI request is one ring enqueue + one signal (claimSlot → tryEnqueue → fireBrokerSignal → poller → asyncSpawn → handler → completeSlot), with no typed marshalling overhead since.request()runs same-thread on the processing thread. - Event path: a per-context event courier (
api_cbor_event_courier.nim) drives a three-lane dispatch — (1) same-thread Nim listeners via directasyncSpawn, (2) cross-thread Nim listeners via the MT typed-slab path, (3) foreign callbacks via the new courier ring + delivery-thread fan-out. A per-bucket atomicforeignSubsCount, read lock-free on the emit thread, short-circuits the entire FFI lane when no foreign subscriber is registered (zero allocations, zero encode, zero ring touch). When a foreign subscriber exists, emit costs one CBOR encode regardless of subscriber count. Seedoc/CBOR_Round2_PartD_EventCourier.mdanddoc/bench_baseline.md§ "Event dispatch — Part D".
EventBroker(API)/RequestBroker(API)accept the full MT capacity / preset kwargs —queueDepth,slabCapacity,maxPayloadBytes,responseSlots,maxResponseBytes,freeListShards, andpreset = <name>— since the API broker rides the multi-thread lane internally and there is no second transport to size separately. Omitting kwargs yieldsdefaultMtEvtCfg()/defaultMtReqCfg(). Previously these were rejected.
- With native gone, the CBOR surface is now the sole carrier of the full
type matrix: type auto-resolution emits idiomatic typed representations
in every wrapper (C++ struct + jsoncons traits, Python
@dataclass, Rust serde struct, Go struct with cbor tags).seq[Object]batches cross as a single nested CBOR array decoded in the Nim adapter before the provider is invoked — no per-type C struct, no pointer+count layout.
- Full Windows support verified for both
refcandorc. The historicalskipRefcOnWindows/memoryManagerMatrixWindows carve-outs are disabled so the entire wrapper-parity + FFI-example matrix runs--mm:refcand--mm:orcon Windows. CI is green on Windows × Nim 2.2.4 + 2.2.10 acrossnimble test,testApi,runTypeMapTestLib{Cpp,Py,Rust,Go}, andrunFfiExample{Cpp,Py,Rust,Go}. The rawRegisterWaitForSingleObjectTLS-uninit hazard still exists at the OS level (probe reproduces it on refc), but broker code does not trip it — seedoc/LIMITATION.md§2.2. - Support matrix (
doc/LIMITATION.md§1.2) is solid green across Linux + macOS (arm64/amd64) + Windows. Build floor is Nim ≥ 2.2.0 (2.0.x refc deterministically SIGSEGVs onseq[object]FFI payloads, §2.1).
doc/FFI_API.mdswept to the as-shipped CBOR-only runtime: corrected the request-path sequence diagram, cleaned the event-dispatch Mermaid diagram, dropped the native ABI box from the layered-architecture diagram, and replaced the native-shape "Type Mapping Reference" with a two-layer model + per-wrapper cheat-sheet pointing atdoc/TYPE_SURFACE.md/doc/TYPESUPPORT.md.-d:brokerDebugnow dumps generated AST to per-broker files underbuild/broker_debug/(README "## Debug").
2.1.0 — 2026-05-22
FFI type-surface expansion — native Option[T], primitive/void broker
types, tuples, and seq[byte] byte-string fidelity across all five
wrappers.
- Native
Option[T]end-to-end (phases e1–e3) for scalar,string,seq[primitive], and registered-object inner types, across C / C++ / Python / Rust / Go in both native and CBOR modes. Uniform C-ABI layout: everyOptionfield emits an explicit<name>_has_value: boolas the source of truth (not the(nullptr, 0)pattern), so a present-but-empty seq is distinguishable from an absent one.Option[Object]embeds the inner<Inner>CItemby value (no pointer indirection); readers must consulthas_valuefirst. - Primitive broker types —
RequestBroker(API): type X = int32,EventBroker(API): type X = int64. Codegen synthesises a singlevaluefield; the result/payload surfaces as a bare scalar. Across all broker variants (single-thread, MT, API) and all wrappers. - Void broker types —
type X = void. The parser lowersvoidto a unique empty object (unit type) so each broker keeps a distinct identity fortypedescdispatch; the newParsedBrokerType.isVoidflag drops the value parameter. A void request carries only ok/err; a void event is a payload-less notification. Single-thread void events expose an argless listener/emit. CBOR C++ usesjsoncons::jsonfor the empty envelope slot; CBOR Rust uses a#[serde(skip)]placeholder. seq[byte]byte-string fidelity: CBOR Python/Rust/Go inbound byte-string mapping; CBOR C++ now mapsseq[byte]tojsoncons::byte_string(CBOR major type 2) in both directions — fixes jsoncons 1.7.0 encodingstd::vector<uint8_t>as a CBOR array. Note:byte_stringlacks.empty()(use.size() == 0).- Tuple-as-struct codegen + distinct-over-compound mapping (CBOR), with
per-tuple map writer/reader for wire alignment;
Optionfields partitioned into theJSONCONS_N_MEMBER_TRAITStail. - jsoncons 1.7.0 compatibility fix; distinct-over-seq registration.
- Fixed native C++ parity build: un-gated the eight
Optiontests from#ifdef USE_CBOR, and a missing comma in the void-event C++ trait signature. Native C++ parity 114/114, CBOR 119/119. - New reference doc
doc/TYPE_SURFACE.md— full Nim → C/C++/Rust/Go/Python API surface type mapping. RefreshedBrokerDesignPrezi.htmlMT + FFI sections; dropped the TownHall variant.
2.0.1 — 2026-05-13
Hotfix: allow user overloads for MT broker payload field types.
mtMarshalValue/mtUnmarshalValue/mtMarshalSeq/mtUnmarshalSeqinbrokers/internal/mt_codec.nimnow declaremixin mtMarshalValue/mixin mtUnmarshalValue, so user-defined overloads for custom field / element types are picked up at instantiation site instead of being shadowed by the generic codec.
2.0.0 — 2026-05-13
Multi-thread dispatch refactor — lock-free ring + pre-allocated slab + response-slot pool, replacing Channel[T].
- MT broker cross-thread dispatch was rebuilt off Nim stdlib
Channel[T]onto a Vyukov MPSC ring carrying cell indices, a pre-allocated payload slab with refcounted cells, and (forRequestBroker(mt)) a response-slot pool. No per-emit / per-request shared-heap allocation; fan-out is a single slab encode + N ring enqueues with an atomic refcount. - Up to 7.4× throughput and 270× lower average latency vs. the v1.x
Channel[T]design on the cross-thread broadcast benchmark under refc. Full benchmark table indoc/MT_BROKER_REFACTOR_RETROSPECTIVE.md. - Two production bugs closed (LIMITATION.md §2.2, §2.6 — both rooted in
Channel[T].storeAuxdeep-copy interactions with the Nim allocator under refc/ORC). - One shared
ThreadSignalPtrper thread (shared across all(mt)broker types on the thread). fd count is O(threads), not O(brokers × threads). - New compile-time sizing knobs (
queueDepth,slabCapacity,maxPayloadBytes,responseSlots,maxResponseBytes) with type-driven defaults and built-in presets (fastBurst, etc.). Every MT broker call site emits a compile-timehintline with the resolved values, their origin, and an idle-RAM estimate. Seedoc/MT_BROKER_CONFIG.md. - New visible failure mode: bounded ring / pool can return
err(...)/ overflow on full — workloads that relied onChannel[T]'s unbounded buffering must size up or handle the error. - New shared dispatcher hub
brokers/internal/mt_broker_common.nim(getOrInitBrokerSignal,registerBrokerPoller,brokerDispatchLoop,ensureBrokerDispatchStarted,fireBrokerSignal,drainPendingRingFrees). - Fixed a four-bug chain around FFI teardown:
- Use
blockingRequest(notwaitFor) on the FFI caller's thread. - Synchronous deferred ring/slab/pool free on the provider thread.
- Reordered delivery-thread teardown + added
drainPendingRingFrees. - Tear down per-thread
brokerDispatchLoopafter FFIwaitFor.
- Use
- New ASAN coverage for
test_multi_thread_broker_configs. - Resolved
EventBroker/RequestBrokermacro overload ambiguity on Nim 2.2.4.
Breaking:
- Internal MT broker layout changed; payloads no longer flow through
Channel[T]. Anything that introspected the old layout must move tomt_broker_common. - Bounded ring / pool means previously-unbounded workloads can now see
enqueue overflow — review error handling on
emit/request.
1.2.0 — 2026-05-11
Go wrapper generation.
- New
-d:BrokerFfiApiGenGoflag emits a<lib>_go/module (cgo prelude + companion.cfile for typed//exporttrampolines). - Native and CBOR modes reach full parity with Rust on the typemappingtestlib
matrix (41 native / 43 CBOR checks); idiomatic
(T, error)returns,Close()+runtime.SetFinalizer, handler maps undersync.Mutex. - Fixed per-event N×N fan-out and cross-context leakage in Rust + Go codegen.
- All FFI codegen backends (Py / Rust / Go) now emit both methods for
dual-signature
RequestBroker(API). - New
LIMITATION.md §2.1Windows TLS-uninit probe; documented macOS + ORCChannel[T]UAF (§2.6).
1.1.0 — 2026-05-08
Rust wrapper generation.
- New
-d:BrokerFfiApiGenRustflag emits a complete Cargo crate (Cargo.toml+src/lib.rs) next to the.so, with no bindgen/clang dependency —extern "C"blocks are hand-emitted. - Native and CBOR modes reach full parity on the type matrix (primitives,
enums, distinct/alias,
seq[T],seq[string],seq[Object],array[N, primitive]). - CBOR-mode Rust uses a per-method
__Env<T>envelope decoded directly viaciborium(no JSON intermediate — fixesseq[byte]round-trip).
1.0.0 — 2026-05-07
CBOR FFI mode + first stable release.
- New
-d:BrokerFfiApiCBORstrategy: every library exposes a fixed 11-function ABI (_initialize,_createContext,_shutdown,_allocBuffer,_freeBuffer,_call,_subscribe,_unsubscribe,_listApis,_getSchema,_version). Wrappers carry the typed surface; wire format is CBOR. - New
<lib>_version()entry point on every FFI surface (C / C++ / Python / Rust / Go) returning the semver baked fromregisterBrokerLibrary. - Per-library CMake package (
<lib>Config.cmake) emitted alongside generated headers; newtestFfiApiCmaketask validates a downstream consumer. - jsoncons vendored as a git submodule for CBOR-mode C++; new
fetchVendornimble task. - Native ↔ CBOR Python and C++ wrapper parity: the same
cpp_exampleandpython_examplesources compile and run against either build mode (toggled viaUSE_CBOR=ONin the CMake project /MYLIB_BUILD_DIRenv var). - Generated native C++ wrapper reshaped to mirror the CBOR
hpplayout (typedLibclass,Result<T>API,on/offevent API). - CDDL schema emission for the CBOR FFI surface.
0.2.0 — 2026-05-05
Type-support extension + Windows hardening.
- Wider type matrix across the FFI surface (see
doc/TYPESUPPORT.md): enums widened tocintto match the FFI ABI; richer object/seq/array coverage in typemappingtestlib. - Multi-thread broker fixes: per-bucket channel + dispatch-signal leak plug, several broker FFI API teardown ordering fixes.
- Windows FFI builds standardized on
clang + lld + ninja + release UCRT; emit a gnu-format.libimport library via-Wl,--out-implib; skip--mm:refcfor MT and Broker FFI API tasks (chronos Win32RegisterWaitForSingleObjectcallback is unsafe under refc STW GC). - CI: dropped Nim 2.0 from the matrix; split Nim
develinto a non-blocking job; bumped memcheck options to Nim 2.2.10. - Documented macOS + 2.2.4 + refc debug stdlib
Channel[T]regression and skip the affected tests at compile time.
0.1.0 — 2026-04-01
Initial standalone release.
- Broker concept extracted from
logos-messaging/logos-deliveryinto a standalone Nim library (brokersnimble package). - Single-thread brokers:
EventBroker,RequestBroker(async + sync modes),MultiRequestBroker. - Multi-thread brokers:
EventBroker(mt),RequestBroker(mt)withLock-protected shared bucket registry and per-bucketThreadSignalPtr. BrokerContextscoping with thread-global binding and async scoped-swap templates.- Broker FFI API generator (
registerBrokerLibrary): C ABI entry points, generated C header, C++ wrapper, optional Python ctypes wrapper, two-thread runtime (delivery + processing). typemappingtestlibparity harness for C / C++ / Python.