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

@fluxstack/spatial-room

v0.1.0

Published

Spatial LiveRoom with grid-based interest management. Reduces broadcast cost from O(N) to O(visible cells) for games where players only need updates from nearby peers.

Downloads

152

Readme

@fluxstack/spatial-room

Interest management for @fluxstack/live rooms. Reduces broadcast cost from O(N) to O(visible cells) for games where players only need updates from nearby peers.

Two ready-to-use room classes:

  • SpatialLiveRoom — general 2D/3D grid. Top-down RPG, side-scroller, .io games, MMO maps.
  • ChunkRoom — specialised 3D voxel/Minecraft-style chunks. Default cellSize=16 (Minecraft standard chunk), default view distance 1 (3×3×3 = 27 chunks visible).

Why

A LiveRoom with 1000 members fanouts every emit() to all 1000 WebSocket connections. For chat/state updates that's fine; for player movement at 30/60 Hz it crushes the server. Spatial rooms keep an internal grid and only deliver to members in the surrounding cells of the sender.

Measured impact (see tests):

  • 2D, 1000 players in 1000×1000 world, cellSize=100: broadcast reaches 86 peers (8.6% of room) instead of 999 (91% reduction).
  • 3D voxel, 1000 players in 30³ chunks, view distance 1: broadcast reaches 33 peers (3.3% of room) instead of 999 (97% reduction).

Install

bun add @fluxstack/spatial-room

Quick start — 2D top-down

import { SpatialLiveRoom } from '@fluxstack/spatial-room'

interface GameState {
  players: Record<string, { x: number; y: number; hp: number }>
}

interface GameEvents {
  moved: { id: string; x: number; y: number }
  shot:  { from: string; angle: number }
}

class GameRoom extends SpatialLiveRoom<GameState, any, GameEvents> {
  static roomName = 'game'
  static defaultState: GameState = { players: {} }
  static spatial = {
    dimensions: 2,
    cellSize: 100,      // world units per cell
    defaultRange: 1,    // 3×3 visible neighbourhood
  }

  movePlayer(componentId: string, x: number, y: number) {
    this.setMemberPosition(componentId, [x, y])
    this.state.players[componentId] = { ...this.state.players[componentId]!, x, y }
    // Only players in the same or adjacent cells receive this:
    this.emitNearby(componentId, 'moved', { id: componentId, x, y })
  }
}

Quick start — 3D voxel / Minecraft

import { ChunkRoom } from '@fluxstack/spatial-room'

class WorldRoom extends ChunkRoom<{}, any, {
  blockPlaced: { x: number; y: number; z: number; type: number }
}> {
  static roomName = 'world'
  static defaultState = {}
  // Defaults: 3D, cellSize=16, range=1 → 27 chunks visible. Override
  // `static spatial = { cellSize: ..., defaultRange: ... }` if needed.

  placeBlock(componentId: string, x: number, y: number, z: number, type: number) {
    this.setMemberWorldPosition(componentId, x, y, z) // updates the grid
    // Only players in nearby chunks receive this:
    this.emitInChunkRange(componentId, 'blockPlaced', { x, y, z, type })
  }
}

API

SpatialLiveRoom (extends LiveRoom)

| method | description | |---|---| | setMemberPosition(id, [x, y] or [x, y, z]) | place / move a member in the grid. Idempotent; returns true if the member crossed a cell boundary. | | getMemberCell(id) | current cell key (string), or undefined | | emitNearby(senderId, event, data, opts?) | broadcast only to members within opts.range cells of the sender (default defaultRange). Sender excluded unless includeSelf. Falls back to global emit() if sender has no recorded position. | | emitAtPosition([x, y], event, data, opts?) | broadcast to members near an arbitrary world point (no sender — useful for "explosion at X") | | getVisibleMembers(id, range?) | diagnostic: who can see this member | | getOccupiedCellCount() | diagnostic: populated cells — useful to tune cellSize |

The grid property exposes the underlying SpatialGrid for advanced use (custom queries, manual indexing).

ChunkRoom (extends SpatialLiveRoom with 3D defaults)

Adds:

| method | description | |---|---| | setMemberWorldPosition(id, x, y, z) | same as setMemberPosition but with explicit args | | setMemberChunk(id, [cx, cy, cz]) | place a member by chunk coord (places at chunk origin) | | getMemberChunk(id) | current chunk coord [cx, cy, cz] or undefined | | emitInChunkRange(senderId, event, data, opts?) | alias of emitNearby with game-y naming | | emitAtChunk([cx, cy, cz], event, data, opts?) | broadcast to members near a chunk (server-originated events) |

Plus the helper functions:

  • chunkToWorld([cx, cy, cz], chunkSize) → world [x, y, z] at chunk origin
  • worldToChunk([x, y, z], chunkSize) → containing chunk [cx, cy, cz]

SpatialGrid<M> (low-level)

If you want spatial indexing without the LiveRoom integration (e.g. for bots / NPCs / server-side AI):

import { SpatialGrid } from '@fluxstack/spatial-room'

const grid = new SpatialGrid<string>({ dimensions: 2, cellSize: 50 })
grid.setPosition('npc-1', [120, 80])
grid.setPosition('npc-2', [125, 85])
grid.queryNear([100, 100], 1)  // → Set { 'npc-1', 'npc-2' }
grid.queryNearMember('npc-1')  // → Set { 'npc-2' } (excludes self)

Tuning cellSize

Rule of thumb: pick cellSize so that one cell contains the interaction range of a player. For a top-down shooter with bullets visible up to 500 units away, cellSize=500 and range=1 (3×3 = 1500 units of coverage) is a good start.

Diagnostic: if room.getOccupiedCellCount() stays in single digits with hundreds of players, cells are too big (everyone in the same cell — filter does nothing). If it explodes to thousands with few players, cells are too small (lookups walk many empty cells).

What it does NOT do

  • State broadcast: setState still goes to all members. Interest management here is for events (emit), not the shared room state. Most games keep state as "world data" (replicated everywhere) and use events for per-player updates anyway.
  • Custom shapes: only axis-aligned cells. No circles, no view cones, no occlusion. Build those on top by combining queryNear results with your own filters.
  • Dynamic load balancing: cells are uniform. If you have a dense club next to an empty desert, the club's cell will be a hotspot. For that case, look at quadtree / R-tree libraries — this package intentionally stays simple.