Cod3xDocs
guide

Design Genealogy: Event-Driven S3maphore Resolution

How S3maphore stopped polling everything, and what the resulting invalidation bugs taught us.

The old shape

Playlist truth can change when the hour changes, the player changes cell, combat changes, a target dies, a quest changes, or a relevant setting changes.

The blunt solution is to inspect everything from onUpdate. It is easy to understand, but it asks the resolver to keep checking facts that have not changed and makes the update loop responsible for discovering every kind of change.

The reduction

Ask a narrower question:

Which events can change the inputs to playlist eligibility?

Commit dcf5bf21 moved S3maphore toward event-driven resolution. Relevant state changes request resolution; ordinary updates handle the work that genuinely needs time, such as movement-state sampling and playback sequencing.

The current core.lua makes the split visible. realResolvePlaylist asks the generic selection mechanism for the best eligible playlist. Event handlers mark or request the work when their inputs change. The playlist state and event reference describe the public side.

The event-driven resolution shape

Events announce that an eligibility input may have changed; the resolver does work when it is needed instead of rediscovering every fact on every update.

  1. input

    Relevant state changes

    Time, cell, combat, target, quest, or settings state changes.

  2. boundary

    Mark or request resolution

    Invalidate the derived answer at the boundary that owns it.

  3. component

    Resolution work

    Ask the generic selection mechanism for the best eligible playlist.

  4. result

    Playback sequencing

    Updates that genuinely need time continue through the normal update path.

How the pieces relate
  • Relevant state changes Mark or request resolution
  • Mark or request resolution Resolution work
  • Resolution work Playback sequencing
  • Change
  • Invalidation
  • Resolution
  • Playback

This is not “events are always faster.” It is “do not perform a query on every update when you can identify the boundaries at which its answer may change.”

The scars

Event-driven systems move responsibility from repeated polling into invalidation. Missing one invalidation is now a correctness bug.

S3maphore paid for that twice:

  • bfe28c01 fixed journal updates that changed source data without dirtying derived resolver state;
  • fbe9f127 restored silence-chance handling that ordinary playback still needed after the event-driven overhaul.

Those are not arguments for returning to onUpdate polling. They are the price of having explicit ownership of truth changes. The resolver invalidation scar and side-effect regression show the failure modes in detail.

What to steal

First identify the state that can change and the events that announce those changes. Then make invalidation explicit and test each input boundary.

Do not migrate to event-driven work merely to avoid an onUpdate callback. If you cannot name the invalidation events, you are not ready to remove the polling yet.

This study pairs with S3maphore Playlist Eligibility: the predicate keeps the resolver generic, while invalidation tells it when asking the predicate is necessary.

When not to use this

If the truth changes continuously and there is no trustworthy event or coarse invalidation boundary, event-driven work can become a fragile imitation of polling. Keep a bounded sampler when sampling is the actual contract, and measure before replacing it.