Catalog · How the catalog works
maxEventNexus
- ★ 1 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
EventNexus
What's New (2026-04-17)
- Added and documented the full mailbox delivery family: typed
SubscribeIn<T>, exact namedSubscribeNamedIn, named typedSubscribeNamedOfIn<T>, and GUID typedSubscribeGuidOfIn<T>. - Added maintained mailbox samples for mixed topic families, mailbox coalescing, mailbox overflow, and
Clear/Close(True)/Close(False)lifecycle boundaries. - Added
MailboxWorkerIntegrationSampleas the realistic worker/control/progress mailbox sample: named job commands into a worker mailbox, typed progress/results back to the caller mailbox, and exact named shutdown. - Added mailbox-owned overflow policy (
TmaxMailboxPolicy) withMailboxDropNewest,MailboxDropOldest,MailboxBlock, andMailboxDeadline, plus benchmarked/documented portable-only specialization status. - Refreshed the published isolated-process benchmark snapshot for Win32/Win64 async and posting profiles, and stabilized the framework benchmark smoke path around the final
SchedulerComparecontract. Clearnow stays a runtime reset: explicit queue policies and coalescing configuration survive, while queued/pending runtime state is still dropped.PostResult<T>,PostResultNamedOf<T>, andPostResultGuidOf<T>now report liveAutoSubscribehandlers as real receivers instead of returningNoTopic.- Deferred-only
AutoSubscribehandlers now makePostResult<T>,PostResultNamedOf<T>, andPostResultGuidOf<T>returnQueuedinstead of misreportingDispatchedInline, withMain/Backgrounddecided from the actual runtime context. - Named-of topics now resolve queue presets as explicit
SetPolicyNamed(name, ...)policy, then name preset, then type preset, thenUnspecified; type-preset changes also reapply to existing implicit named-of topics. bench/BenchHarnessnow uses the supportedTmaxBus/maxBusObj(...)bridge contract and has a maintainedbench/BenchHarness.dprojbuild path.- Runtime/public units are now fully Delphi-only; remaining non-Delphi conditionals were removed from adapter/facade scheduler paths.
- Tests run through DUnitX (
tests/MaxEventNexusTests.dpr) with compatibility support for published-method legacy suites. - Delphi AutoSubscribe one-parameter attributed handlers now bind correctly for typed, named, and GUID topics.
- Test/CI scripts enforce diagnostics policy (
build/diagnostics-policy.regex) so untriaged warnings/hints fail the build. - Async benchmark profile is stabilized (bounded queue/in-flight guards + scheduler submission fallbacks), with CSV contract rows remaining
status=ok. - Sample and benchmark projects now carry explicit unit search paths for shared foundation units in group builds.
- Queue policy preset defaults/overrides and lock-free posting behavior are documented for typed, named, and GUID topics.
- Added delayed posting APIs (
PostDelayed*) with cancel/pending handle semantics. - Reduced static-analysis top debt again (
C101=20,C103=14) with test-fixture refactors and no public API changes. - Hardened delayed-posting edge coverage: zero-delay dispatch/metrics/cancel semantics and deterministic long-delay pending/cancel behavior.
- Added optional strong object-method subscriptions (
SubscribeStrong<T>,SubscribeNamedOfStrong<T>,SubscribeGuidOfStrong<T>) for callers that guarantee subscriber lifetime and want to skip weak-liveness tracking overhead.
EventNexus is a type-safe event bus for Delphi 12+ with typed, named, and GUID topic routing, delivery-mode control, sticky cache, topic and mailbox coalescing, queue policies, and receiver-owned mailbox delivery.
Start Here
- New to the library: read
Core API shape on Delphi, thenDelivery modes and main-thread policy, thenMailbox delivery. - Migrating from older code: use
MIGRATION.md. - Looking for runnable examples first: use
samples/readme.md. - Looking for the full contract and internals: use
spec.mdandDESIGN.md.
Docs Map
README.md: the fastest product overview, current API shape, and maintained sample map.MIGRATION.md: "old pattern to current EventNexus pattern" guidance.spec.md: the public contract, semantics, and testing matrix.DESIGN.md: the architecture sketch and runtime moving parts.samples/readme.md: sample-by-sample guidance and suggested reading order.bench/readme.md: benchmark entry points, profiles, and interpretation notes.
Performance and Scope Snapshot (2026-03-21)
The cross-library benchmark uses a deliberately small common surface (subscribe + post) across EventNexus, iPub, and EventHorizon.
So raw speed numbers below do not include advanced features such as delayed posting, queue policies, tracing, or sticky/coalescing behavior.
Method:
- Tool:
bench/SchedulerCompare.exewithbench/run-framework-isolated.sh - Isolated-process medians (fresh process per sample)
- Async profile:
--events=2000 --consumers=2 --runs=1 --delivery=async --max-inflight=0 - Posting profile:
--events=2000 --consumers=2 --runs=1 --delivery=posting --max-inflight=0 - Successful samples per framework:
5(retry budgetmax-attempts=10) - Platform lanes: Win32 and Win64
Median async framework results (Win32):
| Framework | Median avg us | Median throughput evt/s | Successful samples | Attempts |
|---|---|---|---|---|
| EventNexus(TTask-weak) | 31003 | 129019 | 5 | 9 |
| EventNexus(TTask-strong) | 18856 | 212134 | 5 | 5 |
| iPub | 16493 | 242527 | 5 | 5 |
| EventHorizon | 16267 | 245896 | 5 | 5 |
Median async framework results (Win64):
| Framework | Median avg us | Median throughput evt/s | Successful samples | Attempts |
|---|---|---|---|---|
| EventNexus(TTask-weak) | 35768 | 111831 | 5 | 5 |
| EventNexus(TTask-strong) | 23531 | 169988 | 5 | 6 |
| iPub | 9431 | 424133 | 5 | 5 |
| EventHorizon | 27335 | 146332 | 5 | 5 |
Median posting framework results (Win32):
| Framework | Median avg us | Median throughput evt/s | Successful samples | Attempts |
|---|---|---|---|---|
| EventNexus(TTask-weak) | 856 | 4672897 | 5 | 5 |
| EventNexus(TTask-strong) | 783 | 5108556 | 5 | 5 |
| iPub | 644 | 6211180 | 5 | 5 |
| EventHorizon | 1687 | 2371072 | 5 | 5 |
Median posting framework results (Win64):
| Framework | Median avg us | Median throughput evt/s | Successful samples | Attempts |
|---|---|---|---|---|
| EventNexus(TTask-weak) | 996 | 4016064 | 5 | 5 |
| EventNexus(TTask-strong) | 761 | 5256241 | 5 | 5 |
| iPub | 608 | 6578947 | 5 | 5 |
| EventHorizon | 1693 | 2362669 | 5 | 5 |
Interpretation:
- Win32 async: EventHorizon and iPub lead this run set; EventNexus strong is third, while EventNexus weak needed several retries before producing five successful samples.
- Win64 async: iPub leads, EventNexus strong is second, EventHorizon is third, and EventNexus weak is fourth; EventNexus strong needed one retry to complete the sample set.
- Posting lanes: iPub leads both lanes, EventNexus strong is second in both lanes, EventNexus weak is third, and EventHorizon is slowest.
- RawThread is intentionally absent from the cross-framework tables: those rows keep EventNexus on the same
subscribe + postTTask-backed surface as the comparison libraries, while RawThread remains our dedicated-thread non-pool scheduler when we want isolation from the Delphi thread pool.
Feature scope beyond baseline pub/sub:
- The benchmark surface is minimal (
subscribe/post), but the runtime surface is broader: typed topics, named topics, GUID topics, 4 delivery modes, main-thread fallback policy, sticky cache, topic coalescing, mailbox delivery, mailbox coalescing, topic queue overflow policies, mailbox-owned overflow policies, queue presets, delayed posting with cancel/pending handle,TryPost*andPostResult*, metrics snapshots/callbacks, wildcard named subscriptions, bulk dispatch APIs, dispatch tracing hooks, and weak/strong object-method subscription modes.
Core API shape on Delphi
Delphi does not allow generic methods on interfaces (E2535), so the public API is split:
ImaxBus/ImaxBusAdvanced/ImaxBusQueues/ImaxBusMetricsexpose non-generic named operations.TmaxBusexposes generic typed/named/GUID methods.- Typed bridge
maxBusObj(...)gives access toTmaxBuswhen we start from an interface.
Interface-first example (named topic)
uses
maxLogic.EventNexus;
var
lBus: ImaxBus;
lSub: ImaxSubscription;
begin
lBus := maxBus;
lSub := lBus.SubscribeNamed('ping',
procedure
begin
Writeln('pong');
end,
TmaxDelivery.Posting);
lBus.PostNamed('ping');
lSub := nil; // auto-unsubscribe
end;Generic example (typed topic)
uses
maxLogic.EventNexus, maxLogic.EventNexus.Core;
type
TOrderPlaced = record
Id: Integer;
end;
var
lBus: TmaxBus;
lSub: ImaxSubscription;
begin
lBus := maxBusObj;
lSub := lBus.Subscribe<TOrderPlaced>(
procedure(const aEvent: TOrderPlaced)
begin
Writeln(aEvent.Id);
end,
TmaxDelivery.Posting);
lBus.Post<TOrderPlaced>(Default(TOrderPlaced));
lSub := nil;
end;Delivery modes and main-thread policy
TmaxDelivery values:
Posting: inline on the caller thread.Main: marshaled to main/UI thread when available.Async: queued via configured scheduler.Background: async when called on main thread, otherwise inline.
For console/service contexts where Main cannot marshal to a UI loop, configure fallback behavior:
maxSetMainThreadPolicy(TmaxMainThreadPolicy.DegradeToPosting);
// or DegradeToAsync / StrictPolicy behavior:
Strict: raisesEmaxMainThreadRequired.DegradeToAsync: reroutesMaindelivery to scheduler async path.DegradeToPosting: runs handler on posting thread.
Delayed posting
Use delayed post APIs when event delivery should happen later:
var
lBus: TmaxBus;
lDelayed: ImaxDelayedPost;
begin
lBus := maxBusObj;
lDelayed := lBus.PostDelayed<Integer>(42, 30000); // 30 seconds
// lDelayed.Cancel;
end;Timing note:
PostDelayed*delay values are configured in milliseconds.- Scheduler/coalescing internals use microsecond request units, but on current Delphi/Windows backends those requests are best-effort and may round up to the nearest supported timer resolution.
0remains immediate-eligible; any positive delay remains delayed.- Delayed-post failures stay on the async path: install
maxSetAsyncErrorHandler(...)if we need to observe them. - Without an async error hook, a delayed handler failure is swallowed after the delayed execution path and is not synchronously re-raised to the original
PostDelayed*caller.
Queue policies
Per-topic policy (TmaxQueuePolicy) controls bounded queue behavior:
MaxDepth = 0means unbounded.Overflow = DropNewest | DropOldest | Block | Deadline.DeadlineUsis used byDeadlineoverflow mode.
Set explicit policy:
var
lQueues: ImaxBusQueues;
lPolicy: TmaxQueuePolicy;
begin
lQueues := maxBus as ImaxBusQueues;
lPolicy.MaxDepth := 256;
lPolicy.Overflow := TmaxOverflow.DropOldest;
lPolicy.DeadlineUs := 0;
lQueues.SetPolicyNamed('orders.state', lPolicy);
end;Preset defaults (TmaxQueuePreset) apply only when no explicit policy exists:
| Preset | MaxDepth | Overflow | DeadlineUs |
|---|---|---|---|
State |
256 | DropOldest |
0 |
Action |
1024 | Deadline |
2000 |
ControlPlane |
1 | Block |
0 |
Unspecified |
0 | DropNewest |
0 |
Configure presets:
maxSetQueuePresetNamed('orders.state', TmaxQueuePreset.State);
maxSetQueuePresetForType(TypeInfo(TOrderPlaced), TmaxQueuePreset.Action);
maxSetQueuePresetGuid(StringToGUID('{00000000-0000-0000-0000-000000000000}'), TmaxQueuePreset.ControlPlane);For named-of topics (SubscribeNamedOf<T> / PostNamedOf<T>), implicit queue policy precedence is:
- explicit
SetPolicyNamed(aName, ...) maxSetQueuePresetNamed(aName, ...)maxSetQueuePresetForType(TypeInfo(T), ...)Unspecified
That means a type preset now acts as the fallback default for named-of topics when no name-specific preset exists, and preset updates reapply to already-created named-of topics while they still use implicit policy.
Sticky and coalescing
- Sticky:
EnableSticky<T>(True)/EnableStickyNamed(...)caches latest event. - Coalescing:
EnableCoalesceOf<T>(...),EnableCoalesceNamedOf<T>(...),EnableCoalesceGuidOf<T>(...)keeps latest value per key per window. - A
0coalesce window still flushes on a deferred scheduler turn so one posting burst can settle to the latest value instead of racing an inline flush. - Mailbox coalescing is a separate receiver-side layer on payload-carrying mailbox subscriptions: it keeps the latest pending item per
(subscription, mailbox key)while unrelated keys keep FIFO order.
Clear stays a runtime reset, not a durable-config wipe:
Cleardrops subscriptions, queued work, sticky cached values, pending coalesce batches, and delayed posts scheduled before the clear boundary.Clearpreserves sticky enablement, explicit per-topic queue policies, queue presets, coalescing selectors/windows, scheduler identity, and bus main-thread identity.
Mailbox delivery
Mailbox delivery adds a receiver-owned execution context without introducing a platform-specific message loop.
- Create
ImaxMailbox/TmaxMailboxon the receiver thread. - Subscribe through
TmaxBus.SubscribeIn<T>(...),SubscribeNamedIn(...),SubscribeNamedOfIn<T>(...), orSubscribeGuidOfIn<T>(...). - Post from any other thread as usual.
- Pump the mailbox on the receiver thread so queued handlers execute there.
Baseline mailbox semantics:
- Cooperative pumping: the receiver thread must call
PumpOne(...)orPumpAll. - Owner-thread enforcement:
PumpOneandPumpAllfail fast on the wrong thread. - FIFO per mailbox.
- Default mailbox policy is unbounded, but mailbox overflow is configurable per mailbox through
TmaxMailboxPolicywithMailboxDropNewest,MailboxDropOldest,MailboxBlock, andMailboxDeadline. - Mailbox overflow is receiver-local final handoff policy; it is not inherited from topic queue policies or queue presets.
- Mailbox coalescing is receiver-side and distinct from topic coalescing.
Clearpurges queued mailbox work owned by the bus.Close(True)rejects future enqueue and discards queued-but-not-dequeued items.- No specialized mailbox implementation ships today; the portable mailbox remains the only maintained path until benchmark thresholds justify a platform-specific version.
Typical patterns:
var
lBusObj: TmaxBus;
lMailbox: ImaxMailbox;
begin
lBusObj := maxBusObj(maxBus);
lMailbox := TmaxMailbox.Create;
lBusObj.SubscribeIn<Integer>(lMailbox,
procedure(const aValue: Integer)
begin
Writeln(aValue);
end);
while not Terminated do
lMailbox.PumpOne(cMaxWaitInfinite);
end;See samples/MailboxWorkerSample.dpr for the basic worker-thread pattern, samples/MailboxTopicFamiliesSample.dpr for the full mailbox subscribe family, samples/MailboxClearShutdownSample.dpr for Clear / Close(True) / Close(False), samples/MailboxLatestWinsSample.dpr for mailbox coalescing, and samples/MailboxOverflowSample.dpr for mailbox overflow.
Maintained mailbox sample guide:
MailboxWorkerSample.dpr: the minimal owner-thread worker pattern (PumpOne(cMaxWaitInfinite)).MailboxWorkerIntegrationSample.dpr: a realistic worker flow with named job commands into the worker mailbox, typed progress/results back to the caller mailbox, and an exact named stop command.MailboxTopicFamiliesSample.dpr: one mailbox handling typed, exact named, named-of, and GUID mailbox subscriptions together.MailboxLatestWinsSample.dpr: receiver-side mailbox coalescing on payload-carrying subscriptions.MailboxOverflowSample.dpr: bounded-mailbox overflow withMailboxDropNewest.MailboxClearShutdownSample.dpr:Clear,Close(True), andClose(False)lifecycle boundaries.
Scheduling adapters
IEventNexusScheduler implementations shipped in this repo:
maxLogic.EventNexus.Threading.MaxAsync(recommended default)maxLogic.EventNexus.Threading.RawThread(threading fallback)maxLogic.EventNexus.Threading.TTask
Recommendation:
- Default to
MaxAsyncfor lowest async latency and highest throughput on current Delphi targets. - Use
RawThreadwhen you want the scheduler to bypass the Delphi thread pool and run async/delayed work on dedicatedTThreadinstances. - Use
TTaskwhen you explicitly want RTL-native scheduling semantics. MaxAsyncpreserves async/delayed semantics on backend submission failure by falling back to dedicated-thread execution before any final inline safety net.
Delay-resolution note:
RunDelayed(aDelayUs)is a best-effort request, not a promise of exact microsecond wake-up precision.- Shipped adapters preserve the semantic distinction between
0and positive delay values; positive delays may round up to backend timer granularity.
Object-method lifetime modes
Subscribe<T>(objproc),SubscribeNamedOf<T>(objproc), andSubscribeGuidOf<T>(objproc)use weak-target liveness tracking by default.SubscribeStrong<T>,SubscribeNamedOfStrong<T>, andSubscribeGuidOfStrong<T>intentionally skip weak-target checks for callers that can guarantee subscriber lifetime through unsubscribe/release.- Use the strong variants only when we fully control the subscriber lifetime and want to skip weak-liveness overhead.
Inject at runtime:
uses
maxLogic.EventNexus, maxLogic.EventNexus.Threading.MaxAsync;
begin
maxSetAsyncScheduler(CreateMaxAsyncScheduler);
end;Performance recommendation (memory manager)
For high-throughput workloads, we recommend using FastMM5 as the process memory manager. EventNexus dispatch paths make heavy use of closures/managed values under async delivery, and FastMM5 usually reduces allocator contention and latency jitter versus the default RTL manager.
- FastMM5 project: https://github.com/pleriche/FastMM5
- Integration: add
FastMM5as the first unit in the.dpruseslist. - Licensing: FastMM5 supports both GPL and commercial licensing.
Metrics
Install callback + throttle interval:
maxSetMetricSampleInterval(250);
maxSetMetricCallback(
procedure(const aName: string; const aStats: TmaxTopicStats)
begin
// lightweight snapshot
end);Dispatch tracing
maxSetDispatchTraceemitsTraceEnqueue,TraceInvokeStart,TraceInvokeEnd, andTraceInvokeError.TraceEnqueueis a topic-queue signal, not a per-subscriber delivery report. ItsDeliveryvalue reflects topic-level queue semantics and is currently emitted asPosting.TraceInvoke*events are per-invocation and carry the subscriber delivery mode actually used for that handler.
Tests (DUnitX)
- Build tests:
./build-tests.sh - Build + run tests:
./build-and-run-tests.sh - Stress runner:
./run-stress.sh - Binary:
tests/MaxEventNexusTests.exe - Coverage depth (current suite): the DUnitX compatibility fixture executes the active published-method regression suite from
tests/src, including dedicated scheduler-contract coverage. - Default verification also runs a lightweight
bench/SchedulerComparesmoke profile plus a framework-only async smoke, and validates the documented benchmark CSV contract. - Diagnostics policy gate: build scripts enforce
build/diagnostics-policy.regexand fail on untriaged warnings/hints. - API coverage proxy:
./build/report-api-test-coverage.sh --enforce-target(target inbuild/api-test-coverage-target.txt, report inbuild/analysis/test-api-coverage.md).
The test runner is DUnitX-based and executes the active fixture suite from tests/src. DUnitX reports one top-level fixture because it hosts the legacy published-method suite runner.
Docs
spec.mdDESIGN.mdMIGRATION.mdsamples/readme.mdtests/readme.md

