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

v3.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 socket.io with one shared session flow.

Why

This library keeps HTTP and 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.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, io, {
  key: "app.sid",
  signed: true,
  maxAge: 1000 * 60 * 60 * 24,
  sameSite: "lax",
  httpOnly: true,
  secure: false
});

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);

API

bridgeSession(app, io, opt)

Creates and returns SessionBridge.

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.clientId, socket.sessionId, socket.withSession(handler, onMissing?)

Methods:

  • 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

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:

  • store (backend store implementation)
  • maxAge (session TTL used by StoreGateway and koa cookie)
  • autoCleanup (default false)
  • autoCleanupMs (used only when autoCleanup === true)
  • clientKey (default ${key}.cid)
  • clientMaxAge (default 1 year)
  • clientAlwaysRoll (default true)

Default behavior:

  • key: random generated when missing
  • signed: true
  • store: new LiveStore()
  • app.keys: auto-generated if missing (recommended to set manually in production)
  • autoCleanupMs: when omitted and autoCleanup is enabled, interval is computed as maxAge / 4, clamped to <1 minute, 1 day>

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, io, {
  store: new LiveStore()
});

FileStore (persistent, @randajan/file-db)

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

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

Behavior and Limitations

  1. Session creation is HTTP-first.
  • WebSocket path does not create missing sessions by itself.
  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.

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