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

@hapnd/client

v0.2.0

Published

TypeScript SDK for the Hapnd event sourcing platform

Readme

@hapnd/client

TypeScript SDK for the Hapnd event sourcing platform.

Append events, query aggregate state, and let Hapnd handle projections, reducers, and all the infrastructure complexity — from any JavaScript runtime.

Installation

npm install @hapnd/client
# or
pnpm add @hapnd/client
# or
yarn add @hapnd/client

Requirements: Node.js 18+, ESM only. No CommonJS support.

Quick Start

import { createHapndClient } from "@hapnd/client";

const hapnd = createHapndClient("sk_live_your_api_key");

// Append an event
const result = await hapnd
  .aggregate("order_123")
  .append("OrderPlaced", { customerId: "cust_456", total: 99.99 });

console.log(result.eventId); // evt_abc123
console.log(result.version); // 1
console.log(result.state); // computed state (if a reducer is bound)

Full API

Client Creation

import { createHapndClient } from "@hapnd/client";

// Simple — just an API key
const hapnd = createHapndClient("sk_live_your_api_key");

// Or, With options
const hapnd = createHapndClient({
  apiKey: "sk_live_your_api_key",
  baseUrl: "https://hapnd-api.lightestnight.workers.dev", // default
  timeout: 30_000, // ms, default 30s

  resilience: {
    maxRetryAttempts: 3, // default
    retryDelay: 500, // base delay ms, default
    maxRetryDelay: 10_000, // max delay ms, default
    enableCircuitBreaker: true, // default
    circuitBreakerDuration: 30_000, // ms open before half-open
    circuitBreakerSamplingDuration: 60_000, // sliding window ms
    circuitBreakerFailureRatio: 0.5, // trip threshold (50%)
    circuitBreakerMinimumThroughput: 10, // min requests before tripping
  },
});

// Or if you'd prefer to disable all resilience (no retries, no circuit breaker)
const hapnd = createHapndClient({
  apiKey: "sk_live_your_api_key",
  resilience: false,
});

Appending Events

The aggregate type is inferred from the ID using the {type}_{id} or {type}-{id} convention. Use .withAggregateType() for non-conforming IDs.

// Single event — minimal
const result = await hapnd
  .aggregate("order_123")
  .append("OrderPlaced", { customerId: "cust_456" });

// Single event — with all options
const result = await hapnd
  .aggregate("order_123")
  .expectVersion(5) // optimistic concurrency
  .withCorrelation("corr_abc")
  .withCausation("evt_xyz")
  .withMetadata({ userId: "u_1" })
  .append("ItemAdded", { item: "Widget", price: 25.0 });

// Non-conforming ID — specify type explicitly
const result = await hapnd
  .aggregate("ORD-2024-00123")
  .withAggregateType("order")
  .append("OrderPlaced", { customerId: "cust_456" });

AppendResult:

{
  eventId: string;       // unique event ID
  aggregateId: string;
  aggregateType: string;
  version: number;       // version after this event
  timestamp: string;     // ISO 8601
  state?: unknown;       // computed state if reducer is bound
}

Batch Append

Atomically append multiple events in a single request:

const result = await hapnd
  .aggregate("order_123")
  .expectVersion(5)
  .appendMany([
    { eventType: "ItemAdded", data: { item: "Widget", price: 25.0 } },
    { eventType: "ItemAdded", data: { item: "Gadget", price: 15.0 } },
  ]);

AppendManyResult:

{
  aggregateId: string;
  aggregateType: string;
  startVersion: number;  // version of first event
  endVersion: number;    // version of last event
  timestamp: string;
  eventIds: string[];
  state?: unknown;
}

Querying State

interface OrderState {
  total: number;
  items: string[];
}

const state = await hapnd.aggregate("order_123").getState<OrderState>();

if (state === null) {
  // Aggregate doesn't exist, or no reducer is bound to this type
} else {
  console.log(state.version); // current version
  console.log(state.state.total); // typed state
}

AggregateState<T>:

{
  aggregateId: string;
  aggregateType: string;
  version: number;
  state: T;
  lastModified: string; // ISO 8601
}

Real-Time Subscriptions

Subscribe to projection state changes in real time over WebSocket:

const subscription = hapnd.subscribe({
  projections: [
    {
      id: "proj_orders",
      onUpdate: async (update) => {
        console.log(`Order ${update.aggregateId} updated:`, update.state);
      },
    },
  ],
  onError: async (error) => {
    console.error(`Stream error on ${error.projectionId}:`, error.error);
    error.action.reconnect(); // resume from last successful sequence
  },
});

Subscribe to multiple projections simultaneously — each runs on its own WebSocket:

