Deferred Event Queue

Queue events now and publish them later, in order, at a flush point you choose.

Queue events in risky spots and publish the whole batch, in order, at a safe point you choose.

InterfaceIDeferredBus
Off switchNullDeferredBus
AssemblyCommonGameSystem.Core
StartupRegistered automatically at boot (one of the 23 services) — no setup needed

What it does

The Deferred Event Queue is a thin layer over the Event Bus (IEventBus). Instead of publishing an event immediately, you Enqueue it. The queue holds everything until you call Flush(), which publishes the whole batch through the Event Bus — in the exact order the events were queued, across all event types.

Use it to move event publication out of a risky spot — a physics callback, a loop over a collection you are also changing, the middle of input processing — to a safe point you choose. The queue owns only the holding and the ordering; routing and subscriber handling stay with the Event Bus. There is no automatic flush: you decide when the batch fires. Main thread only.

Quick start

Define an event as a plain class:

public sealed class EnemyDiedEvent
{
    public int EnemyId { get; }
    public EnemyDiedEvent(int enemyId) { EnemyId = enemyId; }
}

Then queue now, publish later:

using CommonGameSystem.Core;
using UnityEngine;

public class MyDeferredPublisher : MonoBehaviour
{
    private IDeferredBus _bus;

    private void Awake() => _bus = ServiceLocator.Resolve<IDeferredBus>();

    public void OnEnemyDied(int enemyId)
    {
        _bus.Enqueue(new EnemyDiedEvent(enemyId));   // queued — NOT published yet
    }

    private void LateUpdate()
    {
        _bus.Flush();   // publishes the batch through the Event Bus, in queue order
    }
}

Bootstrap registers this service automatically at startup. Resolve and cache it once in Awake(), as shown.

API reference

IDeferredBus

MemberNotes
void Enqueue<TEvent>(TEvent evt) where TEvent : classQueue evt now; it is not published until Flush(). A null event throws ArgumentNullException immediately, so the stack trace points at the caller. The class constraint matches the Event Bus exactly — struct events are not supported. Safe under IL2CPP/AOT builds; nothing is boxed.
void Flush()Publish the queued batch in queue order, across every event type, one Event Bus Publish per event. Events enqueued while a flush is running go into the next batch. Calling Flush from inside a running flush does nothing.
int PendingCount { get; }Number of events queued but not yet flushed. Diagnostic.

DeferredBusOptions — construction-time readonly struct

MemberNotes
int MaxDrainPerFlushCap on events published per Flush. 0 (the default) means unlimited. A non-zero value (clamped to 1–1,000,000) publishes that many, carries the rest to the front of the next batch, and logs one throttled warning.
int InitialQueueCapacityStarting size of the internal buffers. Default 16; clamped to 0–4096.
bool LogLifecycleWhen true, Editor builds log queue/flush traces; the traces are stripped from release builds. Default false.
DeferredBusOptions(int maxDrainPerFlush, int initialQueueCapacity, bool logLifecycle = false)Explicit constructor; the numeric ranges are clamped at construction.
static DeferredBusOptions Default0 / 16 / false. Note this differs from default(DeferredBusOptions), which is 0 / 0 / false.
DeferredBusOptions With(int? maxDrainPerFlush = null, int? initialQueueCapacity = null, bool? logLifecycle = null)An immutable copy with named overrides.

Examples

using CommonGameSystem.Core;
using UnityEngine;

public sealed class DamageDealtEvent
{
    public int EnemyId { get; }
    public int Amount { get; }
    public DamageDealtEvent(int enemyId, int amount) { EnemyId = enemyId; Amount = amount; }
}

public class DamageResolver : MonoBehaviour
{
    private IDeferredBus _deferred;

    private void Awake() => _deferred = ServiceLocator.Resolve<IDeferredBus>();

    // Called from inside a physics callback — publishing immediately here could
    // re-enter the collection we are iterating. Queue it instead.
    public void OnHit(int enemyId, int amount)
    {
        _deferred.Enqueue(new DamageDealtEvent(enemyId, amount));
    }

    // Publish at a known-safe point: the end of the fixed-update step.
    private void FixedUpdate()
    {
        if (_deferred.PendingCount > 0)
            _deferred.Flush();   // every queued event publishes here, in queue order
    }
}

Turning it off

ServiceLocator.Replace<IDeferredBus>(new NullDeferredBus());

This mutes deferral entirely. Enqueue silently drops the event (nothing is rerouted to a real bus), Flush does nothing, and PendingCount stays 0. A game wired against a real deferred queue keeps running with deferral switched off. Unlike the Event Bus's silent NullEventBus, this replacement logs a single warning when constructed. A silently dropped queue ("events seem to queue but never arrive") is hard to diagnose, so an accidental swap is made visible. A null event still throws, exactly like the real service.

Common pitfalls

  • Nothing fires until you Flush. There is no automatic flush. If you queue and never flush, the events never publish and PendingCount grows without bound. Pick one predictable drain point — end of FixedUpdate, end of frame — and always flush there.
  • Events queued during a flush wait for the next one. The batch is fixed the moment Flush begins. Anything a subscriber enqueues while the batch is publishing is held for the following Flush. This is deliberate: it keeps every single flush bounded.
  • MaxDrainPerFlush splits the batch — it never drops events. When the cap is hit, the leftover events move to the front of the next batch, so they still publish before anything queued later. Order is preserved, and one throttled warning is logged. It is a pressure valve, not a discard.
  • A flush never throws, and one bad event cannot stop the rest. If a Publish call throws, the queue catches it, logs it, and continues with the remaining events. The framework's own Event Bus already isolates faulty subscribers itself; this guard covers replacement buses that do not.
  • Event Bus — the routing layer this queue publishes through
  • Scheduler — run a callback later at a chosen time, instead of at a flush point
  • Logger — where flush faults and lifecycle traces go