npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@swarmmachina/swm-core

v5.2.2

Published

High-performance HTTP/WebSocket server built on swm-uws

Readme

@swarmmachina/swm-core

CI License: MPL 2.0 Node.js runtime dependencies stability

A high-performance HTTP/WebSocket server built on the @swarmmachina/swm-uws native binding.

Features

  • Native uWS transport - HTTP/WebSocket transport through swm-uws.
  • HTTP + WebSocket - Both protocols in a single server instance.
  • Resource pooling - Reuses HTTP resources while keeping each request's context isolated.
  • Graceful shutdown - Cleanly closes active connections.
  • Streaming support - Efficient handling of large payloads.
  • Auto Content-Type detection - Automatically sets headers based on response type.
  • Modern ES modules - Native ESM support for Node.js 22 and 24.

Installation

# Install the package and its swm-uws runtime dependency
pnpm add @swarmmachina/swm-core

Migrating from 4.x

Version 5 removes the short request aliases that duplicated the explicit HttpContext API. Update calls as follows:

| 4.x alias | 5.x method | | ---------------------------------- | ------------------------ | | ctx.getHeader(name) / header() | ctx.getReqHeader(name) | | ctx.ip() | ctx.getIP() | | ctx.method() | ctx.getMethod() | | ctx.url() | ctx.getUrl() | | ctx.fullQuery() | ctx.getQuery() | | ctx.query(name) | ctx.getQuery(name) | | ctx.param(name) | ctx.getParameter(name) | | ctx.contentLength() | ctx.getContentLength() | | ctx.status(code) | ctx.setStatus(code) |

