SharedHap intentionally provides no emitSafe (error-reporting
dispatch) counterpart. When that is needed, use Hap, which is single-threaded by design.
Parameters
Args | Argument types passed to attached handlers. |
Module ddn.hap
Provides DDN Haps: synchronous, in-process, application-level notification primitives.
Haps are small multicast observer containers. A subject owns a hap, observers attach handlers to it, and the subject emits the hap when something meaningful "hap"-pens. (You should understand now why I named it "hap"?)
One attached handler record inside HapState, identified by a monotonically increasing id so Registration handles stay stable across splices.
size_t idUnique id assigned by the owning hap.Handler handlerThe attached handler; `.init` marks a removed entry.Heap-backed state shared between a hap and its Registration handles, generic over the record type.
Record[] recordsAttached handlers; may be replaced (not mutated) while dispatching.size_t nextIdNext id handed out by the owning hap.size_t dispatchDepthNumber of in-flight dispatches; copy-on-write is active while non-zero.bool alive`false` once the owning hap has been destroyed.size_t activeCount() const @safe nothrow @nogcCount the attached records. Only live handlers are ever stored, so this is simply the record count.size_t storedRecordCount() const @safe nothrow @nogcNumber of records currently stored regardless of liveness. This is the raw stored-record count, unlike `activeCount` which is only meaningful while the owning hap is alive. Package-visible so unitt...void addRecord(size_t id, Handler handler) @safeAppend a handler record, copying first when a dispatch is in flight.void insertRecord(size_t index, Record record) @safeInsert a record at the given index, shifting later records right, copying first when a dispatch is in flight. Used for sorted insertion by priority-aware haps.void removeRecordById(size_t id) @safeRemove the record with the given id, copying first when dispatching.void removeFirstByHandler(Handler handler) @safeRemove the first record whose handler matches, copying first when dispatching.void removeAt(size_t index) @safeSplice one record out by index; the caller handles copy-on-write.A registration handle returned by Hap, Hap.register or NothrowHap, NothrowHap.register.
Registration represents exactly one attached handler and provides explicit, idempotent detach. It is non-copyable and move-only.
A plain Registration does not auto-detach in its destructor; automatic cleanup belongs to scoped registration helpers and RegistrationBag.
Handler | The delegate handler type of the source hap. |
State-keyed registration implementation behind the public Registration alias.
private State _stateprivate size_t _idprivate bool _detachedbool attached() const @safe nothrow @nogcDetermine whether this registration's source hap is alive and this registration has not been detached.this(State state, size_t id)An RAII wrapper that detaches a Registration, registration automatically at scope exit.
ScopedRegistration is non-copyable and safely movable. When a scoped registration is destroyed, it calls detach() on the underlying Registration. Call release() to transfer ownership or disable automatic cleanup; release() returns the underlying Registration by move and leaves the scoped wrapper inert.
Handler | The delegate handler type of the source hap. |
Registration-keyed scoped wrapper behind the public ScopedRegistration alias.
private Reg _regbool attached() const @safe nothrow @nogcDetermine whether this scoped registration's underlying registration is still attached.this(Reg reg)~thisDestroy this scoped registration, detaching the underlying registration if it is still attached.One stored detach callback inside RegistrationBag.
bool delegate() @safe detachDetach callback; `null` marks an already-cleared entry.Shared state behind a pending registerOneShot registration.
Reg.Handler userHandlerThe wrapped user handler, invoked at most once.Reg regThe underlying regular registration.bool fired`true` once the user handler has been delivered.this(Reg.Handler h)Wrap a user handler.A handle returned by registerOneShot that can cancel a pending one-shot registration.
OneShotRegistration is non-copyable and move-only. Call cancel() to prevent the one-shot handler from ever running, or check attached() to see if it is still pending.
Handler | The delegate handler type of the source hap. |
Registration-keyed one-shot handle behind the public OneShotRegistration alias.
private OneShotState!Reg _statebool attached() const @safe nothrow @nogcDetermine whether the one-shot handler has not yet been delivered and has not been cancelled.this(OneShotState!Reg state)Heap-anchored registration so an owner-held container can hold a registration by pointer and keep its detach callback addressable. Works for any handle type exposing attached() and detach().
Reg regThe anchored registration.this(ref Reg r)Take ownership of a registration.An owner-held container that detaches multiple heterogeneous registrations in bulk.
RegistrationBag stores registrations from any number of Hap or NothrowHap instances, regardless of their argument types, using internal type erasure. Call clear() or let the bag be destroyed to detach all live registrations.
Multiple private bags are expected in real programs: each component, library, plugin, controller, or test fixture that creates registrations should own its own bag. A global or shared bag is not recommended.
RegistrationBag is non-copyable and move-only.
private DetachRecord[] _recordsvoid add(Reg)(auto ref Reg reg) if (is(typeof(reg.attached()) == bool) && is(typeof(reg.detach()) == bool)) @safeAdd a registration handle to this bag.void register(Args...)(ref Hap!Args hap, void delegate(Args) @safe handler) @safeRegister a handler on a hap and add the resulting registration to this bag.void register(Args...)(ref NothrowHap!Args hap, void delegate(Args) @safe nothrow handler) @safe nothrowdittovoid register(Args...)(ref NothrowHap!Args hap, void function(Args) @safe nothrow handler) @safe nothrowditto~thisDestroy this bag, detaching all live registrations.A multicast hap that invokes all attached handlers synchronously.
Hap is the general-purpose, GC-backed variant for ordinary application code. It accepts @safe handlers, supports throwing handlers, and uses snapshot dispatch semantics: mutations made during emit affect later emits, not the handler slice currently being iterated.
Hap is intentionally not thread-safe. Use it as thread-confined owned state, or provide external synchronization when deliberately sharing an owning object.
Args | Argument types passed to attached handlers. |
A multicast hap restricted to handlers that cannot throw exceptions.
NothrowHap has the same ownership and snapshot semantics as Hap, but its handlers are nothrow, so emit is also nothrow.
Args | Argument types passed to attached handlers. |
A fixed-capacity multicast hap for @nogc nothrow dispatch paths.
StaticHap stores handlers inline, never allocates, and reports attachment failure when the configured capacity is reached.
StaticHap does not provide register because the registration lifetime model requires heap-backed state that would compromise @nogc dispatch. Use attach and detach for StaticHap handler management.
capacity | Maximum number of handlers stored by this hap. |
Args | Argument types passed to attached handlers. |
private Handler[capacity] _handlersprivate size_t _countbool attach(FunctionHandler handler) @safe nothrow @nogcAttach a `@nogc nothrow` free-function handler.void opOpAssign(string op : "~")(Handler handler) @safe nothrow @nogcAttach a `@nogc nothrow` delegate handler with `~=` syntax.void opOpAssign(string op : "~")(FunctionHandler handler) @safe nothrow @nogcAttach a `@nogc nothrow` free-function handler with `~=` syntax.bool detach(Handler handler) @safe nothrow @nogcDetach the first matching delegate handler by delegate identity.bool detach(FunctionHandler handler) @safe nothrow @nogcDetach the first matching free-function handler.void opCall(Args args) @safe nothrow @nogcEmit this hap by invoking a stack-local snapshot of fixed handler storage.An observable value that emits when assignment changes the stored value.
Property!T is a small subject in observer-pattern terms: it owns one value of type T and emits onChanged with the old and new values whenever a successful assignment occurs. Unchanged assignments (where the new value compares equal to the old value) do not emit.
Equality is `==`, which is what users expect for strings, arrays and structs. The floating-point consequence: NaN == NaN is false, so assigning NaN to a NaN-valued property emits every time, while -0.0 and 0.0 compare equal and do not emit.
Property is non-copyable because the embedded onChanged hap is an owned notification channel. Move semantics are available via core.lifetime.move.
T | The type of the stored value. |
Hap!(T, T) onChangedEmitted with `(oldValue, newValue)` after an accepted assignment.private T _valueOne attached handler record in SharedState, keyed by a monotonically increasing id so SharedRegistration handles stay stable across splices.
size_t idUnique id assigned by the owning hap.Handler handlerThe attached handler.Lock-protected handler list shared by all SharedHap handles and threads.
SharedRecord!Handler[] recordsAttached handlers; only mutated under `mutex` through `sharedRecordsUnderLock`.size_t dispatchDepthNumber of in-flight dispatches; copy-on-write is active while non-zero. `shared`-qualified and only read or written while `mutex` is held, through plain non-atomic accesses the lock makes safe.size_t nextIdNext id handed out by the owning hap; only touched under `mutex`.Mutex mutexGuard for `records`, `dispatchDepth` and `nextId`; handlers run outside this lock.this()Create the state with its mutex ready.A thread-safe registration handle returned by SharedHap, SharedHap.register.
SharedRegistration represents exactly one attached handler on a SharedHap and provides explicit, idempotent detach. Both attached() and detach() take the internal lock and look the record up by its id, so the handle stays valid across handler-list splices and concurrent emits. It is non-copyable, move-only, and inert when default-initialised or moved-from.
Handler | The delegate handler type of the source hap. |
this(shared(SharedState!Handler) state, size_t id)A thread-safe multicast hap that invokes all attached handlers synchronously.
SharedHap provides the same attach/detach/emit semantics as Hap but with internal synchronization for safe use across threads. Every public method is a this This template, so the hap is usable both as a plain local (SharedHap!T) and as a field of a shared object (shared SharedHap!T) with no casts at call sites.
Handlers are invoked outside the internal lock to prevent deadlocks during re-entrant calls. The internal state is published with an atomic compare-and-swap on first use and every access reads it with acquire semantics, so concurrent first use from multiple threads is safe: no handler is ever lost to an initialization race.
Snapshot semantics: emit captures a snapshot of current handlers under the lock, then releases the lock before invoking handlers. Mutations during emit affect future snapshots, not the current one. A handler already captured in an in-flight snapshot may still run after detach or detachAll returns.
Handler model: handlers are void delegate(Args) @safe, not shared delegates. Handlers run on the emitting thread and must be thread-safe by construction; SharedHap protects only its own handler list, nothing the handlers themselves touch.
SharedHap is non-copyable and move-only. It uses reference-backed shared state so in-flight snapshots remain valid even if the public SharedHap value is moved or destroyed after snapshot capture. Moving the struct value while another thread holds a reference to it is the caller's race; only the heap state is protected.
SharedHap intentionally provides no emitSafe (error-reporting
dispatch) counterpart. When that is needed, use Hap, which is single-threaded by design.
Args | Argument types passed to attached handlers. |
private shared(SharedState!Handler) _statevoid opOpAssign(string op : "~", this This)(Handler handler) @safeAttach a delegate handler with `~=` syntax.void opOpAssign(string op : "~", this This)(FunctionHandler handler) @safeAttach a free-function handler with `~=` syntax.void detach(this This)(Handler handler) @safeDetach the first matching delegate handler by delegate identity.void detach(this This)(FunctionHandler handler) @safeDetach the first matching free-function handler.SharedRegistration!Handler register(this This)(Handler handler) @safeRegister a delegate handler and return a detachable handle.SharedRegistration!Handler register(this This)(FunctionHandler handler) @safeRegister a free-function handler and return a detachable handle.void opCall(this This)(Args args) @safeEmit this hap by invoking all handlers captured at dispatch start.shared(SharedState!Handler) loadState(this This)() @safe nothrowRead the shared state with acquire semantics so a thread can never observe a stale `null` after another thread has published the state.void ensureState(this This)() @safe nothrowCreate the shared state on first use, publishing it atomically.A multicast hap that dispatches handlers in priority order.
PriorityHap associates each handler with an integer priority. Handlers with lower priority values run first. Handlers with equal priority run in stable attachment order. This is a separate type from Hap to keep normal attachment-order dispatch simple and fast.
PriorityHap supports attach, detach, detachAll, emit, and hasHandlers/handlerCount. It is non-copyable and move-only.
Args | Argument types passed to attached handlers. One attached handler record with its dispatch priority. |
size_t idUnique id assigned by the owning hap; monotonic, so it also encodes insertion order among equal priorities.Handler handlerThe attached handler.int priorityLower values run first; equal priorities dispatch in insertion order.void attach(Handler handler, int priority = 0) @safeAttach a delegate handler with the given priority.void attach(FunctionHandler handler, int priority = 0) @safeAttach a free-function handler with the given priority.void opOpAssign(string op : "~")(Handler handler) @safeAttach a delegate handler with `~=` syntax at default priority 0.void opOpAssign(string op : "~")(FunctionHandler handler) @safeAttach a free-function handler with `~=` syntax at default priority 0.Reg register(Handler handler, int priority = 0) @safeRegister a delegate handler with the given priority and return a detachable handle.Reg register(FunctionHandler handler, int priority = 0) @safeRegister a free-function handler with the given priority and return a detachable handle.ScopedRegistrationOf!Reg scopedRegister(Handler handler, int priority) @safeRegister a delegate handler with the given priority and return an RAII handle that detaches automatically at scope exit.ScopedRegistrationOf!Reg scopedRegister(FunctionHandler handler, int priority) @safeRegister a free-function handler with the given priority and return an RAII handle that detaches automatically at scope exit.OneShotRegistrationOf!Reg registerOneShot(Handler handler, int priority) @safeRegister a delegate handler with the given priority that detaches itself after the first invocation.OneShotRegistrationOf!Reg registerOneShot(FunctionHandler handler, int priority) @safeRegister a free-function handler with the given priority that detaches itself after the first invocation.void registerInto(ref RegistrationBag bag, Handler handler) @safeRegister a delegate handler at default priority 0 on this hap and store the registration in a bag.void registerInto(ref RegistrationBag bag, FunctionHandler handler) @safeRegister a free-function handler at default priority 0 on this hap and store the registration in a bag.void registerInto(ref RegistrationBag bag, Handler handler, int priority) @safeRegister a delegate handler with the given priority on this hap and store the registration in a bag.void registerInto(ref RegistrationBag bag, FunctionHandler handler, int priority) @safeRegister a free-function handler with the given priority on this hap and store the registration in a bag.void insertByPriority(Record record) @safeInsert a record before the first record with a greater priority, or append when no such record exists. Equal priorities stay in insertion order because ids are monotonic.Cancellation state passed to handlers during a CancellableHap emit.
A handler calls cancel() to stop later handlers in the same emit from running. Cancellation is one-way: once cancelled, it cannot be undone.
private bool _cancelledA multicast hap that supports propagation-stopping workflows.
CancellableHap passes a CancelToken as the first argument to each handler. When a handler calls token.cancel(), subsequent handlers in that emit are skipped. Handlers before cancellation run in attachment order.
This is a separate type from Hap to keep normal dispatch simple and to make cancellation explicit at the handler signature level.
Args | Argument types passed to attached handlers (excluding CancelToken). |
A multicast hap whose handlers return a value that is collected.
QueryHap is an observer container where every handler answers the query: emit invokes all attached handlers in attachment order and returns their results as a Result[]. Use it to fan a request out to several responders and gather every answer, such as validation votes, plugin lookups, or metric samples.
Snapshot semantics: emit captures the handler list at dispatch start, so attaching or detaching during dispatch affects only future emissions.
Result | The return type of each handler. |
Args | Argument types passed to attached handlers. |
A hap that replays the last emitted value to newly attached handlers.
ReplayHap stores the most recent arguments from the last emit call. When a handler is attached, it immediately receives the stored value if one exists. Subsequent emit calls update the stored value and invoke all handlers as usual.
Two semantics distinguish ReplayHap from the core haps:
attach invokes the new handler synchronously with the stored value, even while an emit on the same hap is in flight. This is a deliberate exception to the core rule that a handler attached during emit runs only on a later emit: a late observer must not miss the current value.emit stores the new value before dispatching. If a handler throws, the stored value is already updated and later attaches replay it.This is useful for state-like values such as settings, selection state, or configuration values where new observers need the current value immediately.
Args | Argument types for the hap. |
void attach(Handler handler) @safeAttach a delegate handler. If a value has been emitted, the handler is invoked immediately with the stored value.void attach(FunctionHandler handler) @safeAttach a free-function handler. If a value has been emitted, the handler is invoked immediately with the stored value.void opOpAssign(string op : "~")(Handler handler) @safeAttach a delegate handler with `~=` syntax. Replays the stored value to the new handler when one exists.void opOpAssign(string op : "~")(FunctionHandler handler) @safeAttach a free-function handler with `~=` syntax. Replays the stored value to the new handler when one exists.Reg register(FunctionHandler handler) @safeRegister a free-function handler and return a detachable handle.void emit(Args args) @safeEmit and store the value. All handlers are invoked, and the value is stored for replay to future attach calls.void reset() @safe nothrowReset the stored value. Future attach calls will not replay until emit is called again.auto assumeSafeAppendTrusted(T)(ref T[] records) @trusted nothrowRestore the append capacity of a shrunk record slice.auto scopedRegister(Args...)(ref Hap!Args hap, void delegate(Args) @safe handler) @safeCreate a scoped registration that detaches automatically at scope exit.auto scopedRegister(Args...)(ref NothrowHap!Args hap,
void delegate(Args) @safe nothrow handler) @safe nothrowdittoauto scopedRegister(Args...)(ref NothrowHap!Args hap,
void function(Args) @safe nothrow handler) @safe nothrowdittoauto registerOneShot(Args...)(ref Hap!Args hap, void delegate(Args) @safe handler) @safeRegister a one-shot delegate handler on a Hap that detaches itself after the first invocation.auto registerOneShot(Args...)(ref NothrowHap!Args hap,
void delegate(Args) @safe nothrow handler) @safe nothrowdittovoid registerInto(Args...)(ref RegistrationBag bag, ref Hap!Args hap,
void delegate(Args) @safe handler) @safeRegister a handler on a hap and add the resulting registration to a bag.void registerInto(Args...)(ref RegistrationBag bag, ref Hap!Args hap,
void function(Args) @safe handler) @safedittovoid registerInto(Args...)(ref RegistrationBag bag, ref NothrowHap!Args hap,
void delegate(Args) @safe nothrow handler) @safe nothrowdittovoid registerInto(Args...)(ref RegistrationBag bag, ref NothrowHap!Args hap,
void function(Args) @safe nothrow handler) @safe nothrowdittoSharedRecord!Handler[] * sharedRecordsUnderLock(Handler)(
shared SharedState!Handler state) @trustedReturns the record array as unshared. Caller MUST hold `state.mutex`: the lock provides the happens-before edge that makes the cast sound.void sharedRemoveRecordAt(Handler)(shared SharedState!Handler state,
size_t index) @safeSplice one record out by index under the lock.auto registerFiltered(Args...)(ref Hap!Args hap,
bool delegate(Args) @safe predicate,
void delegate(Args) @safe handler) @safeRegister a filtered delegate handler that invokes only when a predicate matches.auto registerFiltered(Args...)(ref NothrowHap!Args hap,
bool delegate(Args) @safe nothrow predicate,
void delegate(Args) @safe nothrow handler) @safe nothrowdittoShared implementation core of the dynamic hap family.
HapCore provides the storage, attach/detach/register surface, count queries and dispatch bookkeeping common to Hap, NothrowHap and the specialised variants. It is a mixin template so each hap struct keeps its value semantics; attributes beyond the explicit @safe (nothrow, @nogc) are inferred per instantiation exactly as they were before the mixin existed.
Handler_ | Delegate handler type accepted by the hap. |
Record_ | Record type stored in the hap state; must provide a size_t id and a handler field. Derives the predicate type used by registerFiltered: the same parameter list and function attributes as Handler, but returning bool. |