wavetermdev/waveterm · error
client is done
Error message
client is done
What it means
SendSSEvent broadcasts a server-sent event to all registered SSE channels of a ClientImpl. Before doing so it checks c.GetIsDone(); if the client has been marked done (shut down / disconnected), the event is refused with "client is done". The library throws this to prevent sending events through a client whose lifecycle has ended. It applies to SendSSEvent directly and to callers like SendAsyncInitiation, SendTermWrite, and ShowModal.
Source
Thrown at tsunami/engine/clientimpl.go:284
ch := make(chan ssEvent, 100)
c.SSEChannels[connectionId] = ch
return ch
}
func (c *ClientImpl) UnregisterSSEChannel(connectionId string) {
c.SSEChannelsLock.Lock()
defer c.SSEChannelsLock.Unlock()
if ch, exists := c.SSEChannels[connectionId]; exists {
close(ch)
delete(c.SSEChannels, connectionId)
}
}
func (c *ClientImpl) SendSSEvent(event ssEvent) error {
if c.GetIsDone() {
return fmt.Errorf("client is done")
}
c.SSEChannelsLock.Lock()
defer c.SSEChannelsLock.Unlock()
// Send to all registered SSE channels
for _, ch := range c.SSEChannels {
select {
case ch <- event:
// Successfully sent
default:
// silently drop (below is just for debugging). this wont happen in general
// log.Printf("SSEvent channel is full for connection %s, skipping event", connectionId)
}
}
return nil
}View on GitHub (pinned to a4447c1563)
Solutions
- Check c.GetIsDone() before pushing events and drop/stop the work if true
- Ensure the client lifecycle owner signals done only after all pending SendTermWrite/ShowModal calls complete
- If reconnects happen, re-look-up the current live client instead of caching an old ClientImpl reference
- Handle the returned error gracefully — the event is lost; do not retry on the done client
Example fix
// before
err := client.SendTermWrite(refId, data)
if err != nil { log.Fatal(err) }
// after
if client.GetIsDone() { return } // client torn down; skip write
if err := client.SendTermWrite(refId, data); err != nil {
log.Printf("dropped termwrite after done: %v", err)
} Defensive patterns
Strategy: try-catch
Validate before calling
if client.GetIsDone() {
// skip send; client lifecycle has ended
} Type guard
func isClientLive(c *engine.ClientImpl) bool { return c != nil && !c.GetIsDone() } Try / catch
if err := client.SendTermWrite(refId, data); err != nil {
if client.GetIsDone() {
log.Printf("client done, dropping event: %v", err)
return nil // expected during shutdown, not fatal
}
return err
} Prevention
- Check GetIsDone() before every send on a long-lived client reference
- Never cache ClientImpl across reconnects; re-resolve the current client
- Stop background writers when the client is marked done (channel/goroutine shutdown)
- Treat this error as a lifecycle signal, not a transport failure — do not retry on the same client
When it happens
Trigger: Calling SendTermWrite, SendAsyncInitiation, ShowModal, or SendSSEvent after the client has been marked done (connection closed, client shutdown, or client.IsDone set). Race where the client finishes while another goroutine is still pushing events.
Common situations: Writing to a terminal block after the frontend tab/block was closed; firing async-initiation events after the client teardown goroutine ran; background workers holding a stale client reference after reconnect (new ClientImpl created but old one still used).
Related errors
- client %q not found
- Layout model not found
- Display container not found
- sse handler is nil
- sse handler is nil
AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01).
Data as JSON: /api/errors/070f2871520e27e6.
Report an issue: GitHub.