The package now publishes compiled files from dist/. Root imports remain unchanged; unsupported deep imports from @swarmmachina/swm-core/src/* must be replaced with exports from the package root.

Runtime requirements

The server depends on the native @swarmmachina/swm-uws addon, which ships platform-specific prebuilds:

  • Node.js 22 or 24 — other majors are rejected by the package engine constraint.
  • Linux x64 with glibc — use a bookworm/slim image rather than Alpine/musl.
  • Windows x64 and macOS arm64/x64 are supported.
  • Linux ARM64, Windows ARM64 and musl are not supported.
  • TLS and permessage-deflate are disabled in the native binding; terminate TLS before the application.

Quick Start

Basic HTTP Server

import Server from '@swarmmachina/swm-core'

const server = new Server({
  port: 3000,
  http: {
    onRequest: (ctx) => {
      return { message: 'Hello World' }
    }
  }
})

await server.listen()
console.log('Server listening on port 3000')

Keep a request handler synchronous when it has no real asynchronous work. An async handler always enters the Promise path, which preserves request state for use after the native callback and adds work even when the returned value is already available:

// Prefer this when the result is available immediately.
onRequest: (ctx) => ({ ok: true })

// Use async only when the handler actually awaits asynchronous work.
onRequest: async (ctx) => ({ user: await loadUser(ctx.getParameter('id')) })

Async Work Before Using the Request Body

The default request body mode is lazy. With the native uWS transport, a lazy body reader must be started before the first asynchronous operation. Enable the opt-in prefetch mode when a handler should be able to authenticate a user in a database before it asks for the body:

const server = new Server({
  http: {
    prefetch: true,

    // Body-size values are expressed in bytes. The omitted maxBodyBudget uses
    // the finite 256 MiB process-wide default in both lazy and prefetch modes.
    maxBodySize: 16 * 1024 * 1024, // 16 MiB per request

    onRequest: async (ctx) => {
      if (ctx.getMethod() !== 'post') {
        return null
      }

      const token = ctx.getReqHeader('authorization')
      const user = await findUserByToken(token)

      if (!user) {
        ctx.setStatus(401)
        return { error: 'Unauthorized' }
      }

      // Safe after await: raw bytes have been collected by the framework.
      // JSON parsing itself remains lazy and happens here.
      const data = await ctx.json()

      return { userId: user.id, data }
    }
  }
})

Declarative routes can enable or disable prefetch individually. An omitted route prefetch inherits http.prefetch:

const server = new Server({
  http: {
    routes: [
      {
        method: 'post',
        path: '/users',
        prefetch: true,
        before: async (ctx) => {
          const user = await findUserByToken(ctx.getReqHeader('authorization'))

          if (!user) {
            ctx.setStatus(401).send({ error: 'Unauthorized' })
            return
          }

          ctx.user = user
        },
        handler: async (ctx) => ({ user: ctx.user, data: await ctx.json() })
      }
    ]
  }
})

Prefetch starts collecting raw bytes before before and handler run. It does not eagerly decode text or parse JSON. The trade-off is additional work for requests that never use their body and memory proportional to concurrent request bodies. http.maxBodySize bounds one request; http.maxBodyBudget bounds aggregate retained and in-flight body storage. When the budget is omitted, swm-core applies the same finite 256 MiB safety-net in both lazy and prefetch modes. Use null only as an intentional opt-out.

When keeping the default lazy mode, start the reader manually before the first await. The body is then collected while the user is checked, but it is not parsed or used until the check succeeds:

http: {
  onRequest: async (ctx) => {
    console.log(`http ${ctx.getMethod()} ${ctx.getUrl()}`)

    if (ctx.getMethod() !== 'post') {
      return null // 204 No Content
    }

    // Register the body reader synchronously, before the first await.
    const dataPromise = ctx.text(1024 * 1024) // 1 MiB limit, expressed in bytes

    // The body can fail while the database call is still pending. Attach a
    // rejection handler immediately; awaiting the original promise below still
    // propagates the error when the user is allowed.
    void dataPromise.catch(() => {})

    const token = ctx.getReqHeader('authorization')
    const isBlocked = await checkUserInDatabase(token)

    if (isBlocked) {
      return null // 204 No Content
    }

    const data = JSON.parse(await dataPromise)
    console.log({ data })

    ctx.setStatus(404)
    return { status: false }
  }
}

Warning (lazy mode): call ctx.body(), ctx.buffer(), ctx.text(), or ctx.json() before awaiting any other asynchronous work. Otherwise uWS body events may arrive before the reader is registered, leaving its Promise pending. A route handler runs too late to guarantee this after an asynchronous before. Either enable prefetch, or start the reader synchronously inside before, before its first await, and retain the returned Promise for the handler.

Streaming: call ctx.bodyStream() before the first await. It cannot be combined with body prefetch. If a route has an asynchronous before, create the stream there before its first await and retain it for the handler.

HTTP Server with Routing (Traditional API)

import Server from '@swarmmachina/swm-core'

const server = new Server({
  port: 3000,
  http: {
    onRequest: async (ctx) => {
      // Simple routing
      if (ctx.getUrl() === '/' && ctx.getMethod() === 'get') {
        return { message: 'Welcome to the API' }
      }

      if (ctx.getUrl() === '/users' && ctx.getMethod() === 'get') {
        return { users: await getUsers() }
      }

      if (ctx.getUrl() === '/users' && ctx.getMethod() === 'post') {
        const data = await ctx.json()
        return await createUser(data)
      }

      // 404 Not Found
      ctx.setStatus(404)
      return { error: 'Not found' }
    },
    onError: (event, error) => {
      console.error(`HTTP Error [${event.method} ${event.url}]:`, error)
    }
  }
})

await server.listen()

HTTP Server with Declarative Routing (routes API)

The routes API provides method-specific routing, URL parameters and wildcard matching. Routes are registered directly with uWebSockets.js.

import Server from '@swarmmachina/swm-core'

const server = new Server({
  port: 3000,
  http: {
    routes: [
      {
        method: 'get',
        path: '/',
        handler: () => ({ message: 'Welcome to the API' })
      },
      {
        method: 'get',
        path: '/users',
        handler: async () => ({ users: await getUsers() })
      },
      {
        method: 'get',
        path: '/users/:id',
        handler: (ctx) => {
          const id = ctx.getParameter('id') // or ctx.getParameter(0)
          return getUserById(id)
        }
      },
      {
        method: 'post',
        path: '/users',
        handler: async (ctx) => {
          const data = await ctx.json()
          return await createUser(data)
        }
      },
      {
        method: 'put',
        path: '/users/:id',
        handler: async (ctx) => {
          const id = ctx.getParameter('id')
          const data = await ctx.json()
          return await updateUser(id, data)
        }
      },
      {
        method: 'delete',
        path: '/users/:id',
        handler: (ctx) => {
          const id = ctx.getParameter('id')
          return deleteUser(id)
        }
      }
    ],
    onError: (event, error) => {
      console.error(`HTTP Error [${event.method} ${event.url}]:`, error)
    }
  }
})

await server.listen()

Benefits of Declarative Routing:

  • Direct uWS registration - Routes are registered with the native engine
  • URL Parameters - Built-in support for :param syntax
  • Cleaner Code - Declarative route definitions
  • Method-specific - Automatic HTTP method routing
  • Wildcard catch-all - A { method: 'any', path: '/*' } route matches anything not matched by a more specific route (useful as a 404 handler or static-file fallback). Specific routes always win over /*.

WebSocket Server

import Server from '@swarmmachina/swm-core'

const server = new Server({
  port: 3000,
  http: {
    onRequest: (ctx) => {
      return { message: 'HTTP endpoint' }
    }
  },
  ws: {
    maxPayloadLength: 1024 * 32, // 32 KiB per incoming message, in bytes
    maxBackpressure: 1024 * 64, // 64 KiB per slow WebSocket, in bytes
    closeOnBackpressureLimit: true,
    idleTimeoutSec: 30,
    onUpgrade: (meta) => ({ ip: meta.ip() }),
    onOpen: (ctx) => {
      console.log('Client connected:', ctx.data.ip)
      ctx.send('Welcome!')
    },
    onMessage: (ctx, message, isBinary) => {
      const text = Buffer.from(message).toString()
      console.log('Received:', text)
      ctx.send(`Echo: ${text}`)
    },
    onDropped: (ctx, message, isBinary) => {
      console.warn('Dropped outgoing message for slow client:', message.byteLength, isBinary)
    },
    onClose: (ctx, code, message) => {
      console.log('Client disconnected:', ctx.data.ip)
    },
    onError: (ctx, error) => {
      console.error('WebSocket error:', error)
    }
  }
})

await server.listen()

API Documentation

Server Constructor

new Server(options)

Options:

| Option | Type | Default | Description | | --------------- | --------------- | ------------- | ----------------------------------------------- | | http | Object/null | null | HTTP application configuration (see below) | | ws | Object/null | null | WebSocket application configuration (see below) | | transport | Object/null | null | Explicit native HTTP parser/connection policy | | onServerError | Function | () => {} | Post-listen transport error handler | | host | String | '127.0.0.1' | Address or hostname to bind | | port | Number | 6000 | Server port (1-65535) |

At least one of http or ws must be an object. A nullish value disables that application layer. http: null with a configured ws still creates the minimal HTTP transport required for WebSocket upgrades; ordinary HTTP requests receive a fixed 404 without allocating an HttpContext.

| http | ws | Result | | ------ | ------ | --------------------------------------------- | | object | object | HTTP + WebSocket | | object | null | HTTP only | | null | object | WebSocket with minimal HTTP upgrade transport | | null | null | Configuration error |

HTTP Options (http object):

| Option | Type | Default | Description | | ------------------- | -------------------------- | ------------- | -------------------------------------------------------------------- | | maxBodySize | Number | 1048576 | Maximum HTTP request body size in bytes (1 MiB default, 64 MiB max). | | maxStreamBodySize | Number | maxBodySize | Maximum bodyStream() size in bytes (non-negative safe integer). | | maxBodyBudget | Number/null | 268435456 | Aggregate retained/in-flight body-memory budget in bytes (256 MiB). | | requestTimeoutMs | Number | 30000 | Async handler timeout in ms (100-300000); explicit 0 disables it. | | prefetch | Boolean | false | Collect request bodies before user handlers run. | | prefetchHeaders | false/'all'/String[] | omitted | Retain selected request headers before handlers run. | | onRequest | Function | default 404 | Universal request handler (ctx) => any | | routes | Array | default 404 | Declarative route definitions | | errorDelivery | Object | see below | Bounded async error-delivery policy | | onError | Function | omitted | Request error handler (event, error, { signal }) => any |

http.onRequest and http.routes are mutually exclusive. http: {} enables HTTP with a deterministic default 404 response.

prefetchHeaders is independent from body prefetch. A string list is normalized once and retains only those fields in native-owned storage. false explicitly retains none and 'all' retains every field. When the option is omitted, no headers are retained automatically. Fields remain readable synchronously and successful reads populate ctx.headers, but they are unavailable after the native callback returns unless read and cached before detach. Use a selective list for the usual async case and 'all' only when the complete set is actually required after an await.

ctx.headers is a stable view of headers already retained or read. Accessing it does not enumerate the native request: without header prefetch it starts empty, and each successful ctx.getReqHeader(name) adds that field. ctx.getHeaders() collects the complete set and fills the same view.

new Server({
  http: {
    prefetchHeaders: ['authorization', 'traceparent'],
    onRequest: async (ctx) => {
      await authorize(ctx.headers.authorization)
      return { trace: ctx.headers.traceparent }
    }
  }
})

The native binding capability is checked during listen(). Configuration fails instead of silently falling back to a full JavaScript header scan.

Bounded asynchronous error delivery

http.onError receives an immutable, body-free event rather than the request's HttpContext. The request context and its body budget are released independently of remote logging, tracing, or metrics delivery.

This is the 5.1 contract and intentionally replaces the earlier onError(ctx, error) signature. Migrate context reads to the event fields and configure explicit errorDelivery.headers, errorDelivery.query, or errorDelivery.includeIp selection when that metadata is required.

const server = new Server({
  http: {
    onRequest,
    errorDelivery: {
      concurrency: 4,
      queueLimit: 256,
      timeoutMs: 5_000,
      headers: ['x-request-id', 'traceparent'],
      query: ['requestId', 'operation'],
      includeIp: false
    },
    async onError(event, error, { signal }) {
      await fetch(logEndpoint, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ event, error: error.message }),
        signal
      })
    }
  }
})

Defaults are concurrency: 4, queueLimit: 256, timeoutMs: 5000, no headers, no query parameters, and no IP lookup. Selected headers are retained for error reporting without widening the handler's prefetchHeaders policy. errorDelivery.query is a case-sensitive allowlist; only present selected values appear in event.query. event.url remains the path without the query string. The allowlist accepts at most 100 unique names of 1–256 characters. Request bodies, native request/response handles, and mutable context state are never copied.

The delivery AbortSignal fires at the deadline. Arbitrary Promises cannot be force-cancelled in JavaScript, so a timed-out attempt keeps its dispatcher slot until it actually settles. This prevents code that ignores cancellation from defeating the concurrency bound; once the bounded queue is full, later events are counted as dropped. Use APIs such as fetch that honor the signal. Inspect server.httpErrorDeliveryStats for inFlight, queued, completed, timedOut, aborted, rejected, dropped, and oldestInFlightMs.

Graceful shutdown stops accepting new error events after request/WebSocket lifecycle drain, then waits for accepted delivery work. The server-wide shutdown deadline drops queued events and aborts active delivery signals. A callback that ignores its signal cannot be force-cancelled; shutdown still resolves at its deadline and the unsettled callback remains visible in delivery stats.

Native HTTP transport options (transport object):

| Option | Type | Description | | ------------------------ | --------------- | ------------------------------------------------ | | maxHeaderSize | Number | Request line plus all request headers, in bytes. | | maxHeaderCount | Number | Maximum number of request header fields. | | headersTimeoutMs | Number | Deadline for receiving a complete request head. | | keepAliveTimeoutMs | Number | Idle wait for the next keep-alive request. | | bodyIdleTimeoutMs | Number | Idle timeout while receiving request body bytes. | | minBodyRateBytesPerSec | Number/null | Minimum body receive rate; null disables it. | | responseWriteTimeoutMs | Number | Timeout for outbound backpressure stalls. |

Only explicitly configured values are passed to swm-uws; binding defaults remain binding-owned. Transport values are positive safe integers, timeout values are capped at 300000 ms, and maxHeaderCount is capped at the native capacity of 100.

http.maxBodySize and ws.maxPayloadLength are independent. Changing one does not change the limit for the other protocol. ws.maxPayloadLength must be at least one byte.

Configuration examples

The following configurations cover every public Server option. Choose either http.onRequest or http.routes; they cannot be used together.

HTTP, transport policy, and root options

import Server from '@swarmmachina/swm-core'

const server = new Server({
  host: '127.0.0.1',
  port: 3000,
  onServerError: (error) => {
    console.error('Transport error:', error)
  },
  transport: {
    maxHeaderSize: 32 * 1024,
    maxHeaderCount: 64,
    headersTimeoutMs: 10_000,
    keepAliveTimeoutMs: 60_000,
    bodyIdleTimeoutMs: 15_000,
    minBodyRateBytesPerSec: 1024,
    responseWriteTimeoutMs: 30_000
  },
  http: {
    prefetch: true,
    prefetchHeaders: ['authorization', 'traceparent'],
    maxBodySize: 2 * 1024 * 1024,
    maxBodyBudget: 64 * 1024 * 1024,
    requestTimeoutMs: 15_000,
    errorDelivery: {
      concurrency: 4,
      queueLimit: 256,
      timeoutMs: 5_000,
      headers: ['x-request-id', 'traceparent'],
      query: ['requestId']
    },
    onRequest: async (ctx) => {
      const body = await ctx.json()

      return {
        trace: ctx.headers.traceparent,
        body
      }
    },
    onError: (event, error) => {
      console.error(`Request failed: ${event.method} ${event.url}`, error)
    }
  },
  ws: null
})

await server.listen()

Set minBodyRateBytesPerSec: null when the trusted ingress already enforces a body receive-rate policy. Omit transport to use the binding defaults.

Declarative routes

import Server from '@swarmmachina/swm-core'

// db is the application's database client.
const routeServer = new Server({
  http: {
    maxBodySize: 2 * 1024 * 1024,
    maxStreamBodySize: 16 * 1024 * 1024,
    maxBodyBudget: 32 * 1024 * 1024,
    requestTimeoutMs: 10_000,
    routes: [
      {
        method: 'post',
        path: '/accounts/:accountId',
        prefetch: true,
        prefetchHeaders: ['authorization'],
        maxBodySize: 256 * 1024,
        before: [
          async (ctx) => {
            const token = ctx.headers.authorization

            if (!token) {
              return ctx.setStatus(401).send({ error: 'Unauthorized' })
            }

            const user = await db.users.findByAccessToken(token)
            const accountId = ctx.getParameter('accountId')
            const canWrite = user && (await db.accounts.canWrite(user.id, accountId))

            if (!canWrite) {
              return ctx.setStatus(403).send({ error: 'Forbidden' })
            }
          }
        ],
        handler: async (ctx) => {
          const update = await ctx.json()

          return db.accounts.update(ctx.getParameter('accountId'), update)
        }
      },
      {
        method: 'get',
        path: '/health',
        prefetch: false,
        prefetchHeaders: false,
        maxBodySize: 0,
        handler: () => ({ ok: true })
      }
    ],
    onError: (event, error) => {
      console.error(`Route failed: ${event.url}`, error)
    }
  },
  ws: null
})

await routeServer.listen()

before may be one function or an ordered array. A route limit may lower, but cannot exceed, http.maxBodySize.

WebSocket options and callbacks

import Server from '@swarmmachina/swm-core'

const wsServer = new Server({
  host: '127.0.0.1',
  port: 3001,
  http: null,
  ws: {
    maxPayloadLength: 64 * 1024,
    maxBackpressure: 128 * 1024,
    closeOnBackpressureLimit: true,
    idleTimeoutSec: 30,
    upgradeTimeoutMs: 5_000,
    prefetchHeaders: ['authorization'],
    onUpgrade: async (meta) => {
      const token = meta.headers.authorization

      if (!token) {
        return null
      }

      const user = await authenticate(token)
      return user ? { userId: user.id } : null
    },
    selectProtocol: (requested) => (requested.includes('chat.v1') ? 'chat.v1' : undefined),
    connectionKey: (ctx) => ctx.data.userId,
    onOpen: (ctx) => {
      ctx.subscribe(`user:${ctx.key}`)
    },
    onMessage: (ctx, message, isBinary) => {
      if (!isBinary) {
        ctx.publish('chat', ctx.decode(message))
      }
    },
    onDropped: (ctx, message, isBinary) => {
      console.warn('Dropped message:', ctx.key, message.byteLength, isBinary)
    },
    onDrain: (ctx) => {
      console.log('Writable again:', ctx.key)
    },
    onSubscription: (ctx, topic, newCount, oldCount) => {
      console.log(ctx.key, ctx.decode(topic), oldCount, '->', newCount)
    },
    onClose: (ctx, code, reason) => {
      console.log('Closed:', ctx.key, code, ctx.decode(reason))
    },
    onError: (ctx, error) => {
      console.error('WebSocket error:', ctx?.key, error)
    }
  }
})

await wsServer.listen()

onUpgrade is required and must return an object to accept or null to reject the upgrade. connectionKey is optional; return null or undefined when a connection must not be addressable through server.sendTo().

JavaScript configuration typing

IDE types load automatically from the package root import. For a configuration declared separately, use defineConfig() to retain completion without JSDoc:

import Server, { defineConfig } from '@swarmmachina/swm-core'

const options = defineConfig({
  http: { maxBodyBudget: 256 * 1024 * 1024 }
})

const server = new Server(options)

defineConfig() returns the same object; constructor validation is unchanged. Global declarations and a compilerOptions.types entry are not required. Avoid deep imports from @swarmmachina/swm-core/src/*.

For an existing JSDoc codebase that uses Swm.*, opt in to the type-only namespace once per project or entry point:

/// <reference types="@swarmmachina/swm-core/global" />

/**
 * @typedef {object} ContextState
 * @property {?Swm.HttpContext} req
 * @property {?Swm.HttpContext} res
 */

