diff --git a/src/discord-client.js b/src/discord-client.js index 639c913c..3e860094 100644 --- a/src/discord-client.js +++ b/src/discord-client.js @@ -60,16 +60,17 @@ export function createDiscordClient({ transport, retryDelayMs = 5_000, maxRetryD } if (!connected) { if (elapsedNow() < retryAt) { if (activity === null && clearPending) retryClear(); return false; } - try { await transport.connect(); connected = true; } - catch { const result = failed("DISCORD_NOT_RUNNING"); if (activity === null && clearPending) retryClear(); return result; } + try { await transport.connect(); if (closed) return false; connected = true; } + catch { if (closed) return false; const result = failed("DISCORD_NOT_RUNNING"); if (activity === null && clearPending) retryClear(); return result; } } try { if (activity === null) await transport.clearActivity(); else await transport.setActivity(activity); + if (closed) return false; lastKey = key; lastPublishElapsed = elapsedNow(); failures = 0; lastError = null; if (activity === null) cancelClear(); lastPublishedAt = new Date(now()).toISOString(); return true; - } catch { const result = failed("DISCORD_PUBLISH_FAILED"); if (activity === null && clearPending) retryClear(); return result; } + } catch { if (closed) return false; const result = failed("DISCORD_PUBLISH_FAILED"); if (activity === null && clearPending) retryClear(); return result; } } async function close() { cancelClear(); closed = true; connected = false; lastKey = undefined; if (typeof transport.close === "function") await transport.close(); } diff --git a/test/discord-client-close-race.test.js b/test/discord-client-close-race.test.js new file mode 100644 index 00000000..546720df --- /dev/null +++ b/test/discord-client-close-race.test.js @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createDiscordClient} from '../src/discord-client.js'; +for(const activity of [null,{details:'fixture'}])for(const succeeds of [true,false]){ + test(`close wins pending connect (${activity===null?'clear':'publish'}, ${succeeds?'resolve':'reject'})`,async()=>{ + let release,reject,writes=0; + const transport={connect:()=>new Promise((yes,no)=>{release=yes;reject=no;}),clearActivity:async()=>writes++,setActivity:async()=>writes++,close:async()=>{}}; + const client=createDiscordClient({transport,minUpdateIntervalMs:0}); + const pending=client.publish(activity);await client.close();const closed=client.status(); + if(succeeds)release();else reject(new Error('fixture')); + assert.equal(await pending,false);assert.equal(writes,0);assert.equal(client.connected,false);assert.deepEqual(client.status(),closed); + }); +} +for(const activity of [null,{details:'fixture'}])for(const succeeds of [true,false]){ + test(`close wins pending write (${activity===null?'clear':'publish'}, ${succeeds?'resolve':'reject'})`,async()=>{ + let release,reject,entered; + const gate=new Promise(r=>entered=r); + const write=()=>{entered();return new Promise((yes,no)=>{release=yes;reject=no;});}; + const transport={connect:async()=>{},clearActivity:write,setActivity:write,close:async()=>{}}; + const client=createDiscordClient({transport,minUpdateIntervalMs:0}); + const pending=client.publish(activity);await gate;await client.close();const closed=client.status(); + if(succeeds)release();else reject(new Error('fixture')); + assert.equal(await pending,false);assert.equal(client.connected,false);assert.deepEqual(client.status(),closed); + }); +}