From 69235cac5b3da5ff0dc20310e7474cabd8e63ba3 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 15 Sep 2026 20:14:02 -0400 Subject: feat: route all html editor API mutations through single owner task --- internal/game/owner.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'internal/game/owner.go') diff --git a/internal/game/owner.go b/internal/game/owner.go index 2fb4777..4a3646e 100644 --- a/internal/game/owner.go +++ b/internal/game/owner.go @@ -122,6 +122,37 @@ func (g *Game) run(fn func()) { <-task } +// RunAdminOnOwner synchronously executes one complete editor request. Unlike +// RunOnOwner it rejects work during shutdown and NEVER falls back to executing +// on the HTTP goroutine when the owner has stopped. Call only from outside the +// owner; runtime callbacks within fn must call owner-only methods directly. +func (g *Game) RunAdminOnOwner(fn func()) bool { + if g.isShuttingDown() { + return false + } + accepted := false + task := func() { + if !g.isShuttingDown() { + fn() + accepted = true + } + } + if g.owner == nil { + // Synthetic Games only; production always has an owner. + task() + return accepted + } + done := make(chan struct{}) + if !g.owner.enqueue(func() { + task() + close(done) + }) { + return false + } + <-done + return accepted +} + // tryRun executes fn on the owner if it is currently idle-draining; it is // not currently used for correctness paths but exists for future non-blocking // administrative reads. It returns false when the queue is full. -- cgit v1.2.3