The namespace contains types only. It does not create or declare a runtime globalThis.Swm value.

HTTP body memory limits

All values are byte counts. maxBodySize defaults to 1 MiB and cannot exceed 64 MiB; it limits one request. maxBodyBudget defaults to 256 MiB and limits the aggregate retained and in-flight body memory across requests.

maxStreamBodySize defaults to maxBodySize and limits ctx.bodyStream(). It accepts any non-negative safe integer.

const server = new Server({
  http: {
    maxBodySize: 16 * 1024 * 1024,
    maxStreamBodySize: 512 * 1024 * 1024,
    maxBodyBudget: 256 * 1024 * 1024,
    onRequest
  }
})

A valid Content-Length above maxBodySize is rejected before allocation. Exceeding maxBodySize returns 413; exhausting maxBodyBudget returns 503. maxBodyBudget: 0 permits only empty bodies, while null disables the aggregate limit. The budget accounts for bodies only, not total process RSS.

Body accessors materialize a Buffer; use a streaming or object-storage upload flow for larger uploads. requestTimeoutMs defaults to 30 seconds. Before a response starts it releases the body reservation and returns 408. After an early response it only releases the retained body bytes: an answered request is not reported as an error, receives no second response, and stays active until its handler settles. It does not cancel application work.

ctx.bodyStream(maxSize) does not retain the complete body or use maxBodyBudget. Its per-call and route limits can only lower http.maxStreamBodySize. The normalized values are available on server.effectiveConfig.http.

