TypeScript client SDK for @hamicek/noex-server. Works in the browser (native WebSocket) and Node.js (ws).
- 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 generics —
BucketAPI<T>for fully typed records - Zero runtime dependencies — ESM only, <5 kB gzip target
npm install @hamicek/noex-clientRequires Node.js >= 20. No peer dependencies. For Node.js usage, install ws separately.
import { NoexClient } from '@hamicek/noex-client';
// Use wss:// in production (see Production section below)
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();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();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.
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,
});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 }Gracefully closes the connection. Rejects all pending requests, clears subscriptions, and stops any reconnect loop.
Current connection state: 'connecting' | 'connected' | 'reconnecting' | 'disconnected'.
Shorthand for client.state === 'connected'.
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 |
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 |
interface ReconnectOptions {
maxRetries?: number; // default: Infinity
initialDelayMs?: number; // default: 1000
maxDelayMs?: number; // default: 30000
backoffMultiplier?: number; // default: 2
jitterMs?: number; // default: 500
}Access via client.store.
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 typedSubscribe 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.
Cancel a subscription by its server-assigned ID.
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.
List all defined buckets and their count.
Retrieve store statistics (records, indexes, queries, persistence, TTL).
Access via client.store.bucket(name).
| Method | Returns |
|---|---|
insert(data) |
Promise<T & RecordMeta> |
get(key) |
Promise<(T & RecordMeta) | null> |
update(key, data) |
Promise<T & RecordMeta> |
delete(key) |
Promise<void> |
| 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>> |
| Method | Returns |
|---|---|
sum(field, filter?) |
Promise<number> |
avg(field, filter?) |
Promise<number> |
min(field, filter?) |
Promise<number | null> |
max(field, filter?) |
Promise<number | null> |
| Method | Description |
|---|---|
clear() |
Remove all records from the bucket |
Access via client.rules. Available only when the server has a rules engine configured.
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',
);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();Subscribe to real-time rule events by topic pattern:
const unsub = await client.rules.subscribe('user.*', (event, topic) => {
console.log(`${topic}:`, event);
});
unsub();const stats = await client.rules.stats();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.
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 is enabled by default. When the connection drops unexpectedly:
- All pending requests are rejected with
DisconnectedError - The client enters
'reconnecting'state and emitsreconnectingevents - Exponential backoff with jitter determines the delay between attempts
- On successful reconnect:
- Auto-login is performed (if configured)
- All active subscriptions are restored with fresh data
'reconnected'event is emitted
- If max retries are exhausted, the client enters
'disconnected'state
Calling disconnect() at any point stops the reconnect loop immediately.
The noex-server itself listens on plain ws://. In production, terminate TLS at a reverse proxy (nginx, Caddy, etc.) and connect with wss://:
const client = new NoexClient('wss://api.example.com');
await client.connect();No client-side configuration is needed beyond changing the URL scheme — the underlying WebSocket implementation handles TLS transparently.
See the noex-server Production Considerations for a reverse proxy configuration example.
MIT