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

Commit 82e9966

Browse files
authored
fix: Add Web Workers as a way to run health check (#431)
Added capacity to use Worker for heart beats. We also allow users to override the worker URL so they are able to modify it as they see fit.
1 parent 176ccab commit 82e9966

File tree

5 files changed

+116
-11
lines changed

5 files changed

+116
-11
lines changed

package-lock.json

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"sinon": "^18.0.0",
6060
"typedoc": "^0.22.16",
6161
"typescript": "^4.0.3",
62-
"vitest": "^2.0.5"
62+
"vitest": "^2.0.5",
63+
"web-worker": "1.2.0"
6364
}
6465
}

src/RealtimeClient.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export type RealtimeClientOptions = {
3838
params?: { [key: string]: any }
3939
log_level?: 'info' | 'debug' | 'warn' | 'error'
4040
fetch?: Fetch
41+
worker?: boolean
42+
workerUrl?: string
4143
}
4244

4345
export type RealtimeMessage = {
@@ -69,7 +71,12 @@ interface WebSocketLikeError {
6971
}
7072

7173
const NATIVE_WEBSOCKET_AVAILABLE = typeof WebSocket !== 'undefined'
72-
74+
const WORKER_SCRIPT = `
75+
addEventListener("message", (e) => {
76+
if (e.data.event === "start") {
77+
setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval);
78+
}
79+
});`
7380
export default class RealtimeClient {
7481
accessToken: string | null = null
7582
apiKey: string | null = null
@@ -104,6 +111,9 @@ export default class RealtimeClient {
104111
message: [],
105112
}
106113
fetch: Fetch
114+
worker?: boolean
115+
workerUrl?: string
116+
workerRef?: Worker
107117

108118
/**
109119
* Initializes the Socket.
@@ -119,6 +129,8 @@ export default class RealtimeClient {
119129
* @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload))
120130
* @param options.decode The function to decode incoming messages. Defaults to Serializer's decode.
121131
* @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off.
132+
* @param options.worker Use Web Worker to set a side flow. Defaults to false.
133+
* @param options.workerUrl The URL of the worker script. Defaults to https://realtime.supabase.com/worker.js that includes a heartbeat event call to keep the connection alive.
122134
*/
123135
constructor(endPoint: string, options?: RealtimeClientOptions) {
124136
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
@@ -160,6 +172,13 @@ export default class RealtimeClient {
160172
}, this.reconnectAfterMs)
161173

162174
this.fetch = this._resolveFetch(options?.fetch)
175+
if (options?.worker) {
176+
if (typeof window !== 'undefined' && !window.Worker) {
177+
throw new Error('Web Worker is not supported')
178+
}
179+
this.worker = options?.worker || false
180+
this.workerUrl = options?.workerUrl
181+
}
163182
}
164183

165184
/**
@@ -448,19 +467,45 @@ export default class RealtimeClient {
448467
}
449468

450469
/** @internal */
451-
private _onConnOpen() {
470+
private async _onConnOpen() {
452471
this.log('transport', `connected to ${this._endPointURL()}`)
453472
this._flushSendBuffer()
454473
this.reconnectTimer.reset()
455-
this.heartbeatTimer && clearInterval(this.heartbeatTimer)
456-
this.heartbeatTimer = setInterval(
457-
() => this._sendHeartbeat(),
458-
this.heartbeatIntervalMs
459-
)
474+
if (!this.worker) {
475+
this.heartbeatTimer && clearInterval(this.heartbeatTimer)
476+
this.heartbeatTimer = setInterval(
477+
() => this._sendHeartbeat(),
478+
this.heartbeatIntervalMs
479+
)
480+
} else {
481+
if (this.workerUrl) {
482+
this.log('worker', `starting worker for from ${this.workerUrl}`)
483+
} else {
484+
this.log('worker', `starting default worker`)
485+
}
486+
487+
const objectUrl = this._workerObjectUrl(this.workerUrl!)
488+
this.workerRef = new Worker(objectUrl)
489+
this.workerRef.onerror = (error) => {
490+
this.log('worker', 'worker error', error.message)
491+
this.workerRef!.terminate()
492+
}
493+
this.workerRef.onmessage = (event) => {
494+
if (event.data.event === 'keepAlive') {
495+
this._sendHeartbeat()
496+
}
497+
}
498+
this.workerRef.postMessage({
499+
event: 'start',
500+
interval: this.heartbeatIntervalMs,
501+
})
502+
}
503+
460504
this.stateChangeCallbacks.open.forEach((callback) => callback())!
461505
}
462506

463507
/** @internal */
508+
464509
private _onConnClose(event: any) {
465510
this.log('transport', 'close', event)
466511
this._triggerChanError()
@@ -527,6 +572,17 @@ export default class RealtimeClient {
527572
})
528573
this.setAuth(this.accessToken)
529574
}
575+
576+
private _workerObjectUrl(url: string | undefined): string {
577+
let result_url: string
578+
if (url) {
579+
result_url = url
580+
} else {
581+
const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' })
582+
result_url = URL.createObjectURL(blob)
583+
}
584+
return result_url
585+
}
530586
}
531587

532588
class WSWebSocketDummy {

test/channel.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import assert from 'assert'
22
import sinon from 'sinon'
3-
import { describe, beforeEach, afterEach, test } from 'vitest'
3+
import {
4+
describe,
5+
beforeEach,
6+
afterEach,
7+
test,
8+
beforeAll,
9+
afterAll,
10+
} from 'vitest'
411

512
import RealtimeClient from '../src/RealtimeClient'
613
import RealtimeChannel from '../src/RealtimeChannel'
714
import { Response } from '@supabase/node-fetch'
15+
import { WebSocketServer } from 'ws'
16+
import Worker from 'web-worker'
817

918
let channel, socket
1019
const defaultRef = '1'
@@ -1416,3 +1425,34 @@ describe('trigger', () => {
14161425
assert.equal(client.accessToken, '123')
14171426
})
14181427
})
1428+
1429+
describe('worker', () => {
1430+
let client
1431+
let mockServer
1432+
1433+
beforeAll(() => {
1434+
window.Worker = Worker
1435+
mockServer = new WebSocketServer({ port: 8080 })
1436+
})
1437+
1438+
afterAll(() => {
1439+
window.Worker = undefined
1440+
mockServer.close()
1441+
})
1442+
1443+
beforeEach(() => {
1444+
client = new RealtimeClient('ws://localhost:8080/socket', {
1445+
worker: true,
1446+
workerUrl: 'https://realtime.supabase.com/worker.js',
1447+
heartbeatIntervalMs: 10,
1448+
})
1449+
})
1450+
1451+
test('sets worker flag', () => {
1452+
assert.ok(client.worker)
1453+
})
1454+
1455+
test('sets worker URL', () => {
1456+
assert.equal(client.workerUrl, 'https://realtime.supabase.com/worker.js')
1457+
})
1458+
})

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"esModuleInterop": true,
1313
"moduleResolution": "Node",
1414
"forceConsistentCasingInFileNames": true,
15-
"stripInternal": true
15+
"stripInternal": true,
1616
}
1717
}

0 commit comments

Comments
 (0)