@orieg/expanse

中文文档
0.17.0发布于 2026/11/02MIT OR Apache-2.0TypeScriptCommonJS零依赖构建来源已验证

Clean-room, pure-Rust modern Judy arrays and digital tries with cache-line-tuned node geometries, inline value packing, slab arenas, and OCC concurrency for Node.js, Bun, and Deno.

原始使用文档
文档目录11 节
  1. Overview
  2. Installation
  3. Quick Start
  4. 64-bit Integer Set ( ExpanseSet )
  5. 64-bit Integer Map ( ExpanseMap )
  6. Prefix-Compressed String Map ( ExpanseStrMap )
  7. Arbitrary Binary Map ( ExpanseBytesMap )
  8. High-Performance Blob Map ( ExpanseBlobMap )
  9. Concurrent Structures ( SyncExpanseMap / SyncExpanseSet )
  10. Concurrent string, bytes, ordered-bytes and blob maps
  11. License

@orieg/expanse (expanse-node)

Native Node.js, Bun, and Deno bindings for Expanse: clean-room, pure-Rust Judy arrays and 256-ary digital tries with optimistic concurrency control (OCC).

npm version License

Overview

@orieg/expanse provides drop-in, zero-dependency, ultra-fast sparse data structures for JavaScript and TypeScript runtimes:

  • ExpanseSet: 64-bit unsigned integer set (compat: Judy1) with rank/select and range queries.
  • ExpanseMap: 64-bit integer key/value map (compat: JudyL).
  • ExpanseStrMap: Byte-lexicographically sorted string map (compat: JudySL) with prefix compression.
  • ExpanseBytesMap: Unordered map over arbitrary binary keys (including \0 bytes) (compat: JudyHS).
  • ExpanseBlobMap: Polymorphic blob storage with inline value packing (≤ 7 bytes stored inline with 0 heap allocations), chunked slab arenas, in-place compaction, and mmap image persistence.
  • SyncExpanseMap / SyncExpanseSet: Thread-safe concurrent structures with optimistic concurrency control (OCC) and epoch-based memory reclamation.
  • SyncExpanseStrMap / SyncExpanseBytesMap / SyncExpanseOrderedBytesMap / SyncExpanseBlobMap: The same concurrent form for string, byte-string, ordered byte-string and blob (byte payload plus 24-bit hot metadata) maps.

Installation

npm install @orieg/expanse
# or
bun add @orieg/expanse
# or
pnpm add @orieg/expanse

Quick Start

64-bit Integer Set (ExpanseSet)

import { ExpanseSet } from '@orieg/expanse';

const set = new ExpanseSet();
set.add(42n);
set.add(100n);
set.add(500n);

console.log(set.has(42n)); // true
console.log(set.size());   // 3n

// Rank / Select
console.log(set.rank(100n)); // 1n (1 element strictly < 100)
console.log(set.select(0n));  // 42n (0-th element)

// Navigation
console.log(set.next(42n));  // 100n
console.log(set.prev(500n)); // 100n

// Range queries
console.log(set.countRange(40n, 200n)); // 2n
console.log(set.range(40n, 200n));      // [42n, 100n]

64-bit Integer Map (ExpanseMap)

import { ExpanseMap } from '@orieg/expanse';

const map = new ExpanseMap();
map.set(100n, 9999n);
map.set(200n, 8888n);

console.log(map.get(100n)); // 9999n
console.log(map.first());   // { key: 100n, value: 9999n }

Prefix-Compressed String Map (ExpanseStrMap)

import { ExpanseStrMap } from '@orieg/expanse';

const strmap = new ExpanseStrMap();
strmap.set('apple', 1n);
strmap.set('banana', 2n);
strmap.set('cherry', 3n);

console.log(strmap.get('banana')); // 2n
console.log(strmap.first());        // { key: 'apple', value: 1n }
console.log(strmap.next('apple'));  // { key: 'banana', value: 2n }

Arbitrary Binary Map (ExpanseBytesMap)

import { ExpanseBytesMap } from '@orieg/expanse';

const bytesmap = new ExpanseBytesMap();
const binaryKey = Buffer.from([0x00, 0x01, 0x02, 0xff]);
bytesmap.set(binaryKey, 42n);

console.log(bytesmap.get(binaryKey)); // 42n

High-Performance Blob Map (ExpanseBlobMap)

import { ExpanseBlobMap } from '@orieg/expanse';

const blobmap = new ExpanseBlobMap();

// Short payloads (<= 7 bytes) written with hot metadata 0 are stored inline in the
// value slot (0 heap allocations); with metadata they are arena records that keep it
blobmap.set(1n, Buffer.from('inline'), 0);

// Larger payloads are allocated in contiguous slab arenas
blobmap.set(2n, Buffer.from('large payload allocated in slab arena'), 20);

// Lookup with metadata
const res = blobmap.getWithMeta(1n);
console.log(res);
// { payload: <Buffer 69 6e 6c 69 6e 65>, hotMeta: 10, isInline: true }

// Prune based on hot metadata
blobmap.prune((key, hotMeta) => hotMeta > 15);

// Relocatable binary image persistence
blobmap.saveImage('./data.expanse');
const reloaded = ExpanseBlobMap.openImage('./data.expanse');

