Skip to content

Commit

Permalink
feat: add new features (#4)
Browse files Browse the repository at this point in the history
* feat: add new features

* refactor: update code, update types
  • Loading branch information
melishev committed Feb 5, 2024
1 parent 3bfd12b commit 28a866f
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 37 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
143 changes: 114 additions & 29 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<Parameters<typeof send>>) => void
subscribe: (...args: RemoveFirstFromTuple<Parameters<typeof subscribe>>) => void
} => {
ws: WebSocket | undefined
// status: 'OPEN' | 'CLOSED' | 'CONNECTING'

open: () => void
close: () => void
send: (eventName: Parameters<typeof send>[0], data?: Parameters<typeof send>[1]) => ReturnType<typeof send>
subscribe: (...args: RemoveFirstFromTuple<Parameters<typeof subscribe>>) => ReturnType<typeof subscribe>
} {
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>): 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<Parameters<typeof send>>): ReturnType<typeof send> => {
send(ws, ...args)
},
subscribe: (...args: RemoveFirstFromTuple<Parameters<typeof subscribe>>): ReturnType<typeof subscribe> => {
subscribe(subscriptions, ...args)
},
}
function close(ws?: WebSocket, ...[code = 1000, reason]: Parameters<WebSocket['close']>): 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>) => any,
): void {
if (eventName in subscriptions) return

Object.assign(subscriptions, { [eventName]: callback })
}

export default create
48 changes: 42 additions & 6 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
export type Subscriptions = Record<string, (message: any) => any>
export type WSGOSubscriptions = Record<string, (message: any) => any>

export interface WSRespone<T extends { serverToClientName: string; ServerToClientData: any }> {
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<T extends { serverToClientName: string; ServerToClientData: any }> {
/** 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
}
22 changes: 22 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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>): 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)
// // }
// // }
// }

0 comments on commit 28a866f

Please sign in to comment.