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

@qualithm/rqlite-client

v0.2.3

Published

Native rqlite client for JavaScript and TypeScript runtimes.

Readme

Rqlite Client

CI codecov npm

Native rqlite client for JavaScript and TypeScript runtimes. Zero runtime dependencies — uses native fetch.

Features

  • Execute & Query — parameterised writes and reads
  • Batch operations — multiple statements in a single HTTP call
  • Transactions — atomic multi-statement execution
  • Unified requests — mixed read/write batches via /db/request
  • Consistency levelsnone, weak, strong
  • Freshness control — bounded staleness for none consistency reads
  • Leader redirect — automatic 301/307 redirect following
  • Retry with backoff — configurable exponential backoff
  • Authentication — HTTP basic auth
  • TLS & mTLS — HTTPS via native fetch; custom fetch injection for client certificates
  • Cluster inspection — node status, readiness, and cluster listing
  • Result typesResult<T, E> discriminated unions, no thrown exceptions
  • Typed errorsConnectionError, QueryError, AuthenticationError with isError() guards
  • Zero dependencies — uses native fetch
  • Cross-runtime — Bun, Node.js 20+, Deno

Installation

bun add @qualithm/rqlite-client
# or
npm install @qualithm/rqlite-client

Quick Start

import { createRqliteClient } from "@qualithm/rqlite-client"

const client = createRqliteClient({ host: "localhost:4001" })

// Execute a write
await client.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
await client.execute("INSERT INTO users(name) VALUES(?)", ["Alice"])

// Query
const result = await client.query("SELECT * FROM users")
if (result.ok) {
  console.log(result.value.columns) // ["id", "name"]
  console.log(result.value.values) // [[1, "Alice"]]
}

Compatibility

| rqlite version | Client version | Status | | -------------- | -------------- | ------ | | 9.x | 0.x | Tested |

The integration test suite runs against rqlite via Docker. Override the version with the RQLITE_VERSION environment variable:

RQLITE_VERSION=9.4.5 docker compose -f docker-compose.test.yaml up -d

Use serverVersion() at runtime to check the connected server:

const ver = await client.serverVersion()
if (ver.ok) console.log(ver.value) // "v9.4.5"

Usage

Configuration

import { createRqliteClient } from "@qualithm/rqlite-client"

const client = createRqliteClient({
  host: "localhost:4001",
  tls: false, // use HTTPS
  auth: {
    // basic authentication
    username: "admin",
    password: "secret"
  },
  timeout: 10_000, // default request timeout (ms)
  consistencyLevel: "weak", // default for queries
  freshness: {
    // for "none" consistency
    freshness: "5s",
    freshnessStrict: true
  },
  followRedirects: true, // follow leader redirects
  maxRetries: 3, // retry attempts for transient failures
  maxRedirects: 5, // redirect attempts during leader election
  retryBaseDelay: 100, // backoff base delay (ms)
  fetch: customFetch // custom fetch for mTLS (see examples/mtls.ts)
})

Custom Fetch (mTLS)

Supply a custom fetch function to enable mTLS or other advanced transport options. The custom function receives the same arguments as the global fetch and must return a Promise<Response>.

// Node.js with undici
import { Agent, fetch as undiciFetch } from "undici"

const agent = new Agent({
  connect: {
    ca: readFileSync("certs/ca.pem"),
    cert: readFileSync("certs/client-cert.pem"),
    key: readFileSync("certs/client-key.pem")
  }
})

const client = createRqliteClient({
  host: "rqlite.example.com:4001",
  tls: true,
  fetch: (input, init) => undiciFetch(input, { ...init, dispatcher: agent })
})

See examples/mtls.ts for Bun and Deno examples.

Execute (Writes)

// Simple statement
const result = await client.execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT)")

// Parameterised statement
const insert = await client.execute("INSERT INTO foo(name) VALUES(?)", ["bar"])
if (insert.ok) {
  console.log(insert.value.lastInsertId) // 1
  console.log(insert.value.rowsAffected) // 1
}

// Batch execute
const batch = await client.executeBatch([
  ["INSERT INTO foo(name) VALUES(?)", "one"],
  ["INSERT INTO foo(name) VALUES(?)", "two"]
])

// Queue mode — returns immediately, write applied asynchronously
await client.execute("INSERT INTO foo(name) VALUES(?)", ["queued"], { queue: true })

// Wait mode — wait for queued write to be applied
await client.execute("INSERT INTO foo(name) VALUES(?)", ["waited"], { queue: true, wait: true })

Query (Reads)

// Simple query
const result = await client.query("SELECT * FROM foo")
if (result.ok) {
  console.log(result.value.columns) // ["id", "name"]
  console.log(result.value.types) // ["integer", "text"]
  console.log(result.value.values) // [[1, "bar"], [2, "baz"]]
}

