diff --git a/package-lock.json b/package-lock.json index cbdfa53..92e246e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@supabase/node-fetch": "^2.6.14", "@types/phoenix": "^1.5.4", - "@types/websocket": "^1.0.3", "ws": "^8.14.2" }, "devDependencies": { @@ -982,21 +981,14 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true }, "node_modules/@types/phoenix": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.5.tgz", "integrity": "sha512-1eWWT19k0L4ZiTvdXjAvJ9KvW0B8SdiVftQmFPJGTEx78Q4PCSIQDpz+EfkFVR1N4U9gREjlW4JXL8YCIlY0bw==" }, - "node_modules/@types/websocket": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz", - "integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/ws": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", diff --git a/package.json b/package.json index 6b7b939..77a6273 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "dependencies": { "@supabase/node-fetch": "^2.6.14", "@types/phoenix": "^1.5.4", - "@types/websocket": "^1.0.3", "ws": "^8.14.2" }, "devDependencies": { diff --git a/src/RealtimeClient.ts b/src/RealtimeClient.ts index 9cc64a4..1eb8abb 100755 --- a/src/RealtimeClient.ts +++ b/src/RealtimeClient.ts @@ -51,7 +51,7 @@ interface WebSocketLikeConstructor { ): WebSocketLike } -type WebSocketLike = WebSocket | WSWebSocket +type WebSocketLike = WebSocket | WSWebSocket | WSWebSocketDummy interface WebSocketLikeError { error: any @@ -61,10 +61,6 @@ interface WebSocketLikeError { const NATIVE_WEBSOCKET_AVAILABLE = typeof WebSocket !== 'undefined' -const WebSocketVariant: WebSocketLikeConstructor = NATIVE_WEBSOCKET_AVAILABLE - ? WebSocket - : require('ws') - export default class RealtimeClient { accessToken: string | null = null channels: RealtimeChannel[] = [] @@ -72,7 +68,7 @@ export default class RealtimeClient { headers?: { [key: string]: string } = DEFAULT_HEADERS params?: { [key: string]: string } = {} timeout: number = DEFAULT_TIMEOUT - transport: WebSocketLikeConstructor = WebSocketVariant + transport: WebSocketLikeConstructor | null heartbeatIntervalMs: number = 30000 heartbeatTimer: ReturnType | undefined = undefined pendingHeartbeatRef: string | null = null @@ -115,11 +111,15 @@ export default class RealtimeClient { constructor(endPoint: string, options?: RealtimeClientOptions) { this.endPoint = `${endPoint}/${TRANSPORTS.websocket}` + if (options?.transport) { + this.transport = options.transport + } else { + this.transport = null + } if (options?.params) this.params = options.params if (options?.headers) this.headers = { ...this.headers, ...options.headers } if (options?.timeout) this.timeout = options.timeout if (options?.logger) this.logger = options.logger - if (options?.transport) this.transport = options.transport if (options?.heartbeatIntervalMs) this.heartbeatIntervalMs = options.heartbeatIntervalMs @@ -155,22 +155,31 @@ export default class RealtimeClient { return } - if (NATIVE_WEBSOCKET_AVAILABLE) { - this.conn = new this.transport(this._endPointURL()) - } else { + if (this.transport) { this.conn = new this.transport(this._endPointURL(), undefined, { headers: this.headers, }) + return } - if (this.conn) { - this.conn.binaryType = 'arraybuffer' - this.conn.onopen = () => this._onConnOpen() - this.conn.onerror = (error: WebSocketLikeError) => - this._onConnError(error as WebSocketLikeError) - this.conn.onmessage = (event: any) => this._onConnMessage(event) - this.conn.onclose = (event: any) => this._onConnClose(event) + if (NATIVE_WEBSOCKET_AVAILABLE) { + this.conn = new WebSocket(this._endPointURL()) + this.setupConnection() + return } + + this.conn = new WSWebSocketDummy(this._endPointURL(), undefined, { + close: () => { + this.conn = null + }, + }) + + import('ws').then(({ default: WS }) => { + this.conn = new WS(this._endPointURL(), undefined, { + headers: this.headers, + }) + this.setupConnection() + }) } /** @@ -368,6 +377,22 @@ export default class RealtimeClient { ) } + /** + * Sets up connection handlers. + * + * @internal + */ + private setupConnection(): void { + if (this.conn) { + this.conn.binaryType = 'arraybuffer' + this.conn.onopen = () => this._onConnOpen() + this.conn.onerror = (error: WebSocketLikeError) => + this._onConnError(error as WebSocketLikeError) + this.conn.onmessage = (event: any) => this._onConnMessage(event) + this.conn.onclose = (event: any) => this._onConnClose(event) + } + } + /** * Returns the URL of the websocket. * @@ -489,3 +514,24 @@ export default class RealtimeClient { this.setAuth(this.accessToken) } } + +class WSWebSocketDummy { + binaryType: string = 'arraybuffer' + close: Function + onclose: Function = () => {} + onerror: Function = () => {} + onmessage: Function = () => {} + onopen: Function = () => {} + readyState: number = SOCKET_STATES.connecting + send: Function = () => {} + url: string | URL | null = null + + constructor( + address: string, + _protocols: undefined, + options: { close: Function } + ) { + this.url = address + this.close = options.close + } +} diff --git a/test/socket_test.js b/test/socket_test.js index 49f3ac2..ab8568a 100755 --- a/test/socket_test.js +++ b/test/socket_test.js @@ -1,9 +1,8 @@ import assert from 'assert' import { Server as WebSocketServer, WebSocket } from 'mock-socket' import sinon from 'sinon' -import { WebSocket as W3CWebSocket } from 'ws' +import { WebSocket as WSWebSocket } from 'ws' import { RealtimeClient } from '../dist/main' -import { CONNECTION_STATE } from '../dist/main/lib/constants' let socket @@ -33,7 +32,7 @@ describe('constructor', () => { error: [], message: [], }) - assert.equal(socket.transport, W3CWebSocket) + assert.equal(socket.transport, null) assert.equal(socket.timeout, 10000) assert.equal(socket.heartbeatIntervalMs, 30000) assert.equal(typeof socket.logger, 'function') @@ -82,7 +81,7 @@ describe('constructor', () => { it('defaults to Websocket transport if available', () => { socket = new RealtimeClient('wss://example.com/socket') - assert.equal(socket.transport, W3CWebSocket) + assert.equal(socket.transport, null) }) }) }) @@ -145,7 +144,6 @@ describe('connect with WebSocket', () => { socket.connect() let conn = socket.conn - assert.ok(conn instanceof W3CWebSocket) assert.equal(conn.url, socket._endPointURL()) }) @@ -227,7 +225,7 @@ describe('connectionState', () => { assert.equal(socket.connectionState(), 'closed') }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('returns closed if readyState unrecognized', () => { socket.connect() @@ -235,7 +233,7 @@ describe('connectionState', () => { assert.equal(socket.connectionState(), 'closed') }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('returns connecting', () => { socket.connect() @@ -244,7 +242,7 @@ describe('connectionState', () => { assert.ok(!socket.isConnected(), 'is not connected') }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('returns open', () => { socket.connect() @@ -253,7 +251,7 @@ describe('connectionState', () => { assert.ok(socket.isConnected(), 'is connected') }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('returns closing', () => { socket.connect() @@ -262,7 +260,7 @@ describe('connectionState', () => { assert.ok(!socket.isConnected(), 'is not connected') }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('returns closed', () => { socket.connect() @@ -417,7 +415,7 @@ describe('push', () => { socket.disconnect() }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('sends data to connection when connected', () => { socket.connect() socket.conn.readyState = 1 // open @@ -429,7 +427,7 @@ describe('push', () => { assert.ok(spy.calledWith(json)) }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('buffers data when not connected', () => { socket.connect() socket.conn.readyState = 0 // connecting @@ -539,7 +537,7 @@ describe('sendHeartbeat', () => { socket.disconnect() }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip("closes socket when heartbeat is not ack'd within heartbeat window", () => { let closed = false socket.conn.readyState = 1 // open @@ -551,7 +549,7 @@ describe('sendHeartbeat', () => { assert.equal(closed, true) }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('pushes heartbeat data when connected', () => { socket.conn.readyState = 1 // open @@ -563,7 +561,7 @@ describe('sendHeartbeat', () => { assert.ok(spy.calledWith(data)) }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('no ops when not connected', () => { socket.conn.readyState = 0 // connecting @@ -594,7 +592,7 @@ describe('flushSendBuffer', () => { socket.disconnect() }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('calls callbacks in buffer when connected', () => { socket.conn.readyState = 1 // open const spy1 = sinon.spy() @@ -610,7 +608,7 @@ describe('flushSendBuffer', () => { assert.equal(spy3.callCount, 0) }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('empties sendBuffer', () => { socket.conn.readyState = 1 // open socket.sendBuffer.push(() => { }) @@ -646,7 +644,7 @@ describe('_onConnOpen', () => { socket.disconnect() }) - // TODO: fix for W3CWebSocket + // TODO: fix for WSWebSocket it.skip('flushes the send buffer', () => { socket.conn.readyState = 1 // open