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

@loradb/lora-node

v0.6.0

Published

Node.js / TypeScript bindings for the Lora in-memory graph database

Readme

lora-node

Node.js / TypeScript bindings for the Lora graph engine. The package exposes a first-class typed API: query results are modelled as discriminated unions, temporal values carry kind tags, and the Database class is strongly typed in both directions (params and rows).

Non-blocking: execute() dispatches each query to the libuv threadpool via napi::Task. The JS event loop stays free for the full duration of a query — a 2 000-node MATCH happily interleaves with setImmediate ticks on the main thread (proven by a dedicated vitest).

Status: prototype / feasibility check. Not published to npm.

Install (local dev)

cd crates/lora-node
npm install
npm run build   # builds the Rust cdylib + TypeScript declarations
npm test        # runs the vitest suite

The npm run build:native step uses @napi-rs/cli and produces a platform-specific lora-node.<platform>-<arch>.node artifact next to package.json.

Usage

lora-node is async-only — the sole initialization pattern is createDatabase(...). There is no synchronous constructor and no Database.create() static; Database is a type-only export.

import { createDatabase, isNode, type LoraNode } from "lora-node";

const db = await createDatabase(); // in-memory by default
await db.execute("CREATE (:Person {name: $n, age: $a})", { n: "Alice", a: 30 });

const res = await db.execute<{ n: LoraNode }>("MATCH (n:Person) RETURN n");
for (const row of res.rows) {
  if (isNode(row.n)) {
    console.log(row.n.properties.name);
  }
}

The initialization rule is:

import { createDatabase } from "lora-node";

const inMemory = await createDatabase();           // in-memory only
const defaultPersistent = await createDatabase("app"); // ./app.loradb
const nestedPersistent = await createDatabase("app", {
  databaseDir: "./data",
  syncMode: "group",                              // default
});                                                // ./data/app.loradb

Passing a database name enables persistence. Use databaseDir when you want a directory other than the current working directory. The default syncMode: "group" writes WAL bytes before execute() resolves and batches fsyncs for write-heavy workloads. That recovers from ordinary process death, while syncMode: "perCommit" also protects each committed write against power loss before execute() resolves. Call await db.sync() before copying the portable .loradb archive while the database is still open.

Node also has an archive-backed convenience overload:

import { createDatabase } from "lora-node";

const db = await createDatabase("app", { databaseDir: "./data" });

The database name is validated and resolved under databaseDir, or under the current working directory when no directory is supplied, appending .loradb to the basename when needed. Relative paths resolve from the current working directory. This is a Node-only initialization convenience; the query surface, shared types, and async method signatures still match lora-wasm.

Persistent opens for the same resolved archive path in one Node process share a single live native engine. Call db.dispose() when you need to release a handle eagerly; cross-process opens of the same archive are blocked to prevent split-brain writers.

For explicit WAL directories with managed snapshots, use openWalDatabase:

import { openWalDatabase } from "lora-node";

const db = await openWalDatabase({
  walDir: "./data/wal",
  snapshotDir: "./data/snapshots",
  snapshotEveryCommits: 1000,
  snapshotKeepOld: 2,
});

snapshotOptions accepts the same compression/encryption options as saveSnapshot.

Snapshots

saveSnapshot(path) writes the current graph to a local file. Plain strings are always treated as paths. Calling saveSnapshot() returns a Node Buffer; object formats such as { format: "base64" }, { format: "arrayBuffer" }, { format: "uint8Array" }, and { format: "stream" } return in-memory snapshot data in that shape. { format: "path", path } accepts either a path string or file: URL.

loadSnapshot accepts a NodeSnapshotSource: a filesystem path, file: URL, HTTP(S) or data: URL, Buffer, Uint8Array, ArrayBuffer, Node Readable, web ReadableStream, or async iterable of byte chunks.

import { readFile } from "node:fs/promises";
import { createReadStream } from "node:fs";
import { pathToFileURL } from "node:url";
import { createDatabase } from "lora-node";

const db = await createDatabase();
await db.execute("CREATE (:Person {name: 'Alice'})");
await db.saveSnapshot("./graph.lorasnap");
const bytes = await db.saveSnapshot();
const base64 = await db.saveSnapshot({ format: "base64" });
const stream = await db.saveSnapshot({ format: "stream" });