Route Definition (for routes array):

| Property | Type | Description | | ------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | | method | String | HTTP method: 'get', 'post', 'put', 'delete'/'del', 'patch', 'options', 'head', 'any' | | path | String | URL path pattern. Supports :param segments and a /* wildcard catch-all | | handler | Function | Handler function (ctx) => any \| Promise<any> | | before | Function/Array | Optional. One function or an array, run before handler (see Route before hooks) | | prefetch | Boolean | Optional route body-prefetch override. Omitted inherits http.prefetch. | | prefetchHeaders | false/'all'/String[] | Optional header-retention override. Omitted inherits http.prefetchHeaders. | | maxBodySize | Number | Optional route body limit in bytes; cannot exceed http.maxBodySize. | | maxStreamBodySize | Number | Optional bodyStream() limit; defaults to explicit route maxBodySize, otherwise inherits http.maxStreamBodySize. |

WebSocket Options (ws object):

| Option | Type | Default | Description | | -------------------------- | -------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | maxPayloadLength | Number | 1048576 | Maximum bytes in one incoming WebSocket message (1 MiB default, 64 MiB max). | | maxBackpressure | Number | 65536 | Maximum permitted outgoing backpressure in bytes per WebSocket. | | closeOnBackpressureLimit | Boolean | true | Close a slow WebSocket when an outgoing message is dropped at the backpressure limit. | | idleTimeoutSec | Number | 15 | Idle timeout in seconds (integer: 8-960). | | upgradeTimeoutMs | Number | 10000 | Deadline for an asynchronous onUpgrade decision in milliseconds (0-300000). 0 schedules a zero-delay timeout; it does not disable the deadline. | | prefetchHeaders | false/'all'/String[] | omitted | Retain selected HTTP upgrade headers for asynchronous onUpgrade authorization. | | onOpen | Function | (ctx) => {} | Called when client connects. | | onMessage | Function | (ctx, message, isBinary) => {} | Called when message received. | | onDropped | Function | (ctx, message, isBinary) => {} | Called when an outgoing message is dropped because the connection exceeded its backpressure limit. Copy message synchronously if it is needed after the callback returns or across an await. | | onClose | Function | (ctx, code, message) => {} | Called when client disconnects. message is copied and remains readable across await. | | onDrain | Function | (ctx) => {} | Called when socket is writable again. | | onError | Function | (ctx, error) => {} | Called on WebSocket error. | | onUpgrade | Function | required | Authorize the upgrade. Return null to reject with 403, or a flat object to accept; that exact object becomes ctx.data. Async handlers can safely use meta after an await. | | selectProtocol | Function | undefined | Optional synchronous (requested, userData) => string \| undefined subprotocol selector. The returned token must be present in the client-requested list. | | onSubscription | Function | (ctx, topic, newCount, oldCount) => {} | Called on topic subscription change. | | connectionKey | Function | undefined | Opt-in. (ctx) => string \| number \| null. Derive a stable key (e.g. a user id) so the connection can be addressed via server.sendTo(). Computed once in onOpen; return nullish to skip. Unset = no registry (zero overhead). |

ws.onUpgrade is required. Use onUpgrade: () => ({}) only for intentionally anonymous endpoints; use ws: null to disable WebSocket.

ws.prefetchHeaders runs on the HTTP request before onUpgrade is called, so the selected fields remain available from meta.headers and meta.getHeader() after an await. Without prefetch, meta.headers starts empty and is populated by successful meta.getHeader(name) calls without enumerating the native request. Those reads must happen synchronously to remain available after detach. Fields outside a selective list are available only during the synchronous part of onUpgrade. The protocol-required sec-websocket-key, sec-websocket-protocol, and sec-websocket-extensions fields are captured directly by swm-core and do not need to be included in the list.

WebSocket payload and backpressure limits

WebSocket input uses maxPayloadLength, not HTTP maxBodySize. All limits are byte counts:

const server = new Server({
  http: null,
  ws: {
    maxPayloadLength: 1024 * 32, // 32 KiB per incoming message
    maxBackpressure: 1024 * 64, // 64 KiB per slow connection
    closeOnBackpressureLimit: true,
    onUpgrade: () => ({}) // Explicitly allow anonymous connections.
  }
})

maxPayloadLength applies to each reconstructed text or binary message; oversized messages close before onMessage. maxBackpressure applies per socket. send() and sendTo() report backpressure or dropped messages; onDropped observes drops and onDrain signals recovery. With the default closeOnBackpressureLimit: true, a dropped message closes the slow socket. Set it to false only when dropping messages is acceptable.

selectProtocol runs synchronously after onUpgrade accepts. Return one of the requested tokens; returning undefined (or omitting the selector) negotiates no subprotocol.

const server = new Server({
  http: null,
  ws: {
    onUpgrade: async (meta) => {
      const token = meta.getHeader('authorization')
      const user = await authenticate(token)

      return user ? { userId: user.id, role: user.role } : null
    },
    selectProtocol: (requested, userData) => {
      if (userData.role === 'admin' && requested.includes('admin.v1')) {
        return 'admin.v1'
      }

      return requested.includes('chat.v1') ? 'chat.v1' : undefined
    }
  }
})

// Browser/client: offer the protocols the client supports.
const socket = new WebSocket('wss://example.com', ['chat.v1', 'admin.v1'])

The negotiated value is available as socket.protocol. Async upgrades capture URL, IP, headers, query, and the /* parameter before the native request expires, so UpgradeMeta remains safe after an await. This capture is unrelated to http.prefetch.

Server Methods

server.listen()

Start the server and begin accepting connections.

await server.listen()

server.shutdown([timeout])

Gracefully shutdown the server. Waits for active connections to finish.

server.shutdown(10000) // 10 second timeout

server.close()

Forcefully close the server immediately.

server.close()

server.publish(topic, message, [isBinary])

Publish message to all WebSocket clients subscribed to a topic.

server.publish('news', 'Breaking news!', false)

Returns: boolean - Success status

server.getSubscribersCount(topic)

Get number of subscribers for a topic.

const count = server.getSubscribersCount('news')

Returns: number - Subscriber count

server.sendTo(key, message, [isBinary])

Send a message directly to the single connection registered under key (the value returned from ws.connectionKey). For 1:1 messaging where topic pub/sub would be overkill. Requires ws.connectionKey to be configured.

server.sendTo('user-42', 'private message')

Returns: boolean - true if a live connection was found and the transport did not report the message as dropped; false when the key is unknown or the backpressure limit was exceeded.

Keys are matched with strict Map identity: 42 and '42' are different keys. Pick one type for connectionKey return values and sendTo() arguments.

server.closeConnection(key, [code], [reason])

Gracefully close the WebSocket registered under key. A close frame is sent, so the client receives the status code and reason. The connection is removed from the addressable registry before its onClose callback runs.

server.closeConnection('user-42', 1008, 'policy violation')

Returns: boolean - true when a live registered connection was found and closing was initiated; false for an unknown key.

The close reason is limited to 123 UTF-8 bytes. Reserved wire codes such as 1005, 1006, and 1015 are rejected. Use 1008 for policy violations, 1013 for temporary overload, or an application code in the 3000-4999 range.

server.terminateConnection(key)

Immediately force-close the WebSocket registered under key without sending a close frame. Use this for abusive or unresponsive peers when graceful shutdown is not appropriate.

server.terminateConnection('user-42')

Returns: boolean - true when a live registered connection was found and terminated; false for an unknown key. The client observes an abnormal close (typically code 1006) and receives no reason.

server.hasConnection(key) / server.getConnection(key) / server.connectionCount

Inspect the connection registry. hasConnection returns a boolean; getConnection returns the raw WebSocket handle (or undefined); connectionCount is the number of registered connections. The package types only guarantee the methods declared by RawWebSocket; the native handle may provide additional uWS methods.

if (server.hasConnection('user-42')) {
  /* ... */
}
const raw = server.getConnection('user-42') // raw uWS handle or undefined
console.log(server.connectionCount)

