From 0949fc076ef750af90f7d48fb0a96664b3877911 Mon Sep 17 00:00:00 2001 From: Tanmay Sharma Date: Wed, 3 Dec 2025 06:23:19 +0530 Subject: [PATCH 1/2] fix(realtime): terminate web worker on disconnect to prevent memory leak Web Workers were created in _startWorkerHeartbeat() but never terminated in _teardownConnection(), causing memory accumulation with each disconnect/reconnect cycle. This fix adds a _terminateWorker() private method that properly terminates the Web Worker and clears the reference, then calls it during connection teardown. Fixes #1902 --- .../core/realtime-js/src/RealtimeClient.ts | 13 ++++++++++ .../test/RealtimeClient.worker.test.ts | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/core/realtime-js/src/RealtimeClient.ts b/packages/core/realtime-js/src/RealtimeClient.ts index 19db54fa6..40dc75e98 100755 --- a/packages/core/realtime-js/src/RealtimeClient.ts +++ b/packages/core/realtime-js/src/RealtimeClient.ts @@ -645,6 +645,7 @@ export default class RealtimeClient { this.conn = null } this._clearAllTimers() + this._terminateWorker() this.channels.forEach((channel) => channel.teardown()) } @@ -710,6 +711,18 @@ export default class RealtimeClient { interval: this.heartbeatIntervalMs, }) } + + /** + * Terminate the Web Worker and clear the reference + * @internal + */ + private _terminateWorker(): void { + if (this.workerRef) { + this.log('worker', 'terminating worker') + this.workerRef.terminate() + this.workerRef = undefined + } + } /** @internal */ private _onConnClose(event: any) { this._setConnectionState('disconnected') diff --git a/packages/core/realtime-js/test/RealtimeClient.worker.test.ts b/packages/core/realtime-js/test/RealtimeClient.worker.test.ts index 0c0bd644c..b159326c5 100644 --- a/packages/core/realtime-js/test/RealtimeClient.worker.test.ts +++ b/packages/core/realtime-js/test/RealtimeClient.worker.test.ts @@ -98,3 +98,27 @@ test('creates worker with blob URL when no workerUrl provided', () => { global.URL.createObjectURL = originalCreateObjectURL } }) + +test('terminates worker on disconnect', () => { + // Establish connection first + client.connect() + + // Trigger worker creation + client._onConnOpen() + + // Verify worker was created + assert.ok(client.workerRef) + const worker = client.workerRef + + // Spy on worker terminate method + const terminateSpy = vi.spyOn(worker, 'terminate') + + // Disconnect the client + client.disconnect() + + // Verify worker was terminated + expect(terminateSpy).toHaveBeenCalled() + + // Verify workerRef was cleared + assert.equal(client.workerRef, undefined) +}) From 6f6fec53ad2bf426c06905b3583c618869f6a84b Mon Sep 17 00:00:00 2001 From: Tanmay Sharma Date: Wed, 3 Dec 2025 20:34:27 +0530 Subject: [PATCH 2/2] fix(realtime): updated the worker clearance in the _startWorkerHeartBeat function Previously, the worker error handler in _startWorkerHeartbeat() directly calledworkerRef.terminate() without clearing the workerRef reference. This caused amemory leak on worker errors because on reconnection, the guard check for workerRef would prevent creating a new worker, leaving the connection withouta heartbeat mechanism.Now uses the centralized _terminateWorker() method which both terminates theworker and clears the reference, ensuring proper cleanup and allowing newworkers to be created on reconnection.This complements the fix in _teardownConnection() to fully prevent workermemory leaks across all disconnect/error scenarios. --- packages/core/realtime-js/src/RealtimeClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/realtime-js/src/RealtimeClient.ts b/packages/core/realtime-js/src/RealtimeClient.ts index 40dc75e98..94794d4b2 100755 --- a/packages/core/realtime-js/src/RealtimeClient.ts +++ b/packages/core/realtime-js/src/RealtimeClient.ts @@ -699,7 +699,7 @@ export default class RealtimeClient { this.workerRef = new Worker(objectUrl) this.workerRef.onerror = (error) => { this.log('worker', 'worker error', (error as ErrorEvent).message) - this.workerRef!.terminate() + this._terminateWorker() } this.workerRef.onmessage = (event) => { if (event.data.event === 'keepAlive') {