await db.loadSnapshot("./graph.lorasnap");
await db.loadSnapshot(pathToFileURL("./graph.lorasnap"));
await db.loadSnapshot(await readFile("./graph.lorasnap"));
await db.loadSnapshot(createReadStream("./graph.lorasnap"));
await db.loadSnapshot(bytes);
await db.loadSnapshot(stream);
await db.loadSnapshot(new URL("https://example.com/graph.lorasnap"));

Typed value model

| TS type | Runtime shape | |-------------------------|-------------------------------------------------------------------------------| | null/boolean/number/string | pass-through JS primitives | | LoraValue[] / object | homogeneous arrays and nested records | | LoraNode | { kind: "node", id, labels, properties } | | LoraRelationship | { kind: "relationship", id, startId, endId, type, properties } | | LoraPath | { kind: "path", nodes: number[], rels: number[] } | | LoraDateLoraDuration | { kind: "date", iso: "YYYY-MM-DD" } etc. | | LoraPoint | Discriminated union on srid, see below |

LoraPoint is a discriminated union over the four supported CRSes:

| Shape | Meaning | |--------------------------------------------------------------------------------------------------------------|----------------------| | { kind: "point", srid: 7203, crs: "cartesian", x, y } | Cartesian 2D | | { kind: "point", srid: 9157, crs: "cartesian-3D", x, y, z } | Cartesian 3D | | { kind: "point", srid: 4326, crs: "WGS-84-2D", x, y, longitude, latitude } | WGS-84 2D | | { kind: "point", srid: 4979, crs: "WGS-84-3D", x, y, z, longitude, latitude, height } | WGS-84 3D |

Helper constructors (date("2025-01-15"), cartesian(1, 2), cartesian3d(1, 2, 3), wgs84(lon, lat), wgs84_3d(lon, lat, height), duration("P1M"), …) and narrowing guards (isNode, isRelationship, isPath, isPoint, isTemporal) are exported from lora-node.

distance() on WGS-84-3D points ignores height — see functions reference for the full spatial reference and known limitations.

Architecture

lora-database (Rust)
   └── lora-node (crate, cdylib)        <- napi-rs bindings, AsyncTask
          └── ts/index.ts                 <- strongly-typed async wrapper
                 └── ../shared-ts/types.ts  <- shared TS contract (with lora-wasm)

Query execution path:

JS main thread         libuv threadpool             Rust
──────────────         ───────────────────          ────────────────
db.execute(…)   ──►   ExecuteTask::compute()   ──►  parser → analyzer →
                                                    compiler → executor →
                                                    storage
             ◄──   resolve() wraps serde_json::Value
                   into JsUnknown and resolves the Promise

The Rust crate is added to the workspace root (Cargo.toml). The Node side is self-contained inside this directory. Only sub-millisecond operations (clear, nodeCount, relationshipCount) stay synchronous inside napi; the TS wrapper still exposes them as Promise-returning methods to keep the API identical to lora-wasm.

Errors

db.execute(...) throws LoraError with a narrowed code:

  • LORA_ERROR — parse / analyze / execute failure
  • INVALID_PARAMS — a param value could not be mapped to a LoraValue

Known limitations

  • Concurrent writes. Each execute() hops through the threadpool; read queries can share the store read lock, while writes serialize on the store write lock. Firing many concurrent write queries against the same Database (e.g. 2 000 parallel CREATEs via Promise.all) works but queues behind that write lock. Prefer await-in-a-loop or a single batched query for heavy write workloads.
  • I64 precision. Integer values above Number.MAX_SAFE_INTEGER (2^53) are returned as JS number and lose precision. A bigint-aware path would require extending the value serializer.
  • Cancellation. The napi Task abstraction does not support cancellation once dispatched; a runaway query runs to completion.
  • WAL surface. Node persistence exposes archive-backed initialization, syncMode: "group" | "perCommit", and db.sync(). Checkpoint, truncate, and status controls are not exposed yet.
  • Archive ownership. One archive can only be open by one writer process at a time. Multiple Node handles in the same process share the same live engine; a second process is rejected while the first holds the archive lock.