TypeScript client for WaveHouse — schema-aware real-time API gateway for ClickHouse. One runtime dependency (eventsource-parser, ~1.4 KB gzipped, itself dependency-free).
npm install @wavehouse/sdkRequires Node 22 or newer — the only line this SDK is tested against; Node 18 and 20 are past upstream end-of-life. Neither browsers nor Node need a polyfill: streaming runs on fetch, like the rest of the SDK — see Runtime support.
This works in any framework that uses a bundler — React, Vue, Svelte, Angular, Astro, SolidJS, or plain Vite — with import { createClient } from '@wavehouse/sdk'. The SDK and the server share the SSE wire protocol, so move them as a pair — a skewed pair delivers no usable rows and raises no error (details).
The package is published with npm provenance — npm shows a verified provenance badge on the package page, and npm audit signatures checks the attestation against this repository's signed build.
No bundler, no npm, no framework required — drop the SDK straight into an HTML file you deploy over FTP, object storage, or any static host.
ES module (recommended). Modern browsers run type="module" natively:
<script type="module">
import { createClient } from 'https://esm.sh/@wavehouse/sdk';
const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
const { data, error } = await wh.from('clicks').select('page').limit(10);
console.log(data ?? error);
</script>Pin a version for production (https://esm.sh/@wavehouse/sdk@0.2.0). jsDelivr (https://cdn.jsdelivr.net/npm/@wavehouse/sdk/+esm) and unpkg (https://unpkg.com/@wavehouse/sdk?module) serve the same ES module. The SDK and the server share the SSE wire protocol, so pin them as a pair — a skewed pair delivers no usable rows and raises no error.
Classic global (<script src>). For pages that can't use ES modules, the bundled IIFE build attaches everything to a WaveHouse global:
<script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
<script>
const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
</script>Versioning. A bare CDN URL serves the latest published release; pin for production (@wavehouse/sdk@0.2.0) or float on a range (@0 for the newest 0.x, @0.2 for 0.2.x). Builds from main are published under the dev tag — @wavehouse/sdk@dev — for trying unreleased changes. Whichever you choose, move the SDK and the server together: they share the SSE wire protocol, and a skewed pair delivers no usable rows and raises no error — a newer SDK drops every row, an older one hands you an event per frame with data: undefined.
Streaming (.stream()) runs on fetch, so it works in both forms with no polyfill.
import { createClient } from '@wavehouse/sdk';
const wh = createClient({
baseURL: 'http://localhost:8080',
auth: async () => getAccessToken(), // omit for public access
});baseURL may include a path prefix (https://app.example.com/api/wavehouse) when WaveHouse is served under one — see Serving under a path prefix.
// Structured query with the builder
const { data, error } = await wh
.from('clicks')
.select('page', 'button')
.where('score', '>', 10)
.count('*', 'total')
.groupBy('page', 'button')
.orderBy('total', 'desc')
.limit(50)
.timeRange('received_timestamp', '1h');
// Raw SQL (requires admin role)
const { data } = await wh.sql('SELECT page, count() FROM clicks GROUP BY page LIMIT 10');
// Named pipe
const { data } = await wh.pipe('top_pages', { start_date: '2026-01-01', limit: 50 });const { error } = await wh.from('clicks').insert({ page: '/home', button: 'signup', score: 42.5 });// Callback pattern
const stream = wh.from('clicks').stream();
const unsub = stream.subscribe({
next: (event) => console.log(event.data),
status: (state) => console.log(state), // 'connecting' | 'live' | 'reconnecting' | 'closed'
});
// Async iterator pattern
for await (const event of wh.from('clicks').stream()) {
console.log(event.table, event.data);
}let result = await wh.from('clicks').select().orderBy('received_timestamp', 'desc').limit(100);
while (result.hasMore && result.next) {
result = await result.next();
}Pass a Database type for autocomplete on table names and row shapes:
interface Database {
clicks: { page: string; button: string; score: number; received_timestamp: string };
users: { id: string; name: string; email: string };
}
const wh = createClient<Database>({ baseURL: '...' });
const { data } = await wh.from('clicks').select('page').limit(10);
// data: Array<{ page: string; button: string; ... }> | nullAsync request methods return a Result<T> object — destructure it as { data, error } — and never throw for anything the server returns. .stream() and .liveQuery() return controllers instead, reporting failures through the subscriber's error callback.
The SDK does throw on caller and environment errors: a non-absolute baseURL (REST calls reject with a TypeError; streams report a terminal SSE_CONNECT_ERROR), .stream() / .liveQuery() in a runtime with no global fetch and no options.fetch, and an auth callback that rejects — a token-refresh failure propagates out of the REST call, and on a stream is reported as a retryable SSE_AUTH_ERROR. A dropped stream is re-dialed by the SDK, on a jittered backoff; only non-retryable failures end it. One more exception can escape an SDK call, though it is yours rather than ours: a subscriber's status handler throwing on the first, synchronous .subscribe() / .liveQuery() call — see Error Handling.
const { data, error } = await wh.from('clicks').fetch();
if (error) {
console.error(error.code, error.message, error.retryable);
return;
}
// data is guaranteed non-null hereGenerate TypeScript types from your live WaveHouse schema. The package ships a wavehouse-codegen bin, so after installing you can run it with npx:
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.tsThis introspects /v1/ops/schema, maps ClickHouse column types to TypeScript, and outputs a Database interface you can pass to createClient<Database>(). /v1/ops/schema is admin-only — pass an admin-role token with --auth <jwt> (or -a) against any non-dev policy. Over a nested settings directory the route admits the operator key alone: pass it with --operator-key <key> (or -k) and name the tenant with --tenant <id> (or -t); without --tenant it reads tenant 0's schema.
Clone, then pnpm install (Node 22 or newer). make ci runs everything CI runs; make fix formats and applies safe lint fixes.
Unit tests mock all HTTP calls — no backend required.
pnpm test # run unit tests (no backend needed)
pnpm run test:watch # watch mode
pnpm run test:coverage # coverage report, gated at a 40% statement floormake smoke imports and require()s the built package by name (through its exports map) and runs the IIFE bundle as a classic script, at the oldest Node that engines.node admits, and checks that all three export the same names. It also type-checks a CommonJS and an ESM consumer of the package with tsc --module node16, so a CommonJS project ("type": "commonjs") gets its own .d.cts declarations rather than TS1479. It downloads that Node once into .bin/. pnpm run smoke:dist runs the same check on the Node you are running.
Report SDK bugs at Wave-RF/wavehouse-sdk-js/issues. Server bugs go to Wave-RF/WaveHouse. Until this repository existed the SDK lived in the WaveHouse repository under clients/ts, and its history up to 0.2.0 is carried over here.
See the full SDK API Reference for detailed documentation of every method, type, and option.
Apache-2.0 © Wave RF — see LICENSE.