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

@datonow/sdk

v0.3.0

Published

Typed JavaScript/TypeScript producer and consumer SDK for Datonow.

Readme

@datonow/sdk

Typed JavaScript/TypeScript SDK for Datonow. It has no runtime dependencies and keeps browser consumer credentials separate from server-only producer credentials.

Install

npm install @datonow/sdk

Version 0.2 is a deliberate breaking release. It matches the current server wire contract and does not include aliases for the old Datonow, list, markRead, or snake_case result API.

Browser consumer

import { DatonowConsumer } from "@datonow/sdk";

const notifications = new DatonowConsumer({
  endpoint: "https://notify.example.com",
  appId: "YOUR_APP_ID",
  publicToken: "YOUR_PUBLIC_TOKEN",
  recipientId: currentUser.id,
  // Required when recipient-token protection is enabled for this application:
  recipientToken: tokenFromYourBackend,
});

notifications.on("notification", (notification) => {
  console.log(notification.title, notification.createdAt);
});

notifications.on("stateChanged", (state) => {
  console.log("Datonow connection:", state);
});

// Resolves after the authenticated `auth_ok` frame, not merely after the TCP/WS open.
await notifications.connect();

const page = await notifications.listNotifications({ limit: 20 });
console.log(page.notifications, page.nextCursor, page.unreadCount);

const affected = await notifications.markNotificationsRead(["notification-id"]);
const unread = await notifications.getUnreadCount();

notifications.disconnect();

Construction has no WebSocket side effect. Call connect() explicitly and catch its promise so authentication failures are visible to the application.

The public token is sent in the first TLS-protected WebSocket frame:

{
  "type": "auth",
  "token": "PUBLIC_TOKEN",
  "recipient_token": "OPTIONAL_RECIPIENT_TOKEN"
}

Only app_id and recipient_id are present in the WebSocket URL. REST consumer requests use X-Datonow-Public-Token, X-Datonow-Recipient, and, when supplied, X-Datonow-Recipient-Token.

Server producer

import { DatonowProducer } from "@datonow/sdk";

const producer = new DatonowProducer({
  endpoint: "https://notify.example.com",
  appId: "YOUR_APP_ID",
  secretToken: process.env.DATONOW_SECRET_TOKEN!,
});

const result = await producer.publish({
  eventType: "order.shipped",
  recipientId: "user-123",
  title: "Order shipped",
  body: "Order #456 is on its way.",
  data: { orderId: 456 },
});

console.log(result.accepted); // true; delivery continues asynchronously

publish() does not return an event ID because the current ingest endpoint accepts work into Redis and returns { "ok": true } before the event is persisted. The SDK never retries publish automatically, avoiding duplicate submissions.

Never bundle DatonowProducer or its secret token into browser code.

Recipient-token protection

Mint the short-lived token in your trusted backend, bind it to the authenticated user, then pass only the result to the browser:

const recipientToken = await producer.mintRecipientToken({
  recipientId: currentUser.id,
  ttlSeconds: 900,
});

// Send to that user only:
recipientToken.token;
recipientToken.expiresIn;

Web push

WebSocket delivery only reaches an open page. DatonowWebPush additionally registers the browser with its push service, so notifications arrive while your app is closed.

Copy the shipped service worker to your own web root first — a worker can only control pages inside its own scope, so it cannot be served from a CDN:

cp node_modules/@datonow/sdk/datonow-sw.js public/datonow-sw.js
import { DatonowWebPush, isWebPushSupported } from "@datonow/sdk";

const push = new DatonowWebPush({
  endpoint: "https://notify.example.com",
  appId: "app_123",
  publicToken: "pub_live_...",
  recipientId: currentUser.id,
  recipientToken: recipientToken.token, // always required for push
  serviceWorkerUrl: "/datonow-sw.js",   // default
});

if (isWebPushSupported()) {
  // Call from a click handler: browsers require a user gesture for the prompt.
  const subscription = await push.subscribe();
  if (!subscription) {
    // Push is disabled for this application; WebSocket delivery still works.
  }
}

// Repairs a subscription the browser rotated while the app was closed. The
// service worker also posts { type: "datonow:pushsubscriptionchange" } to open
// pages when the browser replaces a subscription while the app is running.
await push.syncSubscription();

// Stop pushes to this device (server first, then the browser).
await push.unsubscribe();

A recipient token is mandatory here even when the application does not otherwise require one: a subscription outlives the page, so the shared public token alone must never be enough to register a device against a recipientId.

subscribe() throws DatonowError with code PUSH_UNSUPPORTED outside a capable secure context and PUSH_PERMISSION_DENIED when the user declined notifications. It returns null — not an error — when the server has push disabled for the application.

iOS/iPadOS Safari only delivers web push to sites installed to the Home Screen.

Errors and cancellation

import { DatonowApiError, DatonowConnectionError } from "@datonow/sdk";

try {
  await producer.publish(event, { signal: abortController.signal });
} catch (error) {
  if (error instanceof DatonowApiError) {
    console.error(error.status, error.responseBody, error.retryAfterSeconds);
  }
  if (error instanceof DatonowConnectionError) {
    console.error(error.closeCode, error.retryable);
  }
}

HTTP 429 responses expose Retry-After as retryAfterSeconds. Authentication close codes 4001 and 4003 are terminal and are not reconnected automatically. Transient closes use capped exponential backoff.

Configuration

DatonowConsumer:

| Option | Required | Default | Purpose | | ----------------------------------- | -------: | ------: | ------------------------------------------------ | | endpoint | yes | — | API origin; https:// or wss:// in production | | appId | yes | — | Datonow application ID | | publicToken | yes | — | Browser-safe application token | | recipientId | yes | — | Current authenticated user identifier | | recipientToken | no | — | Short-lived recipient-scoped token | | requestTimeoutMs | no | 10000 | REST timeout | | reconnect.enabled | no | true | Retry transient WebSocket closes | | reconnect.initialDelayMs | no | 1000 | First retry delay | | reconnect.maxDelayMs | no | 30000 | Backoff cap | | reconnect.authenticationTimeoutMs | no | 10000 | Wait for auth_ok |

DatonowProducer uses endpoint, appId, secretToken, and optional requestTimeoutMs. All clients accept allowInsecure, defaulting to false. Plaintext is accepted automatically only for loopback hosts.

DatonowWebPush:

| Option | Required | Default | Purpose | | -------------------- | -------: | -----------------: | --------------------------------------------- | | endpoint | yes | — | API origin | | appId | yes | — | Datonow application ID | | publicToken | yes | — | Browser-safe application token | | recipientId | yes | — | Current authenticated user identifier | | recipientToken | yes | — | Recipient-scoped token; never optional here | | serviceWorkerUrl | no | /datonow-sw.js | Worker path on your own origin | | serviceWorkerScope | no | worker directory | Registration scope | | requestTimeoutMs | no | 10000 | REST timeout |

Public data model

The SDK normalizes server snake_case fields to JavaScript camelCase:

  • next_cursor → nextCursor
  • unread_count → unreadCount
  • recipient_token → token
  • expires_in → expiresIn
  • notification created_at / is_read → createdAt / isRead

Realtime notification frames omit client_id, event_id, recipient_id, and is_read. The SDK fills recipientId from configuration and isRead as false; clientId and eventId remain optional.

License

MIT