From e6c3a02a3f33183fc09f93728b51e7ed9f11790b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Uriarte?= Date: Sun, 22 Mar 2026 23:56:34 -0500 Subject: [PATCH] fix: ensure WebSocket notifications are sent before application quits --- src/plugins/api-server/backend/main.ts | 9 ++++++++- .../api-server/backend/routes/index.ts | 2 +- .../api-server/backend/routes/websocket.ts | 20 +++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/plugins/api-server/backend/main.ts b/src/plugins/api-server/backend/main.ts index e81510e278..e287370b8b 100644 --- a/src/plugins/api-server/backend/main.ts +++ b/src/plugins/api-server/backend/main.ts @@ -13,7 +13,7 @@ import { registerCallback } from '@/providers/song-info'; import { createBackend } from '@/utils'; import { JWTPayloadSchema } from './scheme'; -import { registerAuth, registerControl, registerWebsocket } from './routes'; +import { registerAuth, registerControl, registerWebsocket, wsNotifyClose } from './routes'; import { APPLICATION_NAME } from '@/i18n'; @@ -28,8 +28,14 @@ import type { export const backend = createBackend({ async start(ctx) { + const { app } = await import('electron'); const config = await ctx.getConfig(); + // Notify WebSocket clients that playback has stopped before quitting + app.on('before-quit', () => { + wsNotifyClose?.(); + }); + this.init(ctx); registerCallback((songInfo) => { this.songInfo = songInfo; @@ -197,6 +203,7 @@ export const backend = createBackend({ } }, end() { + wsNotifyClose?.(); this.server?.close(); this.server = undefined; }, diff --git a/src/plugins/api-server/backend/routes/index.ts b/src/plugins/api-server/backend/routes/index.ts index a2467098e8..1c4370594b 100644 --- a/src/plugins/api-server/backend/routes/index.ts +++ b/src/plugins/api-server/backend/routes/index.ts @@ -1,3 +1,3 @@ export { register as registerControl } from './control'; export { register as registerAuth } from './auth'; -export { register as registerWebsocket } from './websocket'; +export { register as registerWebsocket, wsNotifyClose } from './websocket'; diff --git a/src/plugins/api-server/backend/routes/websocket.ts b/src/plugins/api-server/backend/routes/websocket.ts index 1c1278e305..416700cf36 100644 --- a/src/plugins/api-server/backend/routes/websocket.ts +++ b/src/plugins/api-server/backend/routes/websocket.ts @@ -55,6 +55,18 @@ export const register = ( ); }; + // Notify all WebSocket clients that playback has stopped (e.g. app closing/hiding) + // We intentionally do NOT close or clear sockets here so that clients like + // Boring Notch keep their connection alive and can receive updates when + // Pear Desktop comes back from hiding. + wsNotifyClose = () => { + console.log(`[API Server WebSocket] NotifyClose called. Sending isPlaying:false to ${sockets.size} clients.`); + send(DataTypes.PlayerStateChanged, { + isPlaying: false, + position: lastSongInfo?.elapsedSeconds ?? 0, + }); + }; + const createPlayerState = ({ songInfo, volumeState, @@ -131,7 +143,7 @@ export const register = ( upgradeWebSocket(() => ({ onOpen(_, ws) { // "Unsafe argument of type `WSContext` assigned to a parameter of type `WSContext`. (@typescript-eslint/no-unsafe-argument)" ????? what? - sockets.add(ws as WSContext); + sockets.add(ws as unknown as WSContext); ws.send( JSON.stringify({ @@ -147,8 +159,12 @@ export const register = ( }, onClose(_, ws) { - sockets.delete(ws as WSContext); + sockets.delete(ws as unknown as WSContext); }, })) as (ctx: Context, next: Next) => Promise, ); }; + +// Exposed so the backend can call it on app close/hide +export let wsNotifyClose: (() => void) | undefined; +