HttpContext API

The ctx object passed to http.onRequest and route handlers:

Request metadata uses explicit get* readers. ctx.getReqHeader() reads an incoming request header; ctx.setHeader() stages an outgoing response header.

Properties

| Property | Type | Description | | ------------- | ------------------------ | ---------------------------------------------------------- | | ctx.headers | Record<string, string> | Stable view of prefetched or already-read request headers. | | ctx.replied | Boolean | Whether response has been sent | | ctx.aborted | Boolean | Whether request was aborted |

Methods

ctx.getMethod()

Get request lowercased method.

const method = ctx.getMethod()

Returns: string

ctx.getUrl()

Get request url.

const url = ctx.getUrl()

Returns: string

ctx.getIP()

Get network source metadata. A valid PROXY Protocol source address is preferred; otherwise the TCP peer address is returned. IPv4-mapped IPv6 is normalized and the value is cached only for the current context generation. X-Forwarded-For is not automatically trusted or parsed.

const ip = ctx.getIP()

Returns: string

ctx.getIP() is not authenticated identity. Accept PROXY Protocol only on a listener reachable exclusively through a trusted ingress or load balancer; otherwise a public client can spoof the source address in its PROXY header. Authorization, rate limiting, and audit attribution need an explicit proxy trust policy.

Recommended:
Internet -> trusted ingress/load balancer -> private PROXY-enabled swm-core listener
Unsafe:
Internet -> public listener accepting arbitrary PROXY headers

