Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ docs/v2
.env
.nyc_output
coverage/
.claude/settings.local.json
.claude/settings.local.json
.DS_Store
28 changes: 23 additions & 5 deletions src/RealtimeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,34 @@ export default class RealtimeClient {
this._setAuthSafely('connect')

// Establish WebSocket connection
if (!this.transport) {
if (this.transport) {
// Use custom transport if provided
this.conn = new this.transport(this.endpointURL()) as WebSocketLike
} else {
// Try to use native WebSocket
try {
this.conn = WebSocketFactory.createWebSocket(this.endpointURL())
} catch (error) {
this._setConnectionState('disconnected')
throw new Error(`WebSocket not available: ${(error as Error).message}`)
const errorMessage = (error as Error).message

// Provide helpful error message based on environment
if (errorMessage.includes('Node.js')) {
throw new Error(
`${errorMessage}\n\n` +
'To use Realtime in Node.js, you need to provide a WebSocket implementation:\n\n' +
'Option 1: Use Node.js 22+ which has native WebSocket support\n' +
'Option 2: Install and provide the "ws" package:\n\n' +
' npm install ws\n\n' +
' import ws from "ws"\n' +
' const client = new RealtimeClient(url, {\n' +
' ...options,\n' +
' transport: ws\n' +
' })'
)
}
throw new Error(`WebSocket not available: ${errorMessage}`)
}
} else {
// Use custom transport if provided
this.conn = new this.transport!(this.endpointURL()) as WebSocketLike
}
this._setupConnectionHandlers()
}
Expand Down
72 changes: 19 additions & 53 deletions src/lib/websocket-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,6 @@ export interface WebSocketEnvironment {
}

export class WebSocketFactory {
/**
* Dynamic require that works in both CJS and ESM environments
* Bulletproof against strict ESM environments where require might not be in scope
* @private
*/
private static dynamicRequire(moduleId: string): any {
try {
// Check if we're in a Node.js environment first
if (
typeof process !== 'undefined' &&
process.versions &&
process.versions.node
) {
// In Node.js, both CJS and ESM support require for dynamic imports
// Wrap in try/catch to handle strict ESM environments
if (typeof require !== 'undefined') {
return require(moduleId)
}
}
return null
} catch {
// Catches any error from typeof require OR require() call in strict ESM
return null
}
}

private static detectEnvironment(): WebSocketEnvironment {
if (typeof WebSocket !== 'undefined') {
return { type: 'native', constructor: WebSocket }
Expand Down Expand Up @@ -112,39 +86,31 @@ export class WebSocketFactory {
process.versions.node
) {
const nodeVersion = parseInt(process.versions.node.split('.')[0])

// Node.js 22+ should have native WebSocket
if (nodeVersion >= 22) {
try {
if (typeof globalThis.WebSocket !== 'undefined') {
return { type: 'native', constructor: globalThis.WebSocket }
}
const undici = this.dynamicRequire('undici')
if (undici && undici.WebSocket) {
return { type: 'native', constructor: undici.WebSocket }
}
throw new Error('undici not available')
} catch (err) {
return {
type: 'unsupported',
error: `Node.js ${nodeVersion} detected but native WebSocket not found.`,
workaround:
'Install the "ws" package or check your Node.js installation.',
}
// Check if native WebSocket is available (should be in Node.js 22+)
if (typeof globalThis.WebSocket !== 'undefined') {
return { type: 'native', constructor: globalThis.WebSocket }
}
}
try {
// Use dynamic require to work in both CJS and ESM environments
const ws = this.dynamicRequire('ws')
if (ws) {
return { type: 'ws', constructor: ws.WebSocket ?? ws }
}
throw new Error('ws package not available')
} catch (err) {
// If not available, user needs to provide it
return {
type: 'unsupported',
error: `Node.js ${nodeVersion} detected without WebSocket support.`,
workaround: 'Install the "ws" package: npm install ws',
error: `Node.js ${nodeVersion} detected but native WebSocket not found.`,
workaround:
'Provide a WebSocket implementation via the transport option.',
}
}

// Node.js < 22 doesn't have native WebSocket
return {
type: 'unsupported',
error: `Node.js ${nodeVersion} detected without native WebSocket support.`,
workaround:
'For Node.js < 22, install "ws" package and provide it via the transport option:\n' +
'import ws from "ws"\n' +
'new RealtimeClient(url, { transport: ws })',
}
}

return {
Expand Down
2 changes: 1 addition & 1 deletion test/RealtimeChannel.lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ describe('Channel Lifecycle Management', () => {
test('_rejoin does nothing when channel state is leaving', () => {
// Set up channel to be in 'leaving' state
channel.state = CHANNEL_STATES.leaving

// Spy on socket methods to verify no actions are taken
const leaveOpenTopicSpy = vi.spyOn(testSetup.socket, '_leaveOpenTopic')
const resendSpy = vi.spyOn(channel.joinPush, 'resend')
Expand Down
10 changes: 8 additions & 2 deletions test/RealtimeChannel.postgres.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,10 @@ describe('PostgreSQL payload transformation', () => {
table: 'users',
commit_timestamp: '2023-01-01T00:00:00Z',
errors: [],
columns: [{ name: 'id', type: 'int4' }, { name: 'name', type: 'text' }],
columns: [
{ name: 'id', type: 'int4' },
{ name: 'name', type: 'text' },
],
record: { id: 1, name: 'updated' },
old_record: { id: 1, name: 'original' },
},
Expand Down Expand Up @@ -627,7 +630,10 @@ describe('PostgreSQL payload transformation', () => {
table: 'users',
commit_timestamp: '2023-01-01T00:00:00Z',
errors: [],
columns: [{ name: 'id', type: 'int4' }, { name: 'name', type: 'text' }],
columns: [
{ name: 'id', type: 'int4' },
{ name: 'name', type: 'text' },
],
old_record: { id: 2, name: 'deleted' },
},
},
Expand Down
Loading