Table of Contents

Incremental Sync

New subscriptions to large Views sync in tick-sized batches instead of one giant first delta.

Status: โœ… Implemented ยท Visibility: Public ยท Level: ๐ŸŸฃ Advanced ยท Category: Subscriptions

๐ŸŽฏ What it solves

When a client subscribes to a View that already holds thousands of entities, sending the whole entity set in a single TickDelta would blow the per-tick output budget and stall every other client behind one large serialization. Incremental sync spreads that initial snapshot across as many ticks as it takes, so a new subscription never causes a latency spike โ€” for everyone else, or for the subscribing client's own steady-state deltas once sync finishes.

โš™๏ธ How it works (in brief)

On subscribe, the runtime snapshots the View's current entity set and walks it forward a fixed number of entities per tick, sending each chunk as a normal Added batch under a Syncing state. No Modified or Removed deltas are sent for that View while sync is in progress โ€” entities that change mid-sync simply pick up their current values whenever their turn in the snapshot comes. Once the last batch is sent, the runtime flips the View to Active for that client and emits a SyncComplete event; normal Added/Modified/Removed deltas resume from the next tick. Small Views that fit in one batch sync and complete in the same tick. The same mechanism is reused for Resync recovery after a backpressure overflow (see Backpressure & Resync Recovery).

๐Ÿ’ป Usage

Incremental sync requires no special call โ€” it activates automatically whenever SetSubscriptions adds a client to a View:

var dungeonNpcs = runtime.PublishView("dungeon_npcs", dungeonNpcsView);
runtime.SetSubscriptions(client, dungeonNpcs);   // sync begins next Output phase

// Client-side: only act on the View once it reports SyncComplete
conn.OnTickDelta += tickDelta =>
{
    foreach (var evt in tickDelta.Events)
    {
        if (evt.Type == EventType.SyncComplete)
        {
            MarkViewReady(evt.ViewId);   // safe to assume the local cache mirrors the server now
        }
    }

    foreach (var viewDelta in tickDelta.Views)
    {
        foreach (var added in viewDelta.Added)
        {
            SpawnOrUpdateClientEntity(added);   // applies to both sync batches and steady-state Added
        }
    }
};
Option (SubscriptionServerOptions) Default Effect
SyncBatchSize 200 Max entities sent per Added batch per client per View per tick while syncing

โš ๏ธ Guarantees & limits

  • No interleaving โ€” a client never receives Modified/Removed deltas for a View while it is Syncing; only Added batches and the terminal SyncComplete event.
  • Consistent terminal state, not consistent snapshot โ€” the snapshot is captured once at subscribe time, but individual entities are read (and their current data sent) lazily as their batch comes up, so an entity changed mid-sync arrives with whatever state it has when its turn comes, not the state at subscribe time.
  • Fixed batch size, not adaptive โ€” SyncBatchSize is a flat per-tick cap; it does not currently scale down automatically under tick overload (see Subscription Cost & Overload Integration). Tune it down if large syncs are pushing OutputPhaseMs over budget.
  • Per-client, per-View state โ€” sync progress is tracked independently for each (client, View) pair; one client mid-sync on a View does not block or slow other clients' deltas on the same View.
  • Entity lifetime during sync is not guarded โ€” the sync snapshot is a fixed list of entity IDs captured at subscribe time; an entity destroyed before its batch comes up is not specially handled, unlike the steady-state delta path which only ever reports entities still present in the View.
  • Memory cost is bounded by subscriber count, not View size growth โ€” the sync snapshot is a flat array of entity IDs (not full component data) held only for the duration of that client's sync, and freed as soon as the last batch ships.

๐Ÿงช Tests

  • ViewDeltaTests โ€” SubscriptionFlow_EntitiesExistBeforeSubscribe_SyncCapturesThem/SubscriptionFlow_RefreshPopulatesView_ThenBeginSync_ThenBuildDelta_NoDoubleAdd: BeginSync snapshot capture and Output-phase ordering (no double-Added)