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

@hamicek/noex-client

v0.1.0

Published

TypeScript client SDK for @hamicek/noex-server

Readme

@hamicek/noex-client

TypeScript client SDK for @hamicek/noex-server. Works in the browser (native WebSocket) and Node.js (ws).

Features

  • Store CRUD with typed bucket API and cursor pagination
  • Reactive subscriptions — subscribe to server-side queries, receive push updates via callbacks
  • Transactions — atomic multi-bucket operations
  • Rules engine proxy — emit events, manage facts, subscribe to rule matches
  • Token-based auth with automatic login on connect and reconnect
  • Automatic reconnect with exponential backoff, jitter, and subscription recovery
  • Heartbeat — automatic pong responses to server ping
  • Type-safe genericsBucketAPI<T> for fully typed records
  • Zero runtime dependencies — ESM only, <5 kB gzip target

Installation

npm install @hamicek/noex-client

Requires Node.js >= 20. No peer dependencies. For Node.js usage, install ws separately.

Quick Start

import { NoexClient } from '@hamicek/noex-client';

const client = new NoexClient('ws://localhost:8080');
await client.connect();

// Store CRUD
const users = client.store.bucket('users');
const alice = await users.insert({ name: 'Alice' });
const all = await users.all();

// Reactive subscription
const unsub = await client.store.subscribe('all-users', (data) => {
  console.log('Updated:', data);
});

// Rules
await client.rules.emit('user.created', { userId: alice.id });

// Cleanup
unsub();
await client.disconnect();

Node.js

In Node.js there is no built-in WebSocket. Pass the ws package via options:

import { NoexClient } from '@hamicek/noex-client';
import WebSocket from 'ws';

const client = new NoexClient('ws://localhost:8080', { WebSocket });
await client.connect();

Auth and Reconnect

const client = new NoexClient('ws://localhost:8080', {
  WebSocket,
  auth: { token: 'my-jwt-token' },
  reconnect: {
    maxRetries: 10,
    initialDelayMs: 500,
    maxDelayMs: 15_000,
  },
  requestTimeoutMs: 5_000,
});

client.on('reconnecting', (attempt) => {
  console.log(`Reconnecting... attempt ${attempt}`);
});

client.on('reconnected', () => {
  console.log('Reconnected! Subscriptions restored.');
});

await client.connect();

When auth.token is set and the server requires authentication, the client automatically sends auth.login after connecting and after every reconnect.


API

NoexClient

new NoexClient(url, options?)

Creates a client instance. Does not open a connection — call connect() to start.

const client = new NoexClient('ws://localhost:8080', {
  auth: { token: 'jwt' },
  reconnect: true,
  requestTimeoutMs: 10_000,
  connectTimeoutMs: 5_000,
  WebSocket,
  heartbeat: true,
});

client.connect(): Promise<WelcomeInfo>

Opens the WebSocket connection and waits for the server welcome message. If auth.token is configured and the server requires authentication, login is performed automatically.

const welcome = await client.connect();
// { version: '1.0.0', serverTime: 1706745600000, requiresAuth: true }

client.disconnect(): Promise<void>

Gracefully closes the connection. Rejects all pending requests, clears subscriptions, and stops any reconnect loop.

client.state: ConnectionState

Current connection state: 'connecting' | 'connected' | 'reconnecting' | 'disconnected'.

client.isConnected: boolean

Shorthand for client.state === 'connected'.

client.on(event, handler): Unsubscribe

Subscribe to client lifecycle events. Returns an unsubscribe function.

| Event | Handler signature | Description | |-------|-------------------|-------------| | 'connected' | () => void | Connection established (initial or reconnect) | | 'disconnected' | (reason: string) => void | Connection lost or closed | | 'reconnecting' | (attempt: number) => void | Reconnect attempt starting | | 'reconnected' | () => void | Successfully reconnected | | 'error' | (error: Error) => void | Transport or reconnect error | | 'welcome' | (info: WelcomeInfo) => void | Welcome message received from server |


ClientOptions

interface ClientOptions {
  auth?: { token: string };
  reconnect?: boolean | ReconnectOptions;
  requestTimeoutMs?: number;
  connectTimeoutMs?: number;
  WebSocket?: WebSocketConstructor;
  heartbeat?: boolean;
}

| Option | Type | Default | Description | |--------|------|---------|-------------| | auth.token | string | — | Token for automatic login after connect | | reconnect | boolean \| ReconnectOptions | true | Enable automatic reconnect with exponential backoff | | requestTimeoutMs | number | 10000 | Timeout for individual request/response round-trips | | connectTimeoutMs | number | 5000 | Timeout for WebSocket connection and welcome message | | WebSocket | WebSocketConstructor | globalThis.WebSocket | WebSocket implementation (pass ws in Node.js) | | heartbeat | boolean | true | Automatically respond to server ping messages |

ReconnectOptions

interface ReconnectOptions {
  maxRetries?: number;       // default: Infinity
  initialDelayMs?: number;   // default: 1000
  maxDelayMs?: number;       // default: 30000
  backoffMultiplier?: number; // default: 2
  jitterMs?: number;         // default: 500
}

StoreAPI

Access via client.store.

store.bucket(name): BucketAPI

Returns a BucketAPI handle for the named bucket. Does not make a request — the bucket handle is a thin wrapper that attaches the bucket name to each operation.

const users = client.store.bucket('users');

For type-safe usage with generics:

interface User {
  id: string;
  name: string;
  role: string;
}

