From d5848422c7d983c6a9035e955247fb9909750d0d Mon Sep 17 00:00:00 2001 From: Matvey Melishev <58311421+melishev@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:18:34 +0100 Subject: [PATCH 1/2] feat: add new features --- .github/workflows/release.yml | 2 - README.md | 9 +++ src/index.ts | 104 ++++++++++++++++++++++++++++------ src/types/index.ts | 47 +++++++++++++-- src/utils.ts | 22 +++++++ 5 files changed, 159 insertions(+), 25 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..5d5af5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,78 @@ -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, ): { + ws: WebSocket | undefined + // status: 'OPEN' | 'CLOSED' | 'CONNECTING' + + open: () => void + close: () => void send: (...args: RemoveFirstFromTuple>) => void subscribe: (...args: RemoveFirstFromTuple>) => void -} => { +} { + let ws: WebSocket | undefined + const subscriptions: WSGOSubscriptions = {} + + if (config?.immediate ?? true) { + ws = open(url, config) + + if (ws !== undefined) { + _listen(ws, subscriptions) + } + } + + return { + ws, + open: () => { + ws = open(url, config) + + if (ws !== undefined) { + _listen(ws, subscriptions) + } + }, + close: () => { + close(ws) + }, + send: (...args: RemoveFirstFromTuple>): ReturnType => { + send(ws, ...args) + }, + subscribe: (...args: RemoveFirstFromTuple>): ReturnType => { + subscribe(subscriptions, ...args) + }, + } +} + +function open(url?: string, config?: WSGOConfig): 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 @@ -27,33 +90,40 @@ const create = ( 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(ws?: WebSocket, eventName: string, data?: any): void { + if (ws === undefined) return + // start debug logging + const timeout = 100 console.group(eventName, data) + ws.send(JSON.stringify({ event: eventName, data })) + + // stop debug logging 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..c95241b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,10 +1,45 @@ -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 + + 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..4ab6f7a --- /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) + // } + // } +} From 1d354dc7be301912332fa94c7fdd762c7825dd80 Mon Sep 17 00:00:00 2001 From: Matvey Melishev <58311421+melishev@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:20:41 +0100 Subject: [PATCH 2/2] refactor: update code, update types --- src/index.ts | 65 ++++++++++++++++++++++++++++------------------ src/types/index.ts | 1 + src/utils.ts | 26 +++++++++---------- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5d5af5d..0ab5390 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,14 +13,14 @@ export default function create( open: () => void close: () => void - send: (...args: RemoveFirstFromTuple>) => void - subscribe: (...args: RemoveFirstFromTuple>) => 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, config) + ws = open(url) if (ws !== undefined) { _listen(ws, subscriptions) @@ -28,9 +28,11 @@ export default function create( } return { - ws, + get ws() { + return ws + }, open: () => { - ws = open(url, config) + ws = open(url) if (ws !== undefined) { _listen(ws, subscriptions) @@ -39,16 +41,16 @@ export default function create( close: () => { close(ws) }, - send: (...args: RemoveFirstFromTuple>): ReturnType => { - send(ws, ...args) + send: (...args) => { + send(...args, ws, config) }, - subscribe: (...args: RemoveFirstFromTuple>): ReturnType => { + subscribe: (...args) => { subscribe(subscriptions, ...args) }, } } -function open(url?: string, config?: WSGOConfig): WebSocket | undefined { +function open(url?: string): WebSocket | undefined { if (url === undefined) return // close() @@ -77,13 +79,25 @@ function _listen(ws: WebSocket, subscriptions: WSGOSubscriptions, config?: WSGOC 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) + } + + return + } - 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 (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) { @@ -92,7 +106,7 @@ function _listen(ws: WebSocket, subscriptions: WSGOSubscriptions, config?: WSGOC } } -function close(ws?: WebSocket, ...[code = 1000, reason]: Parameters): void { +function close(ws?: WebSocket, ...[code = 1000, reason]: Parameters): void { if (ws === undefined) return // stop heartbeat interval @@ -102,19 +116,20 @@ function close(ws?: WebSocket, ...[code = 1000, reason]: Parameters { + console.groupEnd() + }, timeout) + } ws.send(JSON.stringify({ event: eventName, data })) - - // stop debug logging - setTimeout(() => { - console.groupEnd() - }, timeout) } /** Method allows you to subscribe to listen to a specific event */ diff --git a/src/types/index.ts b/src/types/index.ts index c95241b..0b0a398 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,7 @@ export interface WSGOConfig { onDisconnected?: (ws: WebSocket, event: CloseEvent) => void onError?: (ws: WebSocket, event: Event) => void + debugging?: boolean immediate?: boolean } diff --git a/src/utils.ts b/src/utils.ts index 4ab6f7a..bc6d975 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ const heartbeatMessage = 'ping' const heartbeatInterval = 1000 -const heartbeatPongTimeout = 1000 +// const heartbeatPongTimeout = 1000 export function heartbeat(ws: WebSocket): void { setInterval(() => { @@ -8,15 +8,15 @@ export function heartbeat(ws: WebSocket): void { }, 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) - // } - // } -} +// 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) +// // } +// // } +// }