@dogsvr/dogsvr
v0.4.1
Published
Game server framework with main/worker thread separation, hot-update, and pluggable connection layers.
Maintainers
Readme
@dogsvr/dogsvr
Node.js game server framework built around a main thread + worker thread model: the main thread owns connections and routes messages, worker threads run business logic in parallel. Worker code is hot-updatable; connection layers are pluggable; message serialization is whatever you want (Uint8Array or string).
This package is the entry point of the dogsvr polyrepo — if you're new here, start with this README for the framework, then walk through example-proj for a runnable three-server reference.
Ecosystem — the dogsvr polyrepo
The dogsvr stack is intentionally split into small, independently versioned git repos. Each repo publishes at most one or two npm packages; no monorepo, no workspaces.
| Repo / package | Role |
|---|---|
| @dogsvr/dogsvr | Framework core — main thread, worker threads, load balancer, hot update, txn mgr |
| @dogsvr/cl-tsrpc | TSRPC connection layer (WebSocket / HTTP) with per-connection auth + identity binding (openId / zoneId / gid) |
| @dogsvr/cl-grpc | gRPC connection layer for server-to-server unary calls |
| @dogsvr/cfg-luban | Runtime for reading Luban-generated game config — data lives in LMDB (mmap'd, outside the V8 heap), so all worker threads in the process share one pagecache-resident copy, and multiple Node processes on the same host share it via the OS pagecache too. FlatBuffers provides offset-based random access, so no upfront parse and no GC pressure from config tables |
| @dogsvr/cfg-luban-cli | Codegen CLI: Excel → FlatBuffers → LMDB pipeline |
| example-proj | Reference integration — three servers (dir / zonesvr / battlesvr), Redis + MongoDB, Colyseus rooms |
| example-proj-cfg | Reference business config repo that feeds cfg-luban-cli |
| example-proj-client | Reference Phaser 3 web client for example-proj |
You pick which @dogsvr/cl-* packages to install; you pick whether to use @dogsvr/cfg-luban; the framework core only requires one of them to be imported at startup to self-register a CL factory. See Architecture below for how they fit together at runtime.
Features
- Multi-thread via Node
worker_threads— main thread runs one event loop for connections; N worker threads run business handlers. Messages between them are routed by a pluggable load-balancer (round-robin, random, least-load, consistent-hash bygid). - Pluggable connection layers (CL) — import any
@dogsvr/cl-*package to self-register a factory; main thread wires inbound/outbound connections from JSON config. Roll your own CL by extendingBaseCL/BaseCLC. - No serialization opinions —
Msg.bodyisUint8Array | string. Protobuf, JSON, MsgPack, FlatBuffers — it's your call. - Hot update of worker logic — drain in-flight txns and replace workers without dropping connections. Two strategies:
rolling(one at a time, default) orallAtOnce(all new, then drain old). Triggered via pm2tx2action.
Requirements
Node.js: tested on v24.13.0 on Linux (x86-64); other maintained LTS lines are expected to work but are not routinely exercised. File an issue if something breaks on your runtime.
Install
npm install @dogsvr/dogsvr
npm install @dogsvr/cl-tsrpc # pick your connection layer(s)
npm install @dogsvr/cl-grpc # or gRPC, or bothThe
@dogsvr/dogsvrpackage exposes two subpath imports only — there is no root entry. See import paths below.
Quick start
Minimum two files: one that boots the main thread, one that runs in each worker.
server.ts (main thread entry)
import * as dogsvr from '@dogsvr/dogsvr/main_thread';
import '@dogsvr/cl-tsrpc'; // self-registers "tsrpc" CL factory
import * as path from 'node:path';
await dogsvr.startServer({
workerThreadRunFile: path.resolve(__dirname, 'worker.js'),
workerThreadNum: 2,
cl: { "tsrpc": { "type": "tsrpc", "svrType": "ws", "port": 20000 } },
clc: {},
lbStrategy: { strategy: 'roundRobin' },
hotUpdateStrategy: { strategy: 'rolling' },
});Or point to a JSON config file — the shape mirrors the SvrConfig object above (workerThreadRunFile, workerThreadNum, cl, clc, lbStrategy, hotUpdateStrategy, optional workerConfigPath / logLevel / hotUpdateTimeout):
import * as dogsvr from '@dogsvr/dogsvr/main_thread';
import '@dogsvr/cl-tsrpc';
dogsvr.startServer(__dirname + '/main_thread_config.json');worker.ts (runs in each worker thread)
import * as dogsvr from '@dogsvr/dogsvr/worker_thread';
dogsvr.workerReady(async () => {
// one-time init: open DBs, load config, etc.
});
dogsvr.regCmdHandler(10001, async (reqMsg) => {
const req = JSON.parse(reqMsg.body as string);
if (!req.name) {
// non-zero errCode path — framework sends an error response
throw new dogsvr.HandlerError(1001, 'name is required');
}
// return a body (string | Uint8Array) and the framework responds for you
return JSON.stringify({ res: `hello, ${req.name}` });
// other valid returns:
// return { body: '...', head: { serverVersion: '1.2.3' } }; // with head patch
// return; // undefined → silent drop (no response)
});respondCmd / respondError are still exported as escape hatches for advanced cases (e.g. responding to a request from a different async context), but the return-value / throw form above is the canonical handler shape.
Run
pm2 start dist/server.js
pm2 trigger dist/server hotUpdate # redeploy worker.js without dropping connsFor a complete, runnable example with three servers, Redis/MongoDB integration, and room-based battles, see example-proj.
Import paths
@dogsvr/dogsvr exposes two subpaths and no root:
import * as dogsvr from '@dogsvr/dogsvr/main_thread'; // main-thread APIs
import * as dogsvr from '@dogsvr/dogsvr/worker_thread'; // worker-thread APIsAttempting require('@dogsvr/dogsvr') returns ERR_PACKAGE_PATH_NOT_EXPORTED — this is intentional, so that code running in a worker can never accidentally pull in startServer (which would recursively spawn more workers).
Main-thread surface (startServer, sendMsgToWorkerThread, hotUpdate, getConnLayer, BaseCL, BaseCLC, registerCLFactory, registerCLCFactory, loadMainThreadConfig, Msg, log functions…) — see src/main_thread/index.ts.
Worker-thread surface (workerReady, regCmdHandler, respondCmd, respondError, callCmdByClc, pushMsgByCl, loadWorkerThreadConfig, getThreadConfig<T>, Msg, log functions…) — see src/worker_thread/index.ts.
Architecture
- Main thread (
src/main_thread/) owns the event loop for connections and dispatches messages to workers by command ID + routing fields (gidfor consistent-hash LB). - Worker threads (
src/worker_thread/) run your registered handlers. Workers never talk directly — cross-worker comms go through CLC callbacks routed by main. - Messages (
src/message.ts):Msg { head: MsgHeadType, body: MsgBodyType }. Head carriescmdId, routing fields (openId/zoneId/gid), txn id, direction flags (clcOptions/clOptions), and error info. Body is raw bytes or string.
Package resolution compatibility
@dogsvr/dogsvr is authored to resolve correctly under every JavaScript module resolver. Modern resolvers read the exports field; older ones fall back to stub package.json files under main_thread/ and worker_thread/:
| Resolver | Reads exports? | Reads stub package.json? | Result |
|---|:-:|:-:|---|
| Node.js (modern) | ✓ | — | Hits dist/… via exports |
| Node.js (legacy, pre-exports) | ✗ | ✓ | Hits dist/… via stub's main |
| TypeScript moduleResolution: bundler / node16 / nodenext | ✓ | — | Hits dist/….d.ts via exports.types |
| TypeScript moduleResolution: node (TS default) | ✗ | ✓ | Hits dist/….d.ts via stub's types |
| Webpack / Rollup / Vite / Parcel / esbuild (modern) | ✓ | — | Hits dist/… via exports |
| Webpack 4 and other legacy bundlers | ✗ | ✓ | Hits dist/… via stub's main |
In every case, exactly one of the two mechanisms resolves the import — nothing to configure on the consumer side.
Published layout
@dogsvr/dogsvr/
├── main_thread/
│ └── package.json # stub: { "main": "../dist/main_thread/index.js", "types": "../dist/main_thread/index.d.ts" }
├── worker_thread/
│ └── package.json # stub: { "main": "../dist/worker_thread/index.js", "types": "../dist/worker_thread/index.d.ts" }
├── dist/
│ ├── main_thread/index.{js,d.ts}
│ └── worker_thread/index.{js,d.ts}
└── package.json # "exports" map for modern resolversLicense
MIT — see LICENSE.