const users = client.store.bucket<User>('users');
const alice = await users.insert({ name: 'Alice', role: 'admin' });
// alice: User & RecordMeta — fully typed

store.subscribe(query, callback): Promise<Unsubscribe>

store.subscribe(query, params, callback): Promise<Unsubscribe>

Subscribe to a reactive server-side query. The callback receives the initial data immediately and is called again whenever the query result changes on the server.

const unsub = await client.store.subscribe('all-users', (users) => {
  console.log('Users:', users);
});

// With parameters
const unsub = await client.store.subscribe(
  'users-by-role',
  { role: 'admin' },
  (admins) => console.log('Admins:', admins),
);

// Unsubscribe
unsub();

Subscriptions survive reconnect — after a successful reconnect the client automatically resubscribes and delivers fresh data to the callback.

store.unsubscribe(subscriptionId): Promise<void>

Cancel a subscription by its server-assigned ID.

store.transaction(operations): Promise<TransactionResult>

Execute multiple store operations atomically.

const result = await client.store.transaction([
  { op: 'get', bucket: 'users', key: 'user-1' },
  { op: 'update', bucket: 'users', key: 'user-1', data: { credits: 400 } },
  { op: 'insert', bucket: 'logs', data: { action: 'credit_update' } },
]);
// result.results: [{ index: 0, data: ... }, { index: 1, data: ... }, ...]

Supported ops: get, insert, update, delete, where, findOne, count.

store.buckets(): Promise<BucketsInfo>

List all defined buckets and their count.

store.stats(): Promise<StoreStats>

Retrieve store statistics (records, indexes, queries, persistence, TTL).


BucketAPI

Access via client.store.bucket(name).

CRUD

| Method | Returns | |--------|---------| | insert(data) | Promise<T & RecordMeta> | | get(key) | Promise<(T & RecordMeta) \| null> | | update(key, data) | Promise<T & RecordMeta> | | delete(key) | Promise<void> |

Queries

| Method | Returns | |--------|---------| | all() | Promise<(T & RecordMeta)[]> | | where(filter) | Promise<(T & RecordMeta)[]> | | findOne(filter) | Promise<(T & RecordMeta) \| null> | | count(filter?) | Promise<number> | | first(n) | Promise<(T & RecordMeta)[]> | | last(n) | Promise<(T & RecordMeta)[]> | | paginate({ limit, after? }) | Promise<PaginatedResult<T>> |

Aggregation

| Method | Returns | |--------|---------| | sum(field, filter?) | Promise<number> | | avg(field, filter?) | Promise<number> | | min(field, filter?) | Promise<number \| null> | | max(field, filter?) | Promise<number \| null> |

Bulk

| Method | Description | |--------|-------------| | clear() | Remove all records from the bucket |


RulesAPI

Access via client.rules. Available only when the server has a rules engine configured.

Events

const event = await client.rules.emit('user.created', { userId: '123' });
// event: RulesEvent { id, topic, data, timestamp, source, ... }

// With correlation/causation IDs
const event = await client.rules.emit(
  'order.completed',
  { orderId: '456' },
  'correlation-id',
  'causation-id',
);

Facts

await client.rules.setFact('user:1:status', 'active');
const status = await client.rules.getFact('user:1:status');
const deleted = await client.rules.deleteFact('user:1:status');
const facts = await client.rules.queryFacts('user:*:status');
const all = await client.rules.getAllFacts();

Subscriptions

Subscribe to real-time rule events by topic pattern:

const unsub = await client.rules.subscribe('user.*', (event, topic) => {
  console.log(`${topic}:`, event);
});

unsub();

Stats

const stats = await client.rules.stats();

AuthAPI

Access via client.auth.

const session = await client.auth.login('jwt-token');
// session: { userId, roles, metadata?, expiresAt? }

const current = await client.auth.whoami();
await client.auth.logout();

When auth.token is set in ClientOptions, login is performed automatically after connect and after each reconnect.


Error Handling

All errors from the server are propagated as NoexClientError with a machine-readable code:

import { NoexClientError, TimeoutError, DisconnectedError } from '@hamicek/noex-client';

try {
  await client.store.bucket('users').insert({ name: '' });
} catch (err) {
  if (err instanceof NoexClientError) {
    switch (err.code) {
      case 'VALIDATION_ERROR': break;
      case 'UNAUTHORIZED': break;
      case 'NOT_FOUND': break;
      case 'RATE_LIMITED': break;
    }
  }
}

| Error class | Code | Description | |-------------|------|-------------| | NoexClientError | (server code) | Base class for all server errors | | TimeoutError | TIMEOUT | Request did not receive a response within requestTimeoutMs | | DisconnectedError | DISCONNECTED | Attempted to send while not connected, or connection was lost |

Pending requests at the time of a disconnect are rejected with DisconnectedError. They are not retried automatically — the server does not persist request state across connections and automatic retry of non-idempotent operations (insert, emit) could cause duplicates.


Reconnect Behavior

Reconnect is enabled by default. When the connection drops unexpectedly:

  1. All pending requests are rejected with DisconnectedError
  2. The client enters 'reconnecting' state and emits reconnecting events
  3. Exponential backoff with jitter determines the delay between attempts
  4. On successful reconnect:
    • Auto-login is performed (if configured)
    • All active subscriptions are restored with fresh data
    • 'reconnected' event is emitted
  5. If max retries are exhausted, the client enters 'disconnected' state

Calling disconnect() at any point stops the reconnect loop immediately.


License

MIT