The same trust boundary applies to ws.onUpgrade(meta).ip().

ctx.getQuery(name)

Get query parameter value.

const page = ctx.getQuery('page') // ?page=1

Returns: string | undefined - undefined when the parameter is absent

ctx.getQuery()

Get full raw query string.

const q = ctx.getQuery() // page=1&limit=20

Returns: string

ctx.getParameter(indexOrName)

Get URL parameter by index or name (for pattern matching in the routes API).

// By index
const id = ctx.getParameter(0) // First parameter

// By name (routes API only)
const id = ctx.getParameter('id') // /users/:id

// Multiple parameters
const userId = ctx.getParameter('userId') // /users/:userId/posts/:postId
const postId = ctx.getParameter('postId')

Returns: string | undefined - undefined when the parameter is absent

ctx.getReqHeader(name)

Get request header value.

const auth = ctx.getReqHeader('authorization')

Header names are case-insensitive. A missing header returns an empty string.

Returns: string

ctx.getHeaders()

Get a shallow copy of all incoming request headers. Names are lowercase and the returned object has a null prototype. Mutating it does not affect the context's internal request cache. This method always requests the complete header set; it never returns only the selective ctx.headers view. As a side effect, it fills the stable ctx.headers object with the complete set.

With selective or disabled header prefetch, call getHeaders() synchronously before the handler yields. After an async boundary, the complete set is available only if it was already collected or prefetchHeaders: 'all' was configured; otherwise the method throws with code REQUEST_HEADERS_NOT_RETAINED.

const headers = ctx.getHeaders()
const auth = headers.authorization

Returns: Record<string, string>

ctx.getContentLength()

Get a valid non-negative Content-Length value. Returns null when the header is absent or invalid.

const length = ctx.getContentLength()

Returns: number | null

ctx.body([maxSize])

Read request body as Buffer.

const buffer = await ctx.body()
const buffer = await ctx.body(5 * 1024 * 1024) // 5 MiB, expressed in bytes

Returns: Promise<Buffer>

In prefetch mode, http.maxBodySize is the hard collection limit. A smaller per-call maxSize is still enforced when the accessor resolves; a larger value cannot raise the server limit. If http.maxBodyBudget is enabled, the same aggregate budget also applies to manually started lazy readers.

Every maxSize argument is a non-negative safe integer byte count. The first body accessor (or prefetch) fixes the collection limit; later calls reuse one collector. A smaller later value checks the same materialized bytes, while a larger value does not restart or expand an in-flight or failed collection.

ctx.bodyStream([maxSize])

Stream the request body as a Node.js Readable of owned Buffer chunks with backpressure. Set prefetch: false and call it synchronously before any hook or handler yields.

import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'

const upload = ctx.bodyStream()
await pipeline(upload, createWriteStream('./uploads/video.mp4'))

Returns: RequestBodyStream with readonly contentLength: the declared length in bytes, or null.

maxSize is a non-negative safe integer in bytes and can only narrow route and http.maxStreamBodySize limits. Streaming bypasses http.maxBodyBudget; backpressure bounds the stream queue, not total process memory.

Do not combine it with other body readers. Creation errors throw synchronously; stream errors reject pipeline() or asynchronous iteration. If you stop reading early, call upload.destroy() before returning.

ctx.buffer([maxSize])

Alias for ctx.body([maxSize]).

const buffer = await ctx.buffer()

Returns: Promise<Buffer>

ctx.json([maxSize])

Parse request body as JSON.

const data = await ctx.json()

Returns: Promise<any>

ctx.text([maxSize])

Read request body as text.

const text = await ctx.text()

Returns: Promise<string>

ctx.setStatus(code)

Set response status code. Returns context for chaining.

ctx.setStatus(201).send({ created: true })

Returns: HttpContext

ctx.setHeader(key, value)

Set or replace a staged response header. Header names are case-insensitive HTTP tokens; invalid names and CR/LF in values throw before crossing into the native transport. Repeated setHeader() calls replace previously staged values for the same header. An array emits one header field per item in order, which is useful for repeatable fields such as Set-Cookie; matching node:http, Cookie array values are joined with ; . Null or undefined scalar values are silently ignored.

ctx.setHeader('x-header-any', 'string-value').setStatus(201).send({ created: true })

ctx.setHeader('set-cookie', ['access=...; Path=/; HttpOnly', 'refresh=...; Path=/refresh; HttpOnly'])

Returns: HttpContext

ctx.appendHeader(key, value)

Append another staged response header line without replacing existing values. Useful for repeated headers such as Set-Cookie. Null or undefined values are silently ignored.

ctx.appendHeader('set-cookie', 'access=...; Path=/; HttpOnly')
ctx.appendHeader('set-cookie', 'refresh=...; Path=/refresh; HttpOnly')

Returns: HttpContext

ctx.setHeaders(headers)

Set multiple response headers at once. Equivalent to calling setHeader() for each key. Header values may be strings or arrays of strings. A block returned by prepareHeaders() is also accepted.

ctx.setHeaders({
  'x-request-id': '123',
  'cache-control': 'no-cache',
  'set-cookie': ['a=1; Path=/', 'b=2; Path=/refresh']
})
ctx.flushHeaders([headers])

Flush all staged headers (and optionally stage additional ones) to the underlying response. Called automatically by reply() and startStreaming() — only needed for advanced use cases.

ctx.flushHeaders({ 'x-extra': 'value' })
ctx.send(data)

Send response with automatic content-type detection.

ctx.send({ message: 'OK' }) // application/json
ctx.send('Hello') // text/plain
ctx.send(Buffer.from('data')) // application/octet-stream
ctx.send(null) // 204 No Content

Supported types: Objects and arrays (JSON), strings and other primitive values (text), Buffer/ArrayBuffer/typed-array views (binary), and nullish values (204 No Content).

ctx.sendJson(data, [status])

Send a JSON response with explicit status code. Defaults to 200.

ctx.sendJson({ users: [] })
ctx.sendJson({ error: 'Not found' }, 404)
ctx.sendText(text, [status])

Send a plain text response with explicit status code. Defaults to 200.

ctx.sendText('OK')
ctx.sendText('Created', 201)
ctx.sendBuffer(buffer, [status])

Send a binary response with explicit status code. Defaults to 200.

ctx.sendBuffer(Buffer.from('data'))
ctx.sendBuffer(imageBuffer, 201)
ctx.sendError(error)

Send an error response. If error.status is a finite number, uses that as the HTTP status with error.message as the body. Otherwise responds with 500 Internal Server Error.

ctx.sendError(new Error('Something broke'))

// With custom status
const err = new Error('Not found')
err.status = 404
ctx.sendError(err)
ctx.reply(status, headers, body)

Send response with full control over status, headers, and body. Header values may be strings or arrays of strings. Array values are written as separate header lines.

ctx.reply(
  200,
  {
    'content-type': 'application/json',
    'set-cookie': ['a=1; Path=/', 'b=2; Path=/refresh']
  },
  '{"ok":true}'
)
ctx.replyAndClose([status], [headers], [body])

Send a complete HTTP response, then close the underlying connection instead of allowing keep-alive reuse. The response is flushed before the transport closes.

ctx.replyAndClose(403, { 'content-type': 'text/plain' }, 'Forbidden')

Use this for rejected requests where the client should still receive a valid HTTP response. Closing the connection does not prevent the client from opening a new one; authentication, bans, and rate limits remain separate policies.

ctx.terminate()

Immediately force-close the underlying HTTP connection. No response delivery is guaranteed.

ctx.terminate()

Use this for protocol abuse or peers that should not consume more transport resources. The context follows the normal aborted-request cleanup path.

ctx.stream(readable, [status], [headers])

Stream a readable stream to the response.

import fs from 'fs'

const stream = fs.createReadStream('./large-file.mp4')
await ctx.stream(stream, 200, { 'content-type': 'video/mp4' })

Returns: Promise<void>

ctx.startStreaming([status], [headers])

Start streaming response manually (for advanced use cases). Header values may be strings or arrays of strings.

ctx.startStreaming(200, {
  'content-type': 'text/plain',
  'set-cookie': ['a=1; Path=/', 'b=2; Path=/refresh']
})
ctx.write(chunk)

Write chunk to streaming response.

const ok = ctx.write('chunk of data')
if (!ok) {
  // backpressure, pause writing
}

Returns: boolean - false if backpressure detected

ctx.end([chunk])

End streaming response.

ctx.end('final chunk')
ctx.onWritable(callback)

Register callback to be called when the response stream becomes writable again (for backpressure handling). The callback receives the current write offset.

ctx.onWritable((offset) => {
  // Socket is writable again, can resume writing
  // offset is the current write offset
})

Returns: void

ctx.tryEnd(chunk, totalSize)

Try to finish a streaming response whose total byte size is known. totalSize is required and is passed to the transport's tryEnd implementation.

const finalChunk = Buffer.from('final chunk')
const totalSize = ctx.getWriteOffset() + finalChunk.byteLength
const [ok, done] = ctx.tryEnd(finalChunk, totalSize)
if (done) {
  // Response is complete
}

Returns: [boolean, boolean] - [ok, done] where ok indicates success and done indicates completion

ctx.getWriteOffset()

Get the current write offset (useful for tryEnd and backpressure handling).

const offset = ctx.getWriteOffset()

Returns: number - Current write offset

WSContext API

The ctx object passed to WebSocket handlers:

Properties

| Property | Type | Description | | ---------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ctx.data | Object | Exact user data object returned by onUpgrade (ctx.data === dataFromOnUpgrade) | | ctx.ws | RawWebSocket | Raw uWS WebSocket handle. Identity-stable for the connection; the exported RawWebSocket type documents the supported surface (see Context lifetime & pooling) | | ctx.key | string/number | Key this connection is registered under (from connectionKey), or null. Read-only. |

Methods

ctx.send(data, [isBinary])

Send message to this client.

ctx.send('Hello client!')
ctx.send(Buffer.from([1, 2, 3]), true) // binary

Returns: number — send status mirroring uWS: 1 success, 0 backpressure (queued behind backpressure), 2 dropped (not sent — backpressure limit exceeded). Check it to react to backpressure.

ctx.end([code], [reason])

Close this WebSocket connection.

ctx.end(1000, 'Goodbye')

The reason must fit in 123 UTF-8 bytes. Reserved wire codes cannot be sent.

ctx.terminate()

Immediately force-close this WebSocket without sending a close frame. The peer observes an abnormal close and receives no close reason.

ctx.terminate()
ctx.subscribe(topic)

Subscribe this client to a topic.

ctx.subscribe('news')

Returns: boolean - Success status

ctx.unsubscribe(topic)

Unsubscribe this client from a topic.

ctx.unsubscribe('news')

Returns: boolean - Success status

ctx.publish(topic, message, [isBinary])

Publish message to all subscribers of a topic.

ctx.publish('news', 'Breaking news!')

Returns: boolean - Success status

ctx.decode(message)

