From 28a866f9dea4c13d3ddc65c94ea01e23e7d35629 Mon Sep 17 00:00:00 2001 From: Matvey Melishev Date: Mon, 5 Feb 2024 20:34:59 +0100 Subject: [PATCH] feat: add new features (#4) * feat: add new features * refactor: update code, update types --- .github/workflows/release.yml | 2 - README.md | 9 +++ src/index.ts | 143 +++++++++++++++++++++++++++------- src/types/index.ts | 48 ++++++++++-- src/utils.ts | 22 ++++++ 5 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 src/utils.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50f48cf..39aa66e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,8 +76,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - persist-credentials: false - name: Install Node.js uses: actions/setup-node@v4 diff --git a/README.md b/README.md index 25d2039..bca7569 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,12 @@ > [!WARNING] > Please lock the version of the package. This library is not stable yet and may have some behavioral differences depending on the version. + +### What is WSGO? + +The WSGO library acts as an abstraction on top of a pure WebSocket connection. Think of it as: + +- Socket.io, only without being tied to your server implementation +- Axios, just for WebSocket communication + +WSGO is designed to standardize communication between client and server through an explicit and common communication format diff --git a/src/index.ts b/src/index.ts index f742807..0ab5390 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,59 +1,144 @@ -import { type Subscriptions } from './types' +import { type WSGOConfig, type WSGOSubscribeRespone, type WSGOSubscriptions } from './types' import { type RemoveFirstFromTuple } from './types/utils' +import { heartbeat } from './utils' + /** Method allows you create new WebSocket connection */ -const create = ( +export default function create( url: string, + config?: WSGOConfig, ): { - send: (...args: RemoveFirstFromTuple>) => void - subscribe: (...args: RemoveFirstFromTuple>) => void -} => { + ws: WebSocket | undefined + // status: 'OPEN' | 'CLOSED' | 'CONNECTING' + + open: () => void + close: () => void + send: (eventName: Parameters[0], data?: Parameters[1]) => ReturnType + subscribe: (...args: RemoveFirstFromTuple>) => ReturnType +} { + let ws: WebSocket | undefined + const subscriptions: WSGOSubscriptions = {} + + if (config?.immediate ?? true) { + ws = open(url) + + if (ws !== undefined) { + _listen(ws, subscriptions) + } + } + + return { + get ws() { + return ws + }, + open: () => { + ws = open(url) + + if (ws !== undefined) { + _listen(ws, subscriptions) + } + }, + close: () => { + close(ws) + }, + send: (...args) => { + send(...args, ws, config) + }, + subscribe: (...args) => { + subscribe(subscriptions, ...args) + }, + } +} + +function open(url?: string): WebSocket | undefined { + if (url === undefined) return + + // close() + const ws = new WebSocket(url) - const subscriptions: Subscriptions = {} + // initialize heartbeat interval + heartbeat(ws) + + return ws +} + +function _listen(ws: WebSocket, subscriptions: WSGOSubscriptions, config?: WSGOConfig): void { + // TODO: если добавится логика, то можно оставить + ws.onopen = (ev) => { + config?.onConnected?.(ws, ev) + } + + ws.onclose = (ev) => { + config?.onDisconnected?.(ws, ev) + } + + ws.onerror = (ev) => { + config?.onError?.(ws, ev) + } ws.onmessage = (e: MessageEvent): any => { if (e.data === 'pong') return - const message = JSON.parse(e.data) + let message + + try { + message = JSON.parse(e.data) + } catch (e) { + if (config?.debugging ?? false) { + console.error(e) + } - if (message.event === 'exception') { - console.error(message.data) - } else { - const { event, data, time } = message - console.log(`%c${new Date(time).toLocaleTimeString()}%c`, 'color: gray', '', event, data) + return + } + + if (config?.debugging ?? false) { + if (message.event === 'exception') { + console.error(message.data) + } else { + const { event, data, time } = message + console.log(`%c${new Date(time).toLocaleTimeString()}%c`, 'color: gray', '', event, data) + } } if (message.event in subscriptions) { subscriptions[message.event](message) } } +} - return { - send: (...args: RemoveFirstFromTuple>): ReturnType => { - send(ws, ...args) - }, - subscribe: (...args: RemoveFirstFromTuple>): ReturnType => { - subscribe(subscriptions, ...args) - }, - } +function close(ws?: WebSocket, ...[code = 1000, reason]: Parameters): void { + if (ws === undefined) return + + // stop heartbeat interval + + // close websocket connection + ws.close(code, reason) } /** Method allows you to send an event to the server */ -function send(ws: WebSocket, eventName: string, data?: any): void { - const timeout = 100 +function send(eventName: string, data?: any, ws?: WebSocket, config?: WSGOConfig): void { + if (ws === undefined) return + + if (config?.debugging ?? false) { + // start debug logging + const timeout = 100 + console.group(eventName, data) + // stop debug logging + setTimeout(() => { + console.groupEnd() + }, timeout) + } - console.group(eventName, data) ws.send(JSON.stringify({ event: eventName, data })) - setTimeout(() => { - console.groupEnd() - }, timeout) } /** Method allows you to subscribe to listen to a specific event */ -function subscribe(subscriptions: Subscriptions, eventName: string, callback: (message: any) => any): void { +function subscribe( + subscriptions: WSGOSubscriptions, + eventName: string, + callback: (message: WSGOSubscribeRespone) => any, +): void { if (eventName in subscriptions) return Object.assign(subscriptions, { [eventName]: callback }) } - -export default create diff --git a/src/types/index.ts b/src/types/index.ts index 636466c..0b0a398 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,10 +1,46 @@ -export type Subscriptions = Record any> +export type WSGOSubscriptions = Record any> -export interface WSRespone { +export interface WSGOConfig { + onConnected?: (ws: WebSocket, event: Event) => void + onDisconnected?: (ws: WebSocket, event: CloseEvent) => void + onError?: (ws: WebSocket, event: Event) => void + + debugging?: boolean + immediate?: boolean +} + +export interface WSGOSubscribeRespone { + /** Event Name */ event: T['serverToClientName'] + /** Event data */ data: T['ServerToClientData'] - time: number - - /** Время когда сервер отправил событие */ - timefromServer: number + /** Time when the server sent the event */ + timeSended: number + /** Time when the client received the event */ + timeReceived: number } + +export type WSGOHeartbeat = + | boolean + | { + /** + * Message for the heartbeat + * + * @default 'ping' + */ + message?: string | ArrayBuffer | Blob + + /** + * Interval, in milliseconds + * + * @default 1000 + */ + interval?: number + + /** + * Heartbeat response timeout, in milliseconds + * + * @default 1000 + */ + pongTimeout?: number + } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..bc6d975 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,22 @@ +const heartbeatMessage = 'ping' +const heartbeatInterval = 1000 +// const heartbeatPongTimeout = 1000 + +export function heartbeat(ws: WebSocket): void { + setInterval(() => { + ws.send(heartbeatMessage) + }, heartbeatInterval) +} + +// export function subscribeHeartbeat(ws: WebSocket): void { +// // ws.onmessage = (e: MessageEvent): any => { +// // if (e.data === 'pong') return +// // const message = JSON.parse(e.data) +// // if (message.event === 'exception') { +// // console.error(message.data) +// // } else { +// // const { event, data, time } = message +// // console.log(`%c${new Date(time).toLocaleTimeString()}%c`, 'color: gray', '', event, data) +// // } +// // } +// }