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.
-
input
Relevant state changes
Time, cell, combat, target, quest, or settings state changes.
-
boundary
Mark or request resolution
Invalidate the derived answer at the boundary that owns it.
-
component
Resolution work
Ask the generic selection mechanism for the best eligible playlist.
-
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:
bfe28c01fixed journal updates that changed source data without dirtying derived resolver state;fbe9f127restored 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.