{"record":{"id":"3e9184a48f8e2fc7","repo":"block/buzz","slug":"relay-publish-was-superseded-by-a-session-change","errorCode":null,"errorMessage":"Relay publish was superseded by a session change.","messagePattern":"Relay publish was superseded by a session change\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"desktop/src/shared/api/relayClientSession.ts","lineNumber":656,"sourceCode":"  }\n\n  private async sendRaw(payload: unknown[]) {\n    if (this.wsId === null) {\n      throw new Error(\"Relay socket is not connected.\");\n    }\n\n    await invoke(\"plugin:websocket|send\", {\n      id: this.wsId,\n      message: {\n        type: \"Text\",\n        data: JSON.stringify(payload),\n      },\n    });\n  }\n\n  private async sendRawForGeneration(payload: unknown[], generation: number) {\n    if (generation !== this.connectionGeneration || this.wsId === null) {\n      throw new Error(\"Relay publish was superseded by a session change.\");\n    }\n    const wsId = this.wsId;\n    await invoke(\"plugin:websocket|send\", {\n      id: wsId,\n      message: { type: \"Text\", data: JSON.stringify(payload) },\n    });\n  }\n\n  private normalizeRelayError(error: unknown, fallbackMessage: string) {\n    return error instanceof Error ? error : new Error(fallbackMessage);\n  }\n\n  private recoverFromSocketFailure(\n    error: unknown,\n    fallbackMessage: string,\n  ): Error {\n    const normalizedError = this.normalizeRelayError(error, fallbackMessage);\n    this.resetConnection(normalizedError);","sourceCodeStart":638,"sourceCodeEnd":674,"githubUrl":"https://github.com/block/buzz/blob/6c35e82bd50f4ad6587554eeb429e7378d474ba7/desktop/src/shared/api/relayClientSession.ts#L638-L674","documentation":"relayClientSession compares the caller's connection generation with the session's current generation right before sending raw JSON over the Tauri websocket plugin. If the connection was re-established (generation bumped) or wsId became null, the payload would go over a stale socket, so it throws. This is a stale-socket fence for publishes.","triggerScenarios":"sendRawForGeneration invoked with a generation captured before a reconnect/community switch completed; concurrent publish racing a session teardown that nulled wsId.","commonSituations":"Community switch while a publish is in flight; automatic reconnect after network blip; publish queued during rate-limit wait then sent on a replaced socket.","solutions":["Catch the error and re-publish using a freshly obtained generation (e.g. via session.reconnect()).","Avoid holding publishes across community switches; cancel or re-dispatch them after the switch.","Serialize publishes through the session so each uses its current generation.","Surface a retry affordance to the user for failed outbound messages."],"exampleFix":"// before\ntry { await send(payload, oldGeneration); } catch {}\n// after\ntry { await send(payload, oldGeneration); }\ncatch (e) {\n  if (isSuperseded(e)) { const gen = await session.reconnect(); await send(payload, gen); }\n  else throw e;\n}","handlingStrategy":"retry","validationCode":"// verify session generation freshness before send\nif (generation !== session.currentGeneration()) await session.reconnect();\n","typeGuard":"function isSupersededError(e: unknown): boolean { return e instanceof Error && e.message.includes('superseded by a session change'); }\n","tryCatchPattern":"try { await publishEvent(ev); } catch (e) { if (isSupersededError(e)) await publishEvent(ev); else throw e; }\n","preventionTips":["Capture generation as late as possible before sending","Route all reconnects through the session object","Cancel in-flight publishes during community switches"],"tags":["websocket","relay","race-condition","tauri"],"backgroundTag":"stale-connection-generation","analyzedSha":"6c35e82bd50f4ad6587554eeb429e7378d474ba7","analyzedAt":"2026-09-05T18:13:50.666Z","contentChangedAt":"2026-09-05T18:13:50.666Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}