Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.

Commit 918206e

Browse files
author
Guilherme Souza
authored
fix: leavePush timer leaked (#483)
1 parent 723652f commit 918206e

File tree

2 files changed

+72
-9
lines changed

2 files changed

+72
-9
lines changed

src/RealtimeChannel.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ export default class RealtimeChannel {
416416
): RealtimeChannel
417417
on(
418418
type: `${REALTIME_LISTEN_TYPES}`,
419-
filter: { event: string; [key: string]: string },
419+
filter: { event: string;[key: string]: string },
420420
callback: (payload: any) => void
421421
): RealtimeChannel {
422422
return this._on(type, filter, callback)
@@ -516,8 +516,10 @@ export default class RealtimeChannel {
516516

517517
this.joinPush.destroy()
518518

519-
return new Promise((resolve) => {
520-
const leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout)
519+
let leavePush: Push | null = null
520+
521+
return new Promise<RealtimeChannelSendResponse>((resolve) => {
522+
leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout)
521523
leavePush
522524
.receive('ok', () => {
523525
onClose()
@@ -536,6 +538,9 @@ export default class RealtimeChannel {
536538
leavePush.trigger('ok', {})
537539
}
538540
})
541+
.finally(() => {
542+
leavePush?.destroy()
543+
})
539544
}
540545
/**
541546
* Teardown the channel.
@@ -646,7 +651,7 @@ export default class RealtimeChannel {
646651
payload.ids?.includes(bindId) &&
647652
(bindEvent === '*' ||
648653
bindEvent?.toLocaleLowerCase() ===
649-
payload.data?.type.toLocaleLowerCase())
654+
payload.data?.type.toLocaleLowerCase())
650655
)
651656
} else {
652657
const bindEvent = bind?.filter?.event?.toLocaleLowerCase()

test/channel.test.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ describe('subscribe', () => {
204204
sinon.stub(socket, '_makeRef').callsFake(() => defaultRef)
205205
const spy = sinon.spy(socket, 'push')
206206
const cbSpy = sinon.spy()
207-
const func = () => {}
207+
const func = () => { }
208208

209209
channel.bindings.postgres_changes = [
210210
{
@@ -306,7 +306,7 @@ describe('subscribe', () => {
306306
test('unsubscribes to channel with incorrect server postgres_changes resp', () => {
307307
const unsubscribeSpy = sinon.spy(channel, 'unsubscribe')
308308
const callbackSpy = sinon.spy()
309-
const dummyCallback = () => {}
309+
const dummyCallback = () => { }
310310

311311
channel.bindings.postgres_changes = [
312312
{
@@ -357,7 +357,7 @@ describe('subscribe', () => {
357357

358358
assert.equal(joinPush.timeout, defaultTimeout)
359359

360-
channel.subscribe(() => {}, newTimeout)
360+
channel.subscribe(() => { }, newTimeout)
361361

362362
assert.equal(joinPush.timeout, newTimeout)
363363
})
@@ -500,7 +500,7 @@ describe('joinPush', () => {
500500
})
501501

502502
test("sends and empties channel's buffered pushEvents", () => {
503-
const pushEvent: any = { send() {} }
503+
const pushEvent: any = { send() { } }
504504
const spy = sinon.spy(pushEvent, 'send')
505505
channel.pushBuffer.push(pushEvent)
506506
helpers.receiveOk()
@@ -654,7 +654,7 @@ describe('joinPush', () => {
654654

655655
test("does not trigger channel's buffered pushEvents", () => {
656656
// @ts-ignore - we're only testing the pushBuffer
657-
const pushEvent: Push = { send: () => {} }
657+
const pushEvent: Push = { send: () => { } }
658658
const spy = sinon.spy(pushEvent, 'send')
659659

660660
channel.pushBuffer.push(pushEvent)
@@ -1438,3 +1438,61 @@ describe('worker', () => {
14381438
assert.equal(client.workerUrl, 'https://realtime.supabase.com/worker.js')
14391439
})
14401440
})
1441+
1442+
describe('unsubscribe', () => {
1443+
let destroySpy: sinon.SinonSpy
1444+
1445+
beforeEach(() => {
1446+
channel = socket.channel('topic')
1447+
channel.subscribe()
1448+
destroySpy = sinon.spy(Push.prototype, 'destroy')
1449+
})
1450+
1451+
afterEach(() => {
1452+
destroySpy.restore()
1453+
})
1454+
1455+
test('cleans up leavePush on successful unsubscribe', async () => {
1456+
await channel.unsubscribe()
1457+
1458+
assert.ok(destroySpy.calledTwice) // Once for joinPush, once for leavePush
1459+
assert.equal(channel.state, CHANNEL_STATES.closed)
1460+
})
1461+
1462+
test('cleans up leavePush on timeout', async () => {
1463+
sinon.stub(socket, 'push').callsFake(() => {
1464+
// Simulate timeout by not responding
1465+
clock.tick(defaultTimeout + 1)
1466+
})
1467+
1468+
const result = await channel.unsubscribe()
1469+
1470+
assert.ok(destroySpy.calledTwice) // Once for joinPush, once for leavePush
1471+
assert.equal(result, 'timed out')
1472+
assert.equal(channel.state, CHANNEL_STATES.closed)
1473+
})
1474+
1475+
// TODO: Fix this test
1476+
// test('cleans up leavePush on error', async () => {
1477+
// sinon.stub(socket, 'push').callsFake(() => {
1478+
// // Simulate error by triggering error response
1479+
// const leavePush = channel['joinPush']
1480+
// leavePush.trigger('error', {})
1481+
// })
1482+
1483+
// const result = await channel.unsubscribe()
1484+
1485+
// assert.ok(destroySpy.calledTwice) // Once for joinPush, once for leavePush
1486+
// assert.equal(result, 'error')
1487+
// assert.equal(channel.state, CHANNEL_STATES.closed)
1488+
// })
1489+
1490+
test('cleans up leavePush even if socket is not connected', async () => {
1491+
sinon.stub(socket, 'isConnected').returns(false)
1492+
1493+
await channel.unsubscribe()
1494+
1495+
assert.ok(destroySpy.calledTwice) // Once for joinPush, once for leavePush
1496+
assert.equal(channel.state, CHANNEL_STATES.closed)
1497+
})
1498+
})

0 commit comments

Comments
 (0)