Skip to content

HydreIO/shimio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hydre/shimio

A minimal multiplexed WebSocket server and client

Install

npm install @hydre/shimio

Requirements

  • Node.js: >= 20.0.0
  • ws: v8.x (upgraded from v7.x)

What's New in v5.0.0

  • ✨ Upgraded to ws v8.18.0 (from v7.3.1)
  • 🚀 Node.js 20+ support with native private methods and class properties
  • 🧹 Removed Babel dependencies (no longer needed for modern Node.js)
  • 📦 Modernized development dependencies
  • 🔧 Simplified test scripts (removed --harmony flags)

Use

Client

threshold represent the maximum WebSocket bufferedAmount length before starting to delay write operations

The client emit 3 events

  • connected when connected
  • disconnected when disconnected
  • channel when a new channel was openned
import Client from '@hydre/shimio/client'

const client = Client({
    host: 'ws://0.0.0.0:3000',
    threshold: 4096,
    retry_strategy: attempts => 100 // retry connection every 100ms
  })

// possible to pass an option object for testing in nodejs
// see https://github.com/websockets/ws/blob/41b0f9b36749ca1498d22726d22f72233de1424a/lib/websocket.js#L445
await client.connect({
  headers: {}
})

open some channel (must be awaited but do not make any network request so it's free)

const foo = await client.open_channel()
const bar = await client.open_channel()
const baz = await client.open_channel()
  • write is an async function in which you have to pass an Uint8Array
  • read is an async Iterable

A channel emit a close event

await foo.write(Uint8Array.of(100))
await bar.write(Uint8Array.of(42))
await baz.write(Uint8Array.of(100))

for await const(chunk of bar.read)
  console.log(chunk) // Uint8Array<42>

Server

import Server from '@hydre/shimio/server'
import Koa from 'koa'

// not a Class
const server = Server({
  koa: new Koa(),
  timeout: 30_000, // dropping unresponding clients
  on_upgrade: ({ request, socket, head, context }) => true, // authentication
  on_socket : ({ socket, context }) => {
    // the client opened a channel (and wrote at least once)
    socket.on('channel', async channel => {
      // let's send back all datas transparently
      for await (const chunk of channel.read)
        await channel.write(chunk)
    })
  },
  channel_limit: 50, // prevent a client from openning too much channel (encoded on an Uint32 (4,294,967,295))
  threshold    : 4096, // max bufferedAmount before delaying writes
  ws_options   : { // @see https://github.com/websockets/ws
    path             : '/',
    perMessageDeflate: false,
    maxPayload       : 4096 * 4,
  },
  request_limit: { // 20 request max every 10s
    max  : 20,
    every: 1000 * 10,
  },
  time_between_connections: 1000 * 30, // min 30s between 2 connection for an ip
})

await server.listen(3000) // promisified for you folks
await server.close()

Testing Pattern: Lazy Server Startup

For test environments, avoid auto-starting the server on module import. Instead, export startup/shutdown functions:

// src/index.js - Application setup
import Server from '@hydre/shimio/server'
import Koa from 'koa'

// Create server instance (does NOT start listening)
export const server = Server({
  koa: new Koa(),
  on_upgrade: () => true,
  on_socket: ({ socket }) => {
    socket.on('channel', async channel => {
      for await (const chunk of channel.read)
        await channel.write(chunk)
    })
  },
  ws_options: { path: '/ws' }
})

// Export start/shutdown functions
export async function start() {
  await server.listen(3000)
  console.log('Server started on port 3000')
}

export async function shutdown() {
  await server.close()
  console.log('Server stopped')
}
// src/server.js - Production entry point
import { start } from './index.js'
await start()
// test/server.test.js - Tests create their own instances
import { describe, it, before, after } from 'node:test'
import Server from '@hydre/shimio/server'
import Koa from 'koa'

describe('Server tests', () => {
  let server

  before(async () => {
    server = Server({ koa: new Koa(), /* ... */ })
    await server.listen(4000)
  })

  after(async () => {
    await server.close()
  })

  it('should work', async () => {
    // test logic
  })
})

Key Points:

  • The exported server instance is never started in test environment
  • Tests create and manage their own server instances in before/after hooks
  • Production uses a separate entry point that calls start()
  • This prevents tests from hanging due to open server connections

Migration from v4.x

Breaking Changes

ws v8 Import Changes

If you were importing ws directly in your code (not recommended, but if you did):

v4.x (ws v7):

import ws from 'ws'
const wss = new ws.Server({ port: 3000 })
globalThis.WebSocket = ws

v5.0.0 (ws v8):

import { WebSocketServer } from 'ws'
import WebSocket from 'ws'
const wss = new WebSocketServer({ port: 3000 })
globalThis.WebSocket = WebSocket

Note: If you only use @hydre/shimio/client and @hydre/shimio/server, no code changes needed! The ws v8 migration is handled internally.

Node.js Version

Upgrade to Node.js >= 20.0.0 before upgrading shimio to v5.0.0.

No Other Breaking Changes

The shimio API remains unchanged. All client and server APIs are backward compatible.

About

A minimal multiplexed Websocket server and client

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •