fix: preserve context across SSE reconnects on tab visibility change
Datastar aborts SSE on visibilitychange (tab hidden) and reconnects when visible. The previous cleanup-on-disconnect destroyed the context before the client could reconnect. Now SSE disconnect does a soft teardown (mark disconnected, keep context alive) and reconnect drains stale patches before resuming. The reaper uses disconnect time instead of creation time so recently-disconnected contexts aren't prematurely reaped.
This commit is contained in:
@@ -39,8 +39,9 @@ type Context struct {
|
|||||||
subscriptions []Subscription
|
subscriptions []Subscription
|
||||||
subsMu sync.Mutex
|
subsMu sync.Mutex
|
||||||
disposeOnce sync.Once
|
disposeOnce sync.Once
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
sseConnected atomic.Bool
|
sseConnected atomic.Bool
|
||||||
|
sseDisconnectedAt atomic.Pointer[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
// View defines the UI rendered by this context.
|
// View defines the UI rendered by this context.
|
||||||
|
|||||||
31
via.go
31
via.go
@@ -318,8 +318,19 @@ func (v *V) reapOrphanedContexts(ttl time.Duration) {
|
|||||||
v.contextRegistryMutex.RLock()
|
v.contextRegistryMutex.RLock()
|
||||||
var orphans []*Context
|
var orphans []*Context
|
||||||
for _, c := range v.contextRegistry {
|
for _, c := range v.contextRegistry {
|
||||||
if !c.sseConnected.Load() && now.Sub(c.createdAt) > ttl {
|
if c.sseConnected.Load() {
|
||||||
orphans = append(orphans, c)
|
continue
|
||||||
|
}
|
||||||
|
if dc := c.sseDisconnectedAt.Load(); dc != nil {
|
||||||
|
// SSE was connected then dropped — reap if gone too long
|
||||||
|
if now.Sub(*dc) > ttl {
|
||||||
|
orphans = append(orphans, c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SSE never connected — reap based on creation time
|
||||||
|
if now.Sub(c.createdAt) > ttl {
|
||||||
|
orphans = append(orphans, c)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v.contextRegistryMutex.RUnlock()
|
v.contextRegistryMutex.RUnlock()
|
||||||
@@ -624,7 +635,19 @@ func New() *V {
|
|||||||
// use last-event-id to tell if request is a sse reconnect
|
// use last-event-id to tell if request is a sse reconnect
|
||||||
sse.Send(datastar.EventTypePatchElements, []string{}, datastar.WithSSEEventId("via"))
|
sse.Send(datastar.EventTypePatchElements, []string{}, datastar.WithSSEEventId("via"))
|
||||||
|
|
||||||
|
// Drain stale patches on reconnect so the client gets fresh state
|
||||||
|
if c.sseDisconnectedAt.Load() != nil {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.patchChan:
|
||||||
|
default:
|
||||||
|
goto drained
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drained:
|
||||||
|
}
|
||||||
c.sseConnected.Store(true)
|
c.sseConnected.Store(true)
|
||||||
|
c.sseDisconnectedAt.Store(nil)
|
||||||
v.logDebug(c, "SSE connection established")
|
v.logDebug(c, "SSE connection established")
|
||||||
|
|
||||||
go c.Sync()
|
go c.Sync()
|
||||||
@@ -633,7 +656,9 @@ func New() *V {
|
|||||||
select {
|
select {
|
||||||
case <-sse.Context().Done():
|
case <-sse.Context().Done():
|
||||||
v.logDebug(c, "SSE connection ended")
|
v.logDebug(c, "SSE connection ended")
|
||||||
v.cleanupCtx(c)
|
c.sseConnected.Store(false)
|
||||||
|
now := time.Now()
|
||||||
|
c.sseDisconnectedAt.Store(&now)
|
||||||
return
|
return
|
||||||
case <-c.ctxDisposedChan:
|
case <-c.ctxDisposedChan:
|
||||||
v.logDebug(c, "context disposed, closing SSE")
|
v.logDebug(c, "context disposed, closing SSE")
|
||||||
|
|||||||
Reference in New Issue
Block a user