Skip to content

Instantly share code, notes, and snippets.

@TimonLukas
Created August 12, 2023 19:06
Show Gist options
  • Save TimonLukas/7c757a3b234344ad71e6bd5a6a10adc1 to your computer and use it in GitHub Desktop.
Save TimonLukas/7c757a3b234344ad71e6bd5a6a10adc1 to your computer and use it in GitHub Desktop.
TRPC custom message encoding in WebSocket
export class WsClient {
#socket: WebSocket
#listeners: {
open: ((event: Event) => any)[]
close: ((event: CloseEvent) => any)[]
error: ((event: Event) => any)[]
message: ((event: MessageEvent) => any)[]
} = {
open: [],
close: [],
error: [],
message: [],
}
constructor(url: string) {
this.#socket = new WebSocket(url)
this.#socket.addEventListener("open", (e) => this.#listeners.open.forEach((callback) => callback(e)))
this.#socket.addEventListener("close", (e) => this.#listeners.close.forEach((callback) => callback(e)))
this.#socket.addEventListener("error", (e) => this.#listeners.error.forEach((callback) => callback(e)))
this.#socket.addEventListener("message", (e) => {
const [id, result] = JSON.parse(e.data)
const event = { ...e, data: JSON.stringify({ id, result })}
this.#listeners.message.forEach((callback) => callback(event))
})
}
addEventListener(event: string, callback: (e: Event) => any): this {
switch (event) {
case "open":
this.#listeners.open.push(callback)
break
case "close":
this.#listeners.close.push(callback)
break
case "error":
this.#listeners.error.push(callback)
break
case "message":
this.#listeners.message.push(callback)
break
default:
throw new Error(`Unexpected event type: '${event}'`)
}
return this
}
send(data: string): void {
const { id, method, params } = JSON.parse(data)
this.#socket.send(JSON.stringify([id, method, params]))
}
close(code?: number | undefined, reason?: string | undefined): void {
this.#socket.close(code, reason)
}
}
import { IncomingMessage, Server } from "http"
import { WebSocket, WebSocketServer, RawData } from "ws";
export class WsServer {
#server: WebSocketServer
#listeners: {
connection: ((socket: WebSocket, request: IncomingMessage) => Promise<any>)[]
} = {
connection: [],
}
constructor(options: {
server: Server
}) {
this.#server = new WebSocketServer(options)
this.#server.on("connection", (socket, ...args) => {
const mySocket = new WsSocket(socket)
this.#listeners.connection.forEach((callback) => callback(mySocket as unknown as WebSocket, ...args))
})
}
on(event: string, callback: (...args: any[]) => any): this {
switch(event) {
case "connection":
this.#listeners.connection.push(callback)
break
default:
throw new Error(`Unexpected event type: '${event}'`)
}
return this
}
}
class WsSocket {
#socket: WebSocket
#listeners: {
message: ((message: RawData, isBinary: boolean) => any)[]
error: ((error: Error) => any)[]
close: ((code: number, reason: Buffer) => any)[]
} = {
message: [],
error: [],
close: [],
}
constructor(socket: WebSocket) {
this.#socket = socket
this.#socket.on("message", (data: RawData, isBinary: boolean) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const [id, method, params] = JSON.parse(data.toString())
const reformatted = JSON.stringify({ id, method, params })
this.#listeners.message.forEach((callback) => callback(reformatted as unknown as RawData, isBinary))
})
this.#socket.on("error", (error) => {
this.#listeners.error.forEach((callback) => callback(error))
})
this.#socket.on("close", (code: number, reason: Buffer) => {
this.#listeners.close.forEach((callback) => callback(code, reason))
})
}
on(event: string, callback: (...args: any[]) => any): this {
switch (event) {
case "message":
this.#listeners.message.push(callback)
break
case "error":
this.#listeners.error.push(callback)
break
case "close":
this.#listeners.close.push(callback)
break
default:
throw new Error(`Unexpected event type: '${event}'`)
}
return this
}
off(event: string, callback: (...args: any[]) => any): this {
switch (event) {
case "close":
const index = this.#listeners.close.indexOf(callback)
if (index !== -1) {
this.#listeners.close.splice(index, 1)
}
break
default:
throw new Error(`Unexpected event type: '${event}'`)
}
return this
}
once(event: string, callback: (...args: any[]) => any): this {
const handleCallback = (...args: any[]) => {
callback(...args)
this.off(event, handleCallback)
}
this.on(event, handleCallback)
return this
}
send(
data: BufferLike,
options: { mask?: boolean | undefined; binary?: boolean | undefined; compress?: boolean | undefined; fin?: boolean | undefined },
cb?: (err?: Error) => void,
) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const parsedData = JSON.parse(data.toString())
const reformatted = JSON.stringify([parsedData.id, parsedData.result])
return this.#socket.send(reformatted, options, cb)
}
}
type BufferLike =
| string
| Buffer
| DataView
| number
| ArrayBufferView
| Uint8Array
| ArrayBuffer
| SharedArrayBuffer
| { valueOf(): ArrayBuffer }
| { valueOf(): SharedArrayBuffer }
| { valueOf(): Uint8Array }
| { valueOf(): string }
| { [Symbol.toPrimitive](hint: string): string };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment