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

@randajan/koa-io-session

v4.0.0

Published

Simple bridge between koa-session and socket.io. Shares a unified session across HTTP and WebSocket using a common session store.

Readme

@randajan/koa-io-session

NPM JavaScript Style Guide

Bridge between koa-session and selected socket.io servers or namespaces with one shared session flow.

Version 4.0.0

Version 4.0.0 changes the initialization API.

SessionBridge is now created for the Koa application first, and Socket.IO servers or namespaces are attached explicitly with bridge.attach(io).

This is a breaking change from the old bridgeSession(app, io, opt) API.

Why:

  • Koa middleware is application-wide.
  • Socket.IO middleware is server/namespace-specific.
  • Explicit attach() makes it clear which namespaces receive session support.
  • Multiple Socket.IO servers or namespaces can share the same bridge.

Why

This library keeps HTTP and selected WebSocket session work synchronized while preserving native koa-session behavior.

You get:

  • standard ctx.session in HTTP
  • ctx.clientId and socket.clientId
  • ctx.sessionId and socket.sessionId resolved through bridge mapping
  • socket.ctx (Koa context created from socket handshake request with an internal response object)
  • socket.withSession(handler, onMissing?) helper
  • bridge events: sessionSet, sessionDestroy, cleanup

Architecture

Public API is SessionBridge.

Internally, bridge uses a private store layer for TTL/event consistency over your backend store (LiveStore / FileStore / custom).

Install

npm i @randajan/koa-io-session

For persistent file store:

npm i @randajan/file-db

Quick Start

import Koa from "koa";
import { createServer } from "http";
import { Server } from "socket.io";
import bridgeSession from "@randajan/koa-io-session";

const app = new Koa();

// Keep keys stable in production (important for signed cookies after restart)
app.keys = ["your-stable-key-1", "your-stable-key-2"];

const http = createServer(app.callback());
const io = new Server(http, {
  cors: { origin: true, credentials: true }
});

const bridge = bridgeSession(app, {
  key: "app.sid",
  signed: true,
  maxAge: 1000 * 60 * 60 * 24,
  sameSite: "lax",
  httpOnly: true,
  secure: false
});

bridge.attach(io);

bridge.on("sessionSet", ({ clientId, sessionId, isNew, isInit }) => {
  console.log("sessionSet", { clientId, sessionId, isNew, isInit });
});

bridge.on("sessionDestroy", ({ clientId, sessionId }) => {
  console.log("sessionDestroy", { clientId, sessionId });
});

bridge.on("cleanup", (cleared) => {
  console.log("cleanup", cleared);
});

app.use(async (ctx, next) => {
  if (ctx.path !== "/api/session") { return next(); }

  if (ctx.query.reset === "1") {
    ctx.session = null;
    ctx.body = { ok: true, from: "http:reset" };
    return;
  }

  if (!ctx.session.createdAt) { ctx.session.createdAt = Date.now(); }
  if (!Number.isFinite(ctx.session.httpCount)) { ctx.session.httpCount = 0; }
  ctx.session.httpCount += 1;

  ctx.body = {
    ok: true,
    clientId: ctx.clientId,
    sessionId: ctx.sessionId,
    session: ctx.session
  };
});

io.on("connection", (socket) => {
  socket.on("session:get", async (ack) => {
    const payload = await socket.withSession((sessionCtx) => ({
      ok: true,
      sessionId: sessionCtx.sessionId,
      session: sessionCtx.session
    }), { ok: false, error: "missing-session" });

    if (typeof ack === "function") { ack(payload); }
  });
});

http.listen(3000);

Attaching Namespaces

Attach only the namespaces that should receive session support:

const bridge = bridgeSession(app, {
  key: "app.sid",
  signed: true
});

const publicIo = io.of("/public");
const adminIo = io.of("/admin");
const rawIo = io.of("/raw");

bridge.attach(publicIo);
bridge.attach(adminIo);

publicIo.on("connection", (socket) => {
  console.log(socket.clientId, socket.sessionId);
});

adminIo.on("connection", (socket) => {
  socket.withSession(async (sessionCtx) => {
    return sessionCtx.session;
  });
});

rawIo.on("connection", (socket) => {
  // No session bridge is attached here.
});

API

bridgeSession(app, opt)

Creates and returns SessionBridge.

It installs the Koa/HTTP side of the bridge on app.

It does not attach Socket.IO middleware by itself. Use bridge.attach(io) for each Socket.IO server or namespace that should use sessions.

SessionBridge

Extends Node.js EventEmitter.

Events:

  • sessionSet: { clientId, sessionId, isNew, isInit }
  • sessionDestroy: { clientId, sessionId }
  • cleanup: clearedCount (number of expired sessions removed)

sessionSet flags:

  • isNew: backend store reported creation of a new persisted session record (sid had no previous state)
  • isInit: bridge just initialized/attached mapping in current process lifecycle (clientId <-> sessionId)
  • typical combinations:
  • isNew: true, isInit: true -> newly created session was attached now
  • isNew: false, isInit: true -> existing session was attached now (for example after process restart)
  • isNew: false, isInit: false -> already attached session was updated

Runtime additions:

  • HTTP context: ctx.clientId, ctx.sessionId
  • socket: socket.ctx, socket.clientId, socket.sessionId, socket.withSession(handler, onMissing?)
  • socket.ctx is created during socket initialization from the handshake request and an internal response object

Methods:

  • attach(io): this
  • getSessionId(clientId): string | undefined
  • getClientId(sessionId): string | undefined
  • getById(sessionId): Promise<object | undefined>
  • getByClientId(clientId): Promise<object | undefined>
  • destroyById(sessionId): Promise<boolean>
  • destroyByClientId(clientId): Promise<boolean>
  • setById(sessionId, session, maxAge?): Promise<boolean> (cannot create missing session)
  • setByClientId(clientId, session, maxAge?): Promise<boolean> (cannot create missing session)
  • cleanup(): Promise<number>
  • startAutoCleanup(interval?): boolean
  • stopAutoCleanup(): boolean
  • notifyStoreSet(sessionId, isNew?): void
  • notifyStoreDestroy(sessionId): void
  • notifyStoreCleanup(clearedCount): void

bridge.attach(io)

Attaches the Socket.IO side of the bridge to a Server or Namespace.

const bridge = bridgeSession(app, opt);

bridge.attach(io);
bridge.attach(io.of("/admin"));
bridge.attach(io.of("/app"));

Only attached Socket.IO targets receive:

  • socket.ctx
  • socket.clientId
  • socket.sessionId
  • socket.withSession(handler, onMissing?)

Calling attach() twice with the same target throws an error.

Namespaces that are not attached are left untouched.

Missing policy:

  • getBy* on missing mapping: returns undefined
  • destroyBy* on missing mapping: returns false
  • setBy* on missing mapping: throws Error (creating via this path is prohibited)

socket.withSession(handler, onMissing?)

handler receives:

  • sessionCtx.sessionId
  • sessionCtx.session
  • sessionCtx.socket

Rules:

  • default onMissing is error (Session is missing for this socket)
  • if sessionCtx.session = null, session is destroyed
  • if session changed, store set is called
  • same-session calls are serialized by sessionId

onMissing behavior:

  • Error -> throw
  • function -> call and return its value
  • any other value -> return as fallback

Options

opt is mostly forwarded to koa-session, with bridge-specific keys:

  • appKeys (optional array used to initialize app.keys)
  • allowRndAppKeys (default false; suppress runtime warning when keys are generated)
  • store (backend store implementation)
  • maxAge (session TTL used by StoreGateway and koa cookie)
  • autoCleanup (default false)
  • autoCleanupMs (used only when autoCleanup === true)
  • clientKey (default "cid")
  • clientMaxAge (default 1 year)
  • clientAlwaysRoll (default true)

Default behavior:

  • key: "sid" when missing
  • signed: true
  • store: new LiveStore()
  • app.keys: if missing, bridge generates 2 runtime keys (length 32) and logs warning
  • autoCleanupMs: when omitted and autoCleanup is enabled, interval is computed as maxAge / 4, clamped to <1 minute, 1 day>

app.keys resolution:

  • if app.keys already exists and appKeys is not provided: existing app.keys is used
  • if app.keys already exists and appKeys is provided: throws error
  • if app.keys is missing and appKeys is provided: app.keys = appKeys
  • if both are missing: bridge generates runtime keys; warning is shown unless allowRndAppKeys === true

Store Contract

Backend store must implement:

  • get(sid) -> returns stored state or undefined
  • set(sid, state) -> returns boolean (or truthy)
  • destroy(sid) -> returns boolean

Optional:

  • list() -> required for cleanup features
  • optimize(clearedCount) -> called after cleanup if present

Stored state format expected by gateway:

  • { session, expiresAt, ttl } where session is JSON string (serialized session object)

Both sync and async store methods are supported.

Consistency Rule (Important)

After bridge initialization, direct mutation of opt.store is unsupported by default.

Why:

  • it bypasses gateway/bridge consistency flow
  • it can break clientId <-> sessionId synchronization
  • it can cause missing or misleading bridge events

Use SessionBridge methods (setBy*, destroyBy*, cleanup) for controlled mutations.

Advanced bypass (you take full responsibility):

  • if you intentionally mutate backend store directly, call matching notify method right after each mutation:
  • notifyStoreSet(sessionId, isNew?)
  • notifyStoreDestroy(sessionId)
  • notifyStoreCleanup(clearedCount)

Built-in Stores

LiveStore

In-memory backend store.

import bridgeSession, { LiveStore } from "@randajan/koa-io-session";

const bridge = bridgeSession(app, {
  store: new LiveStore()
});

bridge.attach(io);

FileStore (persistent, @randajan/file-db)

import { FileStore } from "@randajan/koa-io-session/fdb";

const bridge = bridgeSession(app, {
  store: new FileStore({ fileName: "sessions" })
});

bridge.attach(io);

Behavior and Limitations

  1. Session creation is HTTP-first.
  • WebSocket path does not create missing sessions by itself.
  1. Socket.IO support is opt-in per target.
  • Call bridge.attach(io) for each Socket.IO server or namespace that should receive session support.
  • Namespaces without attach() do not get socket.ctx, socket.clientId, socket.sessionId, or socket.withSession().
  1. The HTTP side is global for the Koa application.
  • Creating a bridge installs Koa middleware on app.
  • Socket.IO middleware is installed only by attach().
  1. Socket initialization uses an internal response object.
  • Signed cookie reads may ask the cookie layer to refresh or clear signatures.
  • During Socket.IO initialization, those response headers are not sent to the client.
  • Cookie repair is expected to happen on a normal HTTP request.
  1. Mapping (clientId <-> sessionId) is in-memory.
  • After process restart, mapping is rebuilt from incoming cookies and existing store state.
  1. Signed cookies depend on stable app.keys.
  • Changing keys invalidates previous signed cookies.
  1. WS change detection uses JSON.stringify.
  • Non-serializable/cyclic payloads are not recommended in session data.

Migrating From 3.x

Version 3.x:

const bridge = bridgeSession(app, io, opt);

Version 4.0.0:

const bridge = bridgeSession(app, opt);
bridge.attach(io);

For namespaces:

const bridge = bridgeSession(app, opt);

bridge.attach(io.of("/a"));
bridge.attach(io.of("/admin"));

io.of("/raw"); // no session bridge

Exports

Main entry:

import bridgeSession, {
  bridgeSession,
  SessionBridge,
  LiveStore,
  generateUid
} from "@randajan/koa-io-session";

Persistent file store entry:

import { FileStore } from "@randajan/koa-io-session/fdb";

License

MIT (c) randajan