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

@molecule/api-realtime-rooms

v1.0.1

Published

Realtime rooms abstraction: named pub/sub rooms with membership, capacity, join codes, role-based auth, built atop @molecule/api-realtime.

Downloads

518

Readme

@molecule/api-realtime-rooms

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Realtime-rooms utility for molecule.dev.

Authenticated, capacity-bounded, role-aware named pub/sub rooms built on top of @molecule/api-realtime's transport bond and persisted via @molecule/api-database's abstract DataStore.

Solves the IDOR pattern flagged in flagship realtime apps (quiz-platform live sessions, virtual-classroom rooms) where any authenticated user could subscribe to / broadcast on any channel — {@link assertCanAct} is the central guard.

Quick Start

import {
  createRoom,
  joinRoom,
  broadcast,
  subscribe,
  assertCanAct,
} from '@molecule/api-realtime-rooms'

// Host creates a private room with a join code.
const room = await createRoom({
  kind: 'quiz-session',
  ownerId: hostUserId,
  capacity: 30,
  joinCode: 'ABC123',
})

// Guest joins.
await joinRoom(room.id, guestUserId, 'ABC123')

// Broadcast a question — handler must authorise first.
await assertCanAct(room.id, hostUserId, 'host')
await broadcast(room.id, { kind: 'question-asked', payload: { qid: 1 } })

Type

utility

Installation

npm install @molecule/api-realtime-rooms @molecule/api-database @molecule/api-realtime

API

Interfaces

BroadcastOptions

Options for {@link broadcast}.

interface BroadcastOptions {
  /** Event name / discriminator. */
  kind: string
  /** Event payload. */
  payload: unknown
  /** Optional override for the broadcast timestamp. Defaults to `new Date()`. */
  sentAt?: Date
}

CreateRoomOptions

Options for {@link createRoom}.

interface CreateRoomOptions {
  /** App-level discriminator. */
  kind: string
  /** User id that will be recorded as the host. */
  ownerId: string
  /** Maximum concurrent members (host counts). Optional. */
  capacity?: number
  /** Shared secret required to join. When supplied, `isPublic` is forced `false`. */
  joinCode?: string
  /** Whether non-invited users may join freely. Defaults to `true`. */
  isPublic?: boolean
  /**
   * Optional pre-generated room id. When omitted, a new id is minted by
   * the underlying DataStore.
   */
  id?: string
}

Room

A persisted realtime room.

kind is an opaque app-level discriminator (e.g. 'quiz-session', 'virtual-classroom') so a single rooms table can serve multiple features without collision.

interface Room {
  /** Unique room identifier. */
  id: string
  /** App-level discriminator, e.g. `'quiz-session'`. */
  kind: string
  /** User id of the room host (creator). */
  ownerId: string
  /** Maximum number of concurrent members. `undefined` means uncapped. */
  capacity?: number
  /** Optional shared secret required to join private rooms. */
  joinCode?: string
  /** When `false`, joiners must supply a matching {@link Room.joinCode}. */
  isPublic: boolean
  /** Creation timestamp. */
  createdAt: Date
}

RoomEvent

An event broadcast to all subscribers of a room.

kind is the event name (e.g. 'question-asked', 'answer-revealed') and payload is the arbitrary serialisable body.

interface RoomEvent {
  /** The room this event was broadcast on. */
  roomId: string
  /** Event name / discriminator. */
  kind: string
  /** Event payload. Must be JSON-serialisable. */
  payload: unknown
  /** Server-side broadcast timestamp. */
  sentAt: Date
}

RoomMember

A persisted member of a room.

interface RoomMember {
  /** The room this membership belongs to. */
  roomId: string
  /** The user holding the membership. */
  userId: string
  /** The user's role within the room. */
  role: RoomRole
  /** When the user joined. */
  joinedAt: Date
}

Types

RoomEventHandler

Subscription handler receiving every broadcast on a room.

type RoomEventHandler = (event: RoomEvent) => void | Promise<void>

RoomRole

Role a member holds within a room.

  • host — created the room, full control (close, kick, broadcast).
  • guest — joined the room, may broadcast iff the host allows it.
type RoomRole = 'host' | 'guest'

Unsubscribe

Function returned by {@link subscribe} that removes the subscription when invoked. Implementations should be idempotent.

type Unsubscribe = () => void

Classes

InvalidJoinCodeError

Supplied join code did not match the room's configured code.

RoomCapacityExceededError

Room is full — capacity has been reached.

RoomError

Base class for all realtime-rooms errors.

RoomNotFoundError

The requested room does not exist.

UnauthorizedRoomActionError

The acting user is not authorised to perform the requested action.

Thrown by {@link assertCanAct} when the user is not a member of the room or lacks the required role. This is the central guard that fixes the IDOR pattern in flagship realtime apps.

Functions

assertCanAct(roomId, userId, requiredRole)

