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

@fatagnus/dink-web

v2.25.25

Published

Browser SDK for Dink edge mesh platform via NATS WebSocket

Readme

@fatagnus/dink-web

Browser SDK for connecting to dinkd via NATS WebSocket.

Install

npm install @fatagnus/dink-web

Quick Start

import { DinkWebClient } from '@fatagnus/dink-web';

const client = new DinkWebClient({
  url: 'wss://dinkd.example.com:9222',
  token: 'dk_dev_wb_...',
  appId: 'my-app',
});

await client.connect();

// Call an edge service
const result = await client.call('edge-1', 'SensorService', 'ReadTemperature', {
  sensorId: 'temp-1',
});

// Call a center (cloud) service
const user = await client.callCenter('UserService', 'GetProfile', { userId: '123' });

// Subscribe to streaming updates
const sub = await client.subscribe(
  'edge-1', 'SensorService', 'WatchTemperature',
  { sensorId: 'temp-1' },
  (update) => console.log('Temperature:', update),
);

// Later
sub.unsubscribe();
await client.close();

Token Refresh

Web tokens expire (default 1 hour, max 24 hours). The SDK can auto-refresh them:

const client = new DinkWebClient({
  url: 'wss://dinkd.example.com:9222',
  token: initialToken,
  expiresAt: initialExpiresAt,
  appId: 'my-app',

  // Called 1 minute before expiry (configurable via refreshBeforeExpiryMs)
  tokenRefresher: async () => {
    const resp = await fetch('/api/keys/web-token', {
      method: 'POST',
      headers: { 'X-API-Key': backendApiKey },
      body: JSON.stringify({ app_id: 'my-app' }),
    });
    return resp.json(); // { token, ws_url, expires_at }
  },
});

The client will drain the old connection and reconnect with the new token seamlessly.

Connection Quality

// Check stats at any time
const { status, reconnectCount, rttMs, tokenExpiresAt } = client.stats;
console.log(`RTT: ${rttMs}ms, reconnects: ${reconnectCount}`);

// React to status changes
const client = new DinkWebClient({
  // ...
  onStatusChange: (status) => {
    if (status === 'error') showOfflineBanner();
    if (status === 'connected') hideOfflineBanner();
  },
});

RTT is measured every 30 seconds via NATS flush.

Generated Clients

Use dink codegen --output-mode web-client to generate typed clients:

import { DinkWebClient } from '@fatagnus/dink-web';
import { SensorServiceClient } from './generated/sensorservice.client';

const client = new DinkWebClient({ url, token, appId });
await client.connect();

const sensor = new SensorServiceClient('edge-1', client);
const reading = await sensor.ReadTemperature({ sensorId: 'temp-1' });

API

DinkWebClient

| Method | Description | |--------|-------------| | connect() | Establish WebSocket connection | | close() | Drain connection and close | | call(edgeId, service, method, req) | Request/reply to edge service | | callCenter(service, method, req) | Request/reply to center service | | subscribe(edgeId, service, method, req, handler) | Stream from edge service | | exposeService(handler) | Serve RPC methods from this browser client | | service(edgeId, ClientClass) | Create typed service client | | status | Current connection status | | stats | Connection statistics (RTT, reconnects, token expiry) |

Config

| Option | Type | Default | Description | |--------|------|---------|-------------| | url | string | required | WebSocket URL | | token | string | required | Web token | | appId | string | required | App ID | | edgeId | string | web-{random} | Edge ID for this browser client (required for exposeService) | | expiresAt | string \| Date | - | Token expiry (enables auto-refresh) | | timeout | number | 30000 | Request timeout (ms) | | onStatusChange | function | - | Status change callback | | tokenRefresher | function | - | Returns fresh token | | refreshBeforeExpiryMs | number | 60000 | Refresh lead time (ms) |

Exposing Services

Serve RPC methods from the browser so remote edges and agents can call into it.

import { DinkWebClient } from '@fatagnus/dink-web';

const client = new DinkWebClient({
  url: 'wss://dinkd.example.com:9222',
  token: 'dk_dev_wb_...',
  appId: 'my-app',
  edgeId: 'ui-workspace-1', // stable ID for reconnects
});

await client.connect();

const handler = {
  definition() {
    return { name: 'GreetingService', version: '1.0', methods: ['SayHello'] };
  },
  async handleRequest(method, data) {
    const { name } = JSON.parse(new TextDecoder().decode(data));
    return new TextEncoder().encode(JSON.stringify({ message: `Hello, ${name}!` }));
  },
};

const { unexpose } = await client.exposeService(handler);

// Later: stop serving
unexpose();

ServiceHandler Interface

interface ServiceHandler {
  definition(): ServiceDefinition;
  handleRequest(method: string, data: Uint8Array): Promise<Uint8Array>;
  handleStream?(method: string, data: Uint8Array, emit: (data: Uint8Array) => Promise<void>, signal?: AbortSignal): Promise<void>;
  handleChannel?(method: string, channel: EdgeChannel, request: unknown): Promise<void>;
}

| Pattern | Method | Description | |---------|--------|-------------| | Request/reply | handleRequest | Single request → single response | | Server stream | handleStream | Single request → multiple emitted responses | | Bidirectional | handleChannel | Full-duplex data channel |

The service registers with dinkd for edge discovery. Other edges find it via discoverEdges().

Streaming Example

const handler = {
  definition() {
    return { name: 'EventService', version: '1.0', methods: ['Watch'] };
  },
  async handleRequest(method, data) { /* ... */ },
  async handleStream(method, data, emit, signal) {
    const unsub = eventSource.subscribe((event) => {
      if (signal?.aborted) return;
      void emit(encode(event));
    });
    // Wait until cancelled
    await new Promise((resolve) => signal?.addEventListener('abort', resolve, { once: true }));
    unsub();
  },
};

React Hooks

useExposeService(handler)

Manages the expose/unexpose lifecycle in a React component.

import { useExposeService } from '@fatagnus/dink-web/react';

function MyServiceProvider() {
  const handler = useMemo(() => createMyHandler(), []);
  useExposeService(handler); // exposes on connect, unexposes on unmount
  return null;
}

Pass null to skip exposing (e.g., when data isn't ready yet).

License

Apache-2.0