Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new features #4

Merged
merged 2 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
// // }
// // }
// }