Concurrent Structures (SyncExpanseMap / SyncExpanseSet)

import { SyncExpanseMap, SyncExpanseSet } from '@orieg/expanse';

const syncMap = new SyncExpanseMap();
syncMap.set(1000n, 42n);
console.log(syncMap.get(1000n)); // 42n
console.log(syncMap.next(1000n, true)); // { key: 1000n, value: 42n }

SyncExpanseMap's get, has, size, first, last, next and prev run on the optimistic path: they take no writer lock unless a read exhausts its bounded retries, in which case it falls back to the writer-excluding path. The protocol is blocking optimistic lock coupling, not lock-free. rank, select, countRange, keys, values and entries, and the ordered reads of SyncExpanseSet, run with writers excluded.

SyncExpanseMap also has atomic read-modify-write (#1194). compareExchange(key, expected, newValue) stores newValue only if the key's current value is expected, with null meaning absent on both sides ((null, v) inserts if absent, (e, null) removes if equal), and returns { stored, observed }: observed is the value the compare saw (null if absent), so a failed call hands back the value to retry against. getOrInsert(key, value) stores value only if the key is absent and returns the existing value, or null when it stored (the convention of set). update(key, fn) stores fn(current) (null removes the key) and returns the replaced value; it is a loop over compareExchange that calls fn again with the observed value when the exchange loses, so fn may run more than once per call and must be free of side effects, and an exception from fn propagates with the key unchanged. The addon's objects belong to one JavaScript thread, so an exchange loses only to a write made between the read and the exchange (for example from inside fn).

syncMap.update(7n, (cur) => (cur ?? 0n) + 1n); // atomic increment
syncMap.compareExchange(7n, 1n, null); // { stored: true, observed: 1n }: remove if equal to 1n

Concurrent string, bytes, ordered-bytes and blob maps

import {
  SyncExpanseStrMap,
  SyncExpanseBytesMap,
  SyncExpanseOrderedBytesMap,
  SyncExpanseBlobMap,
} from '@orieg/expanse';

const names = new SyncExpanseStrMap(); // NUL-free string keys, u64 values
names.set('alice', 1n);
names.update('alice', (cur) => (cur ?? 0n) + 1n); // atomic increment
names.next('alice'); // { key: ..., value: ... } | null, byte-lexicographical order

const raw = new SyncExpanseBytesMap(); // Buffer / Uint8Array / string keys, any bytes
raw.compareExchange(Buffer.from([0, 1]), null, 5n); // { stored: true, observed: null }

const ordered = new SyncExpanseOrderedBytesMap();
ordered.first(); // { key: Buffer, value: bigint } | null

const blobs = new SyncExpanseBlobMap();
blobs.set(1n, Buffer.alloc(64, 1), 5 /* hotMeta */);
blobs.get(1n); // Buffer
blobs.compareExchangeMeta(1n, 5, 6); // { stored: true, observed: 5 }
blobs.getOrInsert(2n, 'payload longer than seven bytes'); // null: stored

All four take the same compareExchange(key, expected, newValue) and getOrInsert forms as SyncExpanseMap (null is absent; { stored, observed }), and update(key, fn) on the three word-valued maps. The string map rejects a key with an embedded NUL. get, has, the point writes and the string map's first / last / next / prev run on the optimistic path; the ordered-bytes map's first / last / next / prev run with writers excluded, so each call blocks concurrent writers. Blob reads return owned Buffers; getWithMeta adds isInline, and a payload of any length keeps the hot metadata it was written with (#1548). The blob compareExchange, compareExchangeMeta and getOrInsert throw when the map refuses the value (metadata over 24 bits, a refusing arena), where the C forms return a status. Like SyncExpanseMap, the objects belong to one JavaScript thread, so an exchange loses only to a write made in between (for example inside fn).

License

Dual-licensed under MIT and Apache-2.0.

版本与发布记录

@orieg/expanse 共发布 24 个版本,首次发布于 2026/08/24;过去 12 个月发布了 24 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.17.0latest2026/11/021 天前
0.16.02026/10/268 天前
0.15.02026/10/2014 天前
0.14.02026/10/1816 天前
0.13.02026/10/1519 天前
0.12.02026/10/1321 天前

关于 @orieg/expanse 的常见问题

@orieg/expanse 是什么?

Clean-room,纯rust现代Judy数组和数字尝试,具有缓存线调优的节点几何,内联值打包,slab竞技场以及node .js, Bun和Deno的OCC并发性。

如何安装 @orieg/expanse?

在项目目录运行 npm install @orieg/expanse,或使用 pnpm add / yarn add / bun add。

@orieg/expanse 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@orieg/expanse 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@orieg/expanse 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >= 20.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@orieg/expanse 可以商用吗?

@orieg/expanse 使用 MIT OR Apache-2.0 许可。可在 MIT、Apache-2.0 中任选其一遵守。选择 MIT 时,宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@orieg/expanse 还在维护吗?

最新版本 0.17.0 发布于 2026/11/02(1 天前);至今共发布 24 个版本,其中过去 12 个月发布了 24 个;首次发布于 2026/08/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。