Throws {@link UnauthorizedRoomActionError} unless userId is a member of roomId (and optionally holds at least requiredRole).

Role hierarchy: host > guest. Requesting requiredRole = 'guest' is satisfied by any membership; requiredRole = 'host' requires the member's role to be host.

Always call this before any broadcast / membership-mutation / privileged read in your handler. This is the IDOR fix.

function assertCanAct(roomId: string, userId: string, requiredRole?: RoomRole): Promise<RoomMember>
  • roomId — Room being acted on.
  • userId — Acting user id.
  • requiredRole — Minimum role required. Optional.

Returns: The member row (useful when the caller needs the role).

broadcast(roomId, event)

Broadcasts an event to all subscribers of a room via the bonded realtime provider.

Caller responsibility: authorise first via {@link assertCanAct}. This function is intentionally unauthenticated to keep it composable — middleware/handlers decide whether the actor is allowed to publish.

function broadcast(roomId: string, event: BroadcastOptions): Promise<RoomEvent>
  • roomId — Room to broadcast on.
  • event — Event kind + payload.

Returns: The {@link RoomEvent} that was broadcast (useful for echo / local persistence).

channelFor(roomId)

Realtime channel identifier for a given room. Apps subscribing to the underlying transport directly should use this same convention so the abstraction layers up cleanly.

function channelFor(roomId: string): string
  • roomId — Room identifier.

Returns: Stable channel name.

closeRoom(roomId)

Closes a room — deletes membership rows then the room itself.

Subscribers should observe a final event of their choosing (callers commonly broadcast 'room-closed' via {@link broadcast} immediately before invoking this).

function closeRoom(roomId: string): Promise<void>
  • roomId — The room to close.

createRoom(options)

Creates a new room and registers the owner as the host member.

Capacity, join-code, and public/private invariants are enforced here (a join-code forces isPublic=false).

function createRoom(options: CreateRoomOptions): Promise<Room>
  • options — Creation parameters.

Returns: The persisted room.

joinRoom(roomId, userId, joinCode)

Adds a user to a room as a guest.

Validates capacity and join-code. Re-joining is idempotent — a user already in the room receives their existing membership without incrementing the count.

function joinRoom(roomId: string, userId: string, joinCode?: string): Promise<RoomMember>
  • roomId — Room to join.
  • userId — User joining.
  • joinCode — Required when the room has a configured joinCode.

Returns: The user's membership row.

leaveRoom(roomId, userId)

Removes a user from a room. Idempotent — silently succeeds if the user is not a member.

function leaveRoom(roomId: string, userId: string): Promise<void>
  • roomId — Room to leave.
  • userId — User leaving.

listMembers(roomId)

Lists all current members of a room.

Note: callers that need to enforce visibility (only members can list other members) should await assertCanAct(roomId, viewerUserId) first.

function listMembers(roomId: string): Promise<RoomMember[]>
  • roomId — Room to list.

Returns: Members ordered by joined_at ascending.

subscribe(roomId, handler)

Subscribes handler to every event broadcast on roomId.

This package installs exactly one transport-level onMessage listener for the whole process (at module load) and multiplexes it: subscribe() registers handler against the room's channel and the returned {@link Unsubscribe} removes that handler. Subscribing installs no additional transport listener, and unsubscribing fully detaches the handler from the registry — so listeners never accumulate and it is safe to subscribe / unsubscribe per request. The unsubscribe is idempotent.

function subscribe(roomId: string, handler: RoomEventHandler): Unsubscribe
  • roomId — Room to subscribe to.
  • handler — Invoked with each {@link RoomEvent} broadcast on the room.

Returns: Idempotent function that removes this subscription.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-database ^1.0.1
  • @molecule/api-realtime ^1.0.1

Runtime Dependencies

  • @molecule/api-database
  • @molecule/api-realtime

Tables: the .sql file under src/__setup__ creates realtime_rooms + realtime_room_members. An mlcl-scaffolded API replays .sql files under __setup__ automatically on migrate; anywhere else run it once — nothing at runtime creates them. The shipped DDL is PostgreSQL-flavoured (gen_random_uuid(), TIMESTAMPTZ); adapt the column types for SQLite/MySQL — the service itself is dialect-agnostic (abstract DataStore).

Wiring prereqs: a database bond must be wired (any @molecule/api-database-* provider) AND a realtime transport must be set via @molecule/api-realtime's setProvider() (e.g. the @molecule/api-realtime-socketio provider) before broadcast/subscribe deliver anything. subscribe() registrations made before the transport is set are buffered by api-realtime and flushed when it is; broadcast() before then throws "Realtime provider not configured".

subscribe() does NOT install a transport-level listener per call. The package installs exactly ONE shared onMessage listener for the whole process (at import) and multiplexes it to per-room handlers; subscribe() registers a handler and the returned unsubscribe fully removes it. Listeners never accumulate, so subscribing / unsubscribing per request is safe.