{"record":{"id":"070f2871520e27e6","repo":"wavetermdev/waveterm","slug":"client-is-done-070f28","errorCode":null,"errorMessage":"client is done","messagePattern":"client is done","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tsunami/engine/clientimpl.go","lineNumber":284,"sourceCode":"\n\tch := make(chan ssEvent, 100)\n\tc.SSEChannels[connectionId] = ch\n\treturn ch\n}\n\nfunc (c *ClientImpl) UnregisterSSEChannel(connectionId string) {\n\tc.SSEChannelsLock.Lock()\n\tdefer c.SSEChannelsLock.Unlock()\n\n\tif ch, exists := c.SSEChannels[connectionId]; exists {\n\t\tclose(ch)\n\t\tdelete(c.SSEChannels, connectionId)\n\t}\n}\n\nfunc (c *ClientImpl) SendSSEvent(event ssEvent) error {\n\tif c.GetIsDone() {\n\t\treturn fmt.Errorf(\"client is done\")\n\t}\n\n\tc.SSEChannelsLock.Lock()\n\tdefer c.SSEChannelsLock.Unlock()\n\n\t// Send to all registered SSE channels\n\tfor _, ch := range c.SSEChannels {\n\t\tselect {\n\t\tcase ch <- event:\n\t\t\t// Successfully sent\n\t\tdefault:\n\t\t\t// silently drop (below is just for debugging).  this wont happen in general\n\t\t\t// log.Printf(\"SSEvent channel is full for connection %s, skipping event\", connectionId)\n\t\t}\n\t}\n\n\treturn nil\n}","sourceCodeStart":266,"sourceCodeEnd":302,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/tsunami/engine/clientimpl.go#L266-L302","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"// before\nerr := client.SendTermWrite(refId, data)\nif err != nil { log.Fatal(err) }\n// after\nif client.GetIsDone() { return } // client torn down; skip write\nif err := client.SendTermWrite(refId, data); err != nil {\n    log.Printf(\"dropped termwrite after done: %v\", err)\n}","handlingStrategy":"try-catch","validationCode":"if client.GetIsDone() {\n    // skip send; client lifecycle has ended\n}","typeGuard":"func isClientLive(c *engine.ClientImpl) bool { return c != nil && !c.GetIsDone() }","tryCatchPattern":"if err := client.SendTermWrite(refId, data); err != nil {\n    if client.GetIsDone() {\n        log.Printf(\"client done, dropping event: %v\", err)\n        return nil // expected during shutdown, not fatal\n    }\n    return err\n}","preventionTips":["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"],"tags":["go","lifecycle","sse","client-shutdown"],"backgroundTag":"client-already-closed","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}