feat: three-tier context lifecycle (grace → suspended → reaped)
All checks were successful
CI / Build and Test (push) Successful in 1m22s

Contexts that lose their SSE connection now pass through a suspended
state before being fully reaped. Suspended contexts keep their shell
(ID, route, CSRF token) but free page resources. On reconnect, the
page init function is re-run for a seamless resume. Contexts past
the TTL trigger a client-side reload instead of a silent dead page.

Configurable via ContextSuspendAfter (default 15m) and ContextTTL
(default 1h).
This commit is contained in:
Ryan Hamamura
2026-02-13 15:22:08 -10:00
parent 11c6354da0
commit 539a2ad504
4 changed files with 109 additions and 19 deletions

View File

@@ -42,6 +42,7 @@ type Context struct {
createdAt time.Time
sseConnected atomic.Bool
sseDisconnectedAt atomic.Pointer[time.Time]
suspended atomic.Bool
}
// View defines the UI rendered by this context.
@@ -400,6 +401,13 @@ func (c *Context) resetPageState() {
c.mu.Unlock()
}
// suspend frees page-scoped resources while keeping the context shell alive
// in the registry for seamless re-init on reconnect.
func (c *Context) suspend() {
c.resetPageState()
c.suspended.Store(true)
}
// Navigate performs an SPA navigation to the given path. It resets page state,
// runs the target page's init function (with middleware), and pushes the new
// view over the existing SSE connection with a view transition animation.