const subscription = hapnd.subscribe({
  projections: [
    {
      id: "proj_orders",
      onUpdate: async (update) => {
        await updateDashboard(update.state);
      },
    },
    {
      id: "proj_inventory",
      onUpdate: async (update) => {
        await refreshStock(update.state);
      },
    },
  ],
  onError: async (error) => {
    console.error(`Stream ${error.projectionId} failed:`, error.error);
    error.action.reconnect();
  },
});

Error handling: When an onUpdate handler throws, the stream pauses and your onError handler is called. You must call exactly one action:

  • error.action.reconnect() — resume from the last successfully processed sequence
  • error.action.stop() — stop this projection's stream only; others continue
  • error.action.shutdown() — close the entire subscription

If no action is called within 30 seconds, the stream is stopped automatically.

Cleanup:

await subscription.close(); // gracefully closes all WebSocket connections

Per-Request Cancellation

All terminal methods accept an optional AbortSignal:

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);

const result = await hapnd
  .aggregate("order_123")
  .append(
    "OrderPlaced",
    { customerId: "cust_1" },
    { signal: controller.signal },
  );

Aggregate ID Convention

| ID | Inferred type | | -------------- | -------------------------------------------- | | order_123 | order | | cart-abc | cart | | CUSTOMER_456 | customer | | ORD-2024-001 | ord (or use .withAggregateType("order")) |

Rules: first separator (_ or -) wins; type portion normalised to lowercase; must start with a letter.

Disposal

hapnd.dispose();
// Aborts in-flight requests and resets circuit breaker state

Error Handling

All errors extend HapndError. Use instanceof to handle specific cases:

import {
  HapndConcurrencyError,
  HapndApiError,
  HapndCircuitOpenError,
  HapndNetworkError,
  HapndValidationError,
} from "@hapnd/client";

try {
  await hapnd
    .aggregate("order_123")
    .expectVersion(5)
    .append("ItemAdded", { item: "Widget", price: 25.0 });
} catch (err) {
  if (err instanceof HapndConcurrencyError) {
    // Version mismatch — reload and retry
    console.log(`Expected ${err.expectedVersion}, got ${err.actualVersion}`);
  } else if (err instanceof HapndCircuitOpenError) {
    // Circuit is open — back off
    console.log(`Retry after ${err.remainingDuration}ms`);
  } else if (err instanceof HapndNetworkError) {
    // DNS failure, timeout, connection refused, etc.
    console.log(`Network error: ${err.message}`);
  } else if (err instanceof HapndApiError) {
    // Server returned a non-2xx response
    console.log(
      `API error ${err.statusCode} (${err.errorCode}): ${err.message}`,
    );
  } else if (err instanceof HapndValidationError) {
    // Bad arguments before the request was made
    console.log(`Validation: ${err.message}`);
  }
}

Error Class Reference

| Class | errorCode | HTTP | Description | | --------------------------------- | ------------------------- | ------ | --------------------------------- | | HapndValidationError | VALIDATION_ERROR | — | Client-side validation failure | | HapndApiError | from server | varies | Server returned an error response | | HapndConcurrencyError | CONCURRENCY_CONFLICT | 409 | Version mismatch | | HapndAggregateTypeMismatchError | AGGREGATE_TYPE_MISMATCH | 400 | Wrong type for existing aggregate | | HapndNetworkError | NETWORK_ERROR | — | Network failure or timeout | | HapndCircuitOpenError | CIRCUIT_OPEN | — | Circuit breaker is open |

Resilience

Retry

Built-in exponential backoff with full jitter. Retries on:

  • Network failures
  • HTTP 5xx (server errors)
  • HTTP 429 (rate limited, honours Retry-After header)
  • HTTP 408 (request timeout)

Does not retry on 4xx (except 429/408) or 409 (concurrency conflicts need business logic).

Circuit Breaker

Sliding-window circuit breaker prevents cascading failures:

  • Closed → requests pass through normally
  • Open → all requests immediately rejected with HapndCircuitOpenError
  • Half-Open → one probe request allowed; success closes the circuit, failure re-opens it

Configure via resilience options or disable with resilience: false.

Edge Runtime Support

@hapnd/client uses only the Web Fetch API and standard Web APIs — no Node.js built-ins. It works in:

  • Node.js 18+
  • Cloudflare Workers
  • Vercel Edge Functions
  • Deno
  • Bun

Zero Dependencies

The SDK has no runtime dependencies. Everything — retry, circuit breaker, error handling, HTTP — is implemented from scratch using standard Web APIs.

Links - Coming Soon.. a nice TODO!

License

Apache 2.0 — see LICENSE.