-import type { WebSocketLike, WebSocketReadyState } from 'ui-common'
+import type { WebSocketLike } from 'ui-common'
import type { WebSocket as WsWebSocket } from 'ws'
import { Buffer } from 'node:buffer'
+import { createWsAdapter as createWsAdapterBase } from 'ui-common'
const toDataString = (data: WsWebSocket.Data): string => {
if (Buffer.isBuffer(data)) {
return data
}
-export const createWsAdapter = (ws: WsWebSocket): WebSocketLike => {
- let onmessageCallback: WebSocketLike['onmessage'] = null
- let onerrorCallback: WebSocketLike['onerror'] = null
- let oncloseCallback: WebSocketLike['onclose'] = null
- let onopenCallback: WebSocketLike['onopen'] = null
-
- ws.onmessage = event => {
- if (onmessageCallback != null) {
- const data = toDataString(event.data)
- onmessageCallback({ data })
- }
- }
-
- ws.onerror = event => {
- if (onerrorCallback != null) {
- const raw = event as { error?: unknown; message?: string }
- const error =
- raw.error instanceof Error ? raw.error : new Error(raw.message ?? 'Unknown error')
- const message = raw.message ?? error.message
- onerrorCallback({ error, message })
- }
- }
-
- ws.onclose = event => {
- if (oncloseCallback != null) {
- oncloseCallback({ code: event.code, reason: event.reason })
- }
- }
-
- ws.onopen = () => {
- if (onopenCallback != null) {
- onopenCallback()
- }
- }
-
- return {
- close (code?: number, reason?: string): void {
- ws.close(code, reason)
- },
- get onclose () {
- return oncloseCallback
- },
-
- set onclose (callback) {
- oncloseCallback = callback
- },
- get onerror () {
- return onerrorCallback
- },
-
- set onerror (callback) {
- onerrorCallback = callback
- },
- get onmessage () {
- return onmessageCallback
- },
-
- set onmessage (callback) {
- onmessageCallback = callback
- },
- get onopen () {
- return onopenCallback
- },
-
- set onopen (callback) {
- onopenCallback = callback
- },
-
- get readyState (): WebSocketReadyState {
- return ws.readyState as WebSocketReadyState
- },
-
- send (data: string): void {
- ws.send(data)
- },
- }
-}
+export const createWsAdapter = (ws: WsWebSocket): WebSocketLike =>
+ createWsAdapterBase(ws as unknown as Parameters<typeof createWsAdapterBase>[0], {
+ dataConverter: data => toDataString(data as WsWebSocket.Data),
+ errorDefault: 'Unknown error',
+ })
--- /dev/null
+import type { DataConverter, WebSocketReadyState } from './types.js'
+
+import { type WebSocketLike } from './types.js'
+
+interface RawWebSocket {
+ close(code?: number, reason?: string): void
+ set onclose(handler: ((event: { code: number; reason: string }) => void) | null)
+ set onerror(handler: ((event: unknown) => void) | null)
+ set onmessage(handler: ((event: { data: unknown }) => void) | null)
+ set onopen(handler: (() => void) | null)
+ readonly readyState: number
+ send(data: string): void
+}
+
+interface WsAdapterOptions {
+ dataConverter: DataConverter
+ errorDefault?: string
+}
+
+export const createWsAdapter = (ws: RawWebSocket, options: WsAdapterOptions): WebSocketLike => {
+ const { dataConverter, errorDefault = 'WebSocket error' } = options
+ let onmessageCallback: WebSocketLike['onmessage'] = null
+ let onerrorCallback: WebSocketLike['onerror'] = null
+ let oncloseCallback: WebSocketLike['onclose'] = null
+ let onopenCallback: WebSocketLike['onopen'] = null
+
+ ws.onmessage = event => {
+ if (onmessageCallback != null) {
+ const data = dataConverter(event.data)
+ onmessageCallback({ data })
+ }
+ }
+
+ ws.onerror = event => {
+ if (onerrorCallback != null) {
+ const raw = event as { error?: unknown; message?: string }
+ const error = raw.error instanceof Error ? raw.error : new Error(raw.message ?? errorDefault)
+ const message = raw.message ?? error.message
+ onerrorCallback({ error, message })
+ }
+ }
+
+ ws.onclose = event => {
+ if (oncloseCallback != null) {
+ oncloseCallback({ code: event.code, reason: event.reason })
+ }
+ }
+
+ ws.onopen = () => {
+ if (onopenCallback != null) {
+ onopenCallback()
+ }
+ }
+
+ return {
+ close (code?: number, reason?: string): void {
+ ws.close(code, reason)
+ },
+ get onclose () {
+ return oncloseCallback
+ },
+
+ set onclose (callback) {
+ oncloseCallback = callback
+ },
+ get onerror () {
+ return onerrorCallback
+ },
+
+ set onerror (callback) {
+ onerrorCallback = callback
+ },
+ get onmessage () {
+ return onmessageCallback
+ },
+
+ set onmessage (callback) {
+ onmessageCallback = callback
+ },
+ get onopen () {
+ return onopenCallback
+ },
+
+ set onopen (callback) {
+ onopenCallback = callback
+ },
+
+ get readyState (): WebSocketReadyState {
+ return ws.readyState as WebSocketReadyState
+ },
+
+ send (data: string): void {
+ ws.send(data)
+ },
+ }
+}
-import type { WebSocketReadyState } from './types.js'
+import type { WebSocketLike } from './types.js'
-import { type WebSocketLike } from './types.js'
+import { createWsAdapter } from './adapter.js'
interface BrowserWebSocket {
close(code?: number, reason?: string): void
send(data: string): void
}
-export const createBrowserWsAdapter = (ws: BrowserWebSocket): WebSocketLike => {
- let onmessageCallback: WebSocketLike['onmessage'] = null
- let onerrorCallback: WebSocketLike['onerror'] = null
- let oncloseCallback: WebSocketLike['onclose'] = null
- let onopenCallback: WebSocketLike['onopen'] = null
-
- ws.onmessage = event => {
- if (onmessageCallback != null) {
- const data = event.data as string
- onmessageCallback({ data })
- }
- }
-
- ws.onerror = event => {
- if (onerrorCallback != null) {
- const raw = event as { error?: unknown; message?: string }
- const error =
- raw.error instanceof Error ? raw.error : new Error(raw.message ?? 'WebSocket error')
- const message = raw.message ?? error.message
- onerrorCallback({ error, message })
- }
- }
-
- ws.onclose = event => {
- if (oncloseCallback != null) {
- oncloseCallback({ code: event.code, reason: event.reason })
- }
- }
-
- ws.onopen = () => {
- if (onopenCallback != null) {
- onopenCallback()
- }
- }
-
- return {
- close (code?: number, reason?: string): void {
- ws.close(code, reason)
- },
- get onclose () {
- return oncloseCallback
- },
-
- set onclose (callback) {
- oncloseCallback = callback
- },
- get onerror () {
- return onerrorCallback
- },
-
- set onerror (callback) {
- onerrorCallback = callback
- },
- get onmessage () {
- return onmessageCallback
- },
-
- set onmessage (callback) {
- onmessageCallback = callback
- },
- get onopen () {
- return onopenCallback
- },
-
- set onopen (callback) {
- onopenCallback = callback
- },
-
- get readyState (): WebSocketReadyState {
- return ws.readyState as WebSocketReadyState
- },
-
- send (data: string): void {
- ws.send(data)
- },
- }
-}
+export const createBrowserWsAdapter = (ws: BrowserWebSocket): WebSocketLike =>
+ createWsAdapter(ws, {
+ dataConverter: data => data as string,
+ errorDefault: 'WebSocket error',
+ })
export type ClientConfig = Omit<UIServerConfigurationSection, 'name'>
+export type DataConverter = (data: unknown) => string
+
export interface ResponseHandler {
reject: (reason?: unknown) => void
resolve: (value: ResponsePayload) => void
+export * from './client/adapter.js'
export * from './client/browser-adapter.js'
export * from './client/types.js'
export * from './client/WebSocketClient.js'
--- /dev/null
+/** @file Unit tests for the generic WebSocket adapter factory */
+
+import assert from 'node:assert'
+import { describe, it } from 'node:test'
+
+import { createWsAdapter } from '../src/client/adapter.js'
+import { WebSocketReadyState } from '../src/client/types.js'
+
+interface MockRawWs {
+ close: (code?: number, reason?: string) => void
+ onclose: ((event: { code: number; reason: string }) => void) | null
+ onerror: ((event: unknown) => void) | null
+ onmessage: ((event: { data: unknown }) => void) | null
+ onopen: (() => void) | null
+ readyState: number
+ send: (data: string) => void
+}
+
+const createMockRawWs = (): MockRawWs => ({
+ close: () => undefined,
+ onclose: null,
+ onerror: null,
+ onmessage: null,
+ onopen: null,
+ readyState: WebSocketReadyState.OPEN,
+ send: () => undefined,
+})
+
+await describe('generic WebSocket adapter factory', async () => {
+ await it('should apply dataConverter to message data', () => {
+ const mockWs = createMockRawWs()
+ const adapter = createWsAdapter(mockWs as never, {
+ dataConverter: data => `converted:${String(data)}`,
+ })
+ let receivedData: string | undefined
+ adapter.onmessage = event => {
+ receivedData = event.data
+ }
+ mockWs.onmessage?.({ data: 'raw' })
+ assert.strictEqual(receivedData, 'converted:raw')
+ })
+
+ await it('should use custom errorDefault in onerror fallback', () => {
+ const mockWs = createMockRawWs()
+ const adapter = createWsAdapter(mockWs as never, {
+ dataConverter: data => data as string,
+ errorDefault: 'Custom fallback',
+ })
+ let receivedMessage: string | undefined
+ adapter.onerror = event => {
+ receivedMessage = event.message
+ }
+ mockWs.onerror?.({})
+ assert.strictEqual(receivedMessage, 'Custom fallback')
+ })
+
+ await it('should default errorDefault to WebSocket error', () => {
+ const mockWs = createMockRawWs()
+ const adapter = createWsAdapter(mockWs as never, {
+ dataConverter: data => data as string,
+ })
+ let receivedMessage: string | undefined
+ adapter.onerror = event => {
+ receivedMessage = event.message
+ }
+ mockWs.onerror?.({})
+ assert.strictEqual(receivedMessage, 'WebSocket error')
+ })
+
+ await it('should delegate close, send, and readyState', () => {
+ const mockWs = createMockRawWs()
+ let closedCode: number | undefined
+ let sentData: string | undefined
+ mockWs.close = (code?: number) => {
+ closedCode = code
+ }
+ mockWs.send = (data: string) => {
+ sentData = data
+ }
+ mockWs.readyState = WebSocketReadyState.CONNECTING
+ const adapter = createWsAdapter(mockWs as never, {
+ dataConverter: data => data as string,
+ })
+ adapter.close(1000)
+ adapter.send('test')
+ assert.strictEqual(closedCode, 1000)
+ assert.strictEqual(sentData, 'test')
+ assert.strictEqual(adapter.readyState, WebSocketReadyState.CONNECTING)
+ })
+
+ await it('should forward onclose and onopen events', () => {
+ const mockWs = createMockRawWs()
+ const adapter = createWsAdapter(mockWs as never, {
+ dataConverter: data => data as string,
+ })
+ let closeCode: number | undefined
+ let opened = false
+ adapter.onclose = event => {
+ closeCode = event.code
+ }
+ adapter.onopen = () => {
+ opened = true
+ }
+ mockWs.onclose?.({ code: 1000, reason: 'normal' })
+ mockWs.onopen?.()
+ assert.strictEqual(closeCode, 1000)
+ assert.strictEqual(opened, true)
+ })
+})