Decode a received binary message to a UTF-8 string — an opt-in convenience for the common Buffer.from(message).toString(). Not called automatically, so handlers that work with raw bytes pay nothing.

onMessage: (ctx, message) => {
  const text = ctx.decode(message)
}

Returns: string

Call it synchronously inside the handler: uWS neuters the message ArrayBuffer at the first await/return, so decoding it later throws. Unlike ctx, the message argument does not survive await — decode first, then await.

WSContext lifetime & pooling

HttpContext is allocated fresh per request; its internal body and response resources are pooled. Hooks and the handler receive the same context, including across await. Application properties such as ctx.user, symbols and non-enumerable or non-configurable properties belong only to that request. Freezing the context is supported. Cleanup detaches its internal resources without enumerating or deleting application properties.

The context remains active until the returned handler Promise settles and the response lifecycle finishes. After cleanup, its HTTP methods throw HTTP context is no longer active; an old reference cannot act on another request. Application properties remain accessible while the application retains the context, so drop those references when background work finishes. Raw native request/response handles retain their own, shorter transport lifetimes.

Context identity across requests, enumeration of internal fields, and replacing framework methods or lifecycle fields are not supported extension points. Add application properties under separate names instead.

WSContext is allocated fresh per connection and never reused.

| Context | Allocated per… | Valid for… | Safe to retain? | | ---------------- | ------------------------- | ----------------------------- | -------------------------------------------- | | HttpContext | request (never reused) | request/response lifecycle | Data only after cleanup; HTTP methods expire | | HttpErrorEvent | reported HTTP error | independent immutable value | Yes — it contains bounded metadata, no body | | WSContext | connection (never reused) | the whole connection lifetime | Yes, for the connection; not past onClose |

  • One instance per connection. The same WSContext is passed to every callback of a given connection (onOpen, onMessage, onClose, onDrain, onSubscription). Instances are not shared between connections, so ctx never silently switches sockets — you can hold it across an await and use it as a stable per-connection identity.
  • Don't use it past onClose. After close the instance is cleared, so a retained reference fails loudly (ws is null) instead of acting on a stale socket. Don't stash ctx in a structure that outlives the connection.
  • Address other connections by key, not by ctx. To reach a different connection (from another handler, a timer, an HTTP route), set connectionKey and use server.sendTo(), or keep the raw ctx.ws handle yourself. See Direct messaging between connections.

Examples

REST API with Error Handling

import Server from '@swarmmachina/swm-core'

const users = new Map()

const server = new Server({
  port: 3000,
  http: {
    onRequest: async (ctx) => {
      try {
        // GET /users
        if (ctx.getUrl() === '/users' && ctx.getMethod() === 'get') {
          return Array.from(users.values())
        }

        // GET /users/:id
        if (ctx.getUrl().startsWith('/users/') && ctx.getMethod() === 'get') {
          const id = ctx.getUrl().split('/')[2]
          const user = users.get(id)

          if (!user) {
            return ctx.setStatus(404).send({ error: 'User not found' })
          }

          return user
        }

        // POST /users
        if (ctx.getUrl() === '/users' && ctx.getMethod() === 'post') {
          const data = await ctx.json()

          if (!data.name || !data.email) {
            return ctx.setStatus(400).send({ error: 'Missing required fields' })
          }

          const user = { id: Date.now().toString(), ...data }
          users.set(user.id, user)

          return ctx.setStatus(201).send(user)
        }

        // 404
        return ctx.setStatus(404).send({ error: 'Not found' })
      } catch (error) {
        console.error('Route error:', error)
        return ctx.setStatus(500).send({ error: 'Internal server error' })
      }
    },
    onError: (event, error) => {
      console.error(`HTTP Error [${event.method} ${event.url}]:`, error)
    }
  }
})

await server.listen()
console.log('REST API running on http://localhost:3000')

File Upload

import Server from '@swarmmachina/swm-core'
import fs from 'fs/promises'

const server = new Server({
  port: 3000,
  http: {
    maxBodySize: 10 * 1024 * 1024, // 10 MiB, expressed in bytes
    onRequest: async (ctx) => {
      if (ctx.getUrl() === '/upload' && ctx.getMethod() === 'post') {
        const filename = ctx.getQuery('filename') || 'upload.bin'
        const body = await ctx.body()

        await fs.writeFile(`./uploads/${filename}`, body)

        return ctx.setStatus(201).send({
          success: true,
          filename,
          size: body.length
        })
      }

      return ctx.setStatus(404).send({ error: 'Not found' })
    }
  }
})

await server.listen()

File Streaming

import Server from '@swarmmachina/swm-core'
import fs from 'fs'

const server = new Server({
  port: 3000,
  http: {
    onRequest: async (ctx) => {
      if (ctx.getUrl() === '/download' && ctx.getMethod() === 'get') {
        const filename = ctx.getQuery('file')

        if (!filename) {
          return ctx.setStatus(400).send({ error: 'Missing file parameter' })
        }

        const stream = fs.createReadStream(`./files/${filename}`)

        await ctx.stream(stream, 200, {
          'content-type': 'application/octet-stream',
          'content-disposition': `attachment; filename="${filename}"`
        })

        return
      }

      return ctx.setStatus(404).send({ error: 'Not found' })
    }
  }
})

await server.listen()

WebSocket Chat Room

import Server from '@swarmmachina/swm-core'

const server = new Server({
  port: 3000,
  http: null,
  ws: {
    onUpgrade: (meta) => ({ username: meta.getQuery('username') || 'Anonymous' }),
    onOpen: (ctx) => {
      console.log('User joined:', ctx.data.username)
      ctx.subscribe('chat')
      ctx.publish(
        'chat',
        JSON.stringify({
          type: 'join',
          user: ctx.data.username
        })
      )
    },
    onMessage: (ctx, message, isBinary) => {
      const text = Buffer.from(message).toString()

      // Broadcast to all clients in the chat room
      ctx.publish(
        'chat',
        JSON.stringify({
          type: 'message',
          user: ctx.data.username,
          text
        })
      )
    },
    onClose: (ctx, code, message) => {
      console.log('User left:', ctx.data.username)
      ctx.publish(
        'chat',
        JSON.stringify({
          type: 'leave',
          user: ctx.data.username
        })
      )
    }
  }
})

await server.listen()
console.log('Chat server running on ws://localhost:3000')