// Parameterised query
const row = await client.query("SELECT * FROM foo WHERE id = ?", [1])

// With consistency level
const strong = await client.query("SELECT * FROM foo", undefined, { level: "strong" })

// Freshness for stale reads
const fresh = await client.query("SELECT * FROM foo", undefined, {
  level: "none",
  freshness: { freshness: "1s", freshnessStrict: true }
})

// Batch query
const results = await client.queryBatch([
  ["SELECT * FROM foo WHERE id = ?", 1],
  ["SELECT COUNT(*) FROM foo"]
])

Converting to Row Objects

Query results use arrays by default (matching the rqlite wire format). Use toRows() to convert to keyed objects when needed:

import { toRows } from "@qualithm/rqlite-client"

const result = await client.query("SELECT id, name FROM foo")
if (result.ok) {
  const rows = toRows(result.value) // [{ id: 1, name: "bar" }, ...]
}

Paginated Queries

Use queryPaginated() to iterate over large result sets in bounded-memory pages. It automatically appends LIMIT/OFFSET to your SQL and yields pages via an async generator:

import { toRowsPaginated } from "@qualithm/rqlite-client"

for await (const page of client.queryPaginated("SELECT * FROM large_table", [], {
  pageSize: 100
})) {
  console.log(page.rows.values.length, page.hasMore, page.offset)

  // Or convert to keyed row objects:
  const { rows } = toRowsPaginated(page)
  // rows → [{ id: 1, name: "Alice" }, ...]
}

You can also start from a custom offset:

for await (const page of client.queryPaginated("SELECT * FROM large_table", [], {
  pageSize: 50,
  offset: 200
})) {
  // starts from row 200
}

Transactions

// All statements succeed or all fail
const transfer = await client.executeBatch(
  [
    ["UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1],
    ["UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2]
  ],
  { transaction: true }
)

Unified Request (Mixed Read/Write)

const results = await client.requestBatch([
  ["INSERT INTO foo(name) VALUES(?)", "new"],
  ["SELECT * FROM foo"]
])

if (results.ok) {
  for (const r of results.value) {
    if (r.type === "execute") console.log(r.rowsAffected)
    if (r.type === "query") console.log(r.columns, r.values)
  }
}

Cluster Operations

// Node readiness
const ready = await client.ready()
if (ready.ok && ready.value.ready) {
  console.log("node is ready, leader:", ready.value.isLeader)
}

// Check readiness without requiring a leader (useful during elections)
await client.ready({ noleader: true })

// List cluster nodes
const nodes = await client.nodes()
if (nodes.ok) {
  for (const node of nodes.value) {
    console.log(node.id, node.leader ? "(leader)" : "", node.apiAddr)
  }
}

// Full node status
const status = await client.status()

Error Handling

All operations return Result<T, RqliteError> — no exceptions are thrown in normal operation.

const result = await client.query("SELECT * FROM foo")

if (!result.ok) {
  const error = result.error

  // Type narrowing with static guards
  if (ConnectionError.isError(error)) {
    console.log("network issue:", error.message, error.url)
  } else if (QueryError.isError(error)) {
    console.log("SQL error:", error.message)
  } else if (AuthenticationError.isError(error)) {
    console.log("auth failed:", error.message)
  }
}

Errors can also be matched by their tag property:

if (!result.ok) {
  switch (result.error.tag) {
    case "ConnectionError": // network, timeout, redirect
    case "QueryError": // SQL errors from rqlite
    case "AuthenticationError": // 401/403
  }
}

API Reference

Full API documentation is generated with TypeDoc:

bun run docs
# Output in docs/

Examples

See the examples/ directory for runnable examples:

| Example | Description | | ----------------------------------------------------- | ---------------------------------------------- | | basic-usage.ts | Connect, execute, and query | | batch-processing.ts | Batch insert, query, and mixed requests | | transactions.ts | Atomic multi-statement transactions | | authentication.ts | Basic auth and TLS | | mtls.ts | Custom fetch injection for mTLS | | cluster-failover.ts | Leader redirect, health checks, cluster status | | error-handling.ts | Result-based error handling and type narrowing |

bun run examples/basic-usage.ts

Development

Prerequisites

  • Bun (recommended), Node.js 20+, or Deno

Setup

bun install

Building

bun run build

Testing

bun run test              # unit tests
bun run test:integration  # against a real rqlite instance
bun run test:coverage     # with coverage report

Linting & Formatting

bun run lint
bun run format
bun run typecheck

Benchmarks

bun run bench

Publishing

The package is automatically published to NPM when CI passes on main. Update the version in package.json before merging to trigger a new release.

Licence

Apache-2.0