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

@dogsvr/cfg-luban

v0.4.0

Published

Luban + FlatBuffers + LMDB game config module for dogsvr.

Readme

@dogsvr/cfg-luban

Runtime library for reading Luban-generated game config.

Data generated by @dogsvr/cfg-luban-cli is stored in LMDB and encoded as FlatBuffers. At runtime the library mmap's the database and reads rows via FlatBuffers' offset-based random access. Practical consequences:

  • Out of the V8 heap. Config bytes live in the OS pagecache, not in Node's managed heap, so they don't count against --max-old-space-size and don't pay for full-heap GC scans.
  • Shared across workers and processes. All worker threads in one Node process map the same backing pages — workerThreadNum: N doesn't multiply the config footprint by N. Multiple Node processes on the same host (e.g. pm2-managed dir/zone/battle servers) all hit the same pagecache pages too; the kernel deduplicates by inode + offset.
  • No upfront parse. A cold start doesn't pay to deserialize every table — rows are decoded only when queried, and only the pages actually read get paged into physical RAM.
  • O(log n) primary-key lookup. cfg-luban-cli sorts every table by its primary key at build time, so the runtime does a binary search over the FlatBuffers vector directly; no hash map build-up at startup.

"Zero-copy" is often claimed for this kind of setup but is not strictly true here — by default getCfgRow unpacks the FlatBuffers accessor into a plain JS object (one copy). Use getCfgRowUnsafe when you want to skip that copy and read fields directly off the accessor (see Unsafe accessor below for the lifetime caveat).

For the config generation pipeline (Excel → LMDB), see the sibling package @dogsvr/cfg-luban-cli. For this repo's overall layout and dev workflow, see the repo README. For how this fits into the wider framework, see @dogsvr/dogsvr.

Install

npm install @dogsvr/cfg-luban

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.

API

| Interface | Purpose | |-----------|---------| | openCfgDb(options) | Open the LMDB database and load table_keys.json. If options.cfgModule is passed (the flatc barrel), every table is auto-registered. | | closeCfgDb() | Close the DB and clear all registered tables | | registerCfgTable(name, rootFn) | Manually register a single table's FlatBuffers root accessor. Primary use: per-worker selective loading of large cfg, or tables that don't follow the getRootAs<fullName> convention. | | getCfgRow<T>(table, keys) | Primary-key lookup; returns a plain object. O(log n) | | getCfgRowList<T>(table, keysList) | Batch lookup on the same table (1 memcpy + N binary searches). O(N log n) | | getCfgRowUnsafe(table, keys) | Primary-key lookup; returns the raw FlatBuffers accessor (no unpack). O(log n) | | forEachCfgRow<T>(table, cb) | Iterate the entire table; return false from cb to stop early |

The table argument is always the Luban full_name form (e.g. 'TbItem'), matching table_keys.json.

Usage

Worker initialization

Config paths should come from the worker thread config — don't hardcode them, so that a single build artifact can serve multiple environments. cfg-luban supports two wiring styles. Pick based on table count + per-worker coverage.

Style A — barrel module (recommended for small-to-medium cfg)

Minimal boilerplate. Eager-loads every flatc class in the barrel; bundler tree-shaking is defeated by the dynamic cfgModule[fullName] access. Fine up to ~1000 tables; beyond that each worker pays for classes it never touches.

import * as dogsvr from '@dogsvr/dogsvr/worker_thread';
import { openCfgDb } from '@dogsvr/cfg-luban';
// The barrel file `ts/<topModule>.ts` is produced by cfg-luban-cli.
// Adjust the relative path to match your project layout.
import * as cfgModule from '<path-to-generated>/ts/cfg';

interface MyCfg { cfgDbPath: string; tableKeysPath: string; }

dogsvr.workerReady(async () => {
    dogsvr.loadWorkerThreadConfig();
    const cfg = dogsvr.getThreadConfig<MyCfg>();

    openCfgDb({
        dbPath: cfg.cfgDbPath,
        tableKeysPath: cfg.tableKeysPath,
        cfgModule,
    });
    // done — no registerCfgTable calls
});

Style B — per-table imports + manual registerCfgTable (recommended for large cfg with per-worker subsets)

Node only loads the imported table modules + their element-type dependencies. For cfg with thousands of tables where each worker role uses a clear subset, resident memory can drop 5–10× vs. Style A. The tradeoff is N lines of boilerplate and the need to keep the per-worker import list in sync with business code.

import * as dogsvr from '@dogsvr/dogsvr/worker_thread';
import { openCfgDb, registerCfgTable } from '@dogsvr/cfg-luban';
// Import only the tables this worker actually queries.
import { TbReward } from '<path-to-generated>/ts/tb-reward';
import { TbSkill }  from '<path-to-generated>/ts/tb-skill';
import { TbItem }   from '<path-to-generated>/ts/tb-item';

dogsvr.workerReady(async () => {
    dogsvr.loadWorkerThreadConfig();
    const cfg = dogsvr.getThreadConfig<{ cfgDbPath: string; tableKeysPath: string }>();

    openCfgDb({ dbPath: cfg.cfgDbPath, tableKeysPath: cfg.tableKeysPath });
    registerCfgTable('TbReward', TbReward.getRootAsTbReward);
    registerCfgTable('TbSkill',  TbSkill.getRootAsTbSkill);
    registerCfgTable('TbItem',   TbItem.getRootAsTbItem);
});

The dbPath and tableKeysPath values come from whatever worker_thread_config.json the worker is launched with (see @dogsvr/dogsvr for how thread config loading works).

Primary-key lookup

import { getCfgRow } from '@dogsvr/cfg-luban';

const reward = getCfgRow<RewardT>('TbReward', 1001);            // single key
const skill  = getCfgRow<SkillT>('TbSkill', [1001, 5]);         // composite key
const text   = getCfgRow<I18nT>('TbI18n', 'LOGIN_TITLE');       // string key

Batch lookup (performance)

import { getCfgRowList } from '@dogsvr/cfg-luban';

// Same table, many keys: 1 memcpy + N binary searches (not N memcpys)
const rewards = getCfgRowList<RewardT>('TbReward', [1001, 1002, 1003]);

Unsafe accessor (skip unpack)

import { getCfgRowUnsafe } from '@dogsvr/cfg-luban';

// Returns a FlatBuffers accessor; fields are read via method calls.
// ⚠️ The caller must finish using it within synchronous code — the accessor
//    becomes invalid after the next getBinaryFast.
const item = getCfgRowUnsafe('TbItem', 2001);
const damage = item?.damage();
const name   = item?.name();

Iteration

import { forEachCfgRow } from '@dogsvr/cfg-luban';

// Find the first match
let found: RewardT | null = null;
forEachCfgRow<RewardT>('TbReward', (row) => {
    if (row.count > 1000) { found = row; return false; }
});

// Filter
const weapons: ItemT[] = [];
forEachCfgRow<ItemT>('TbItem', (row) => {
    if (row.type === 3) weapons.push(row);
});

Migration note

LMDB keys and the tableName argument now use TbXxx (Luban full_name) rather than the lowercase filename stem tbxxx. After upgrading cfg-luban + cfg-luban-cli, rerun npm run build on your config package once so the regenerated LMDB matches the new keys. registerCfgTable('tbitem', ...) emits a one-shot warning to help catch stragglers.