@fatagnus/dink-web
v2.25.25
Published
Browser SDK for Dink edge mesh platform via NATS WebSocket
Maintainers
Readme
@fatagnus/dink-web
Browser SDK for connecting to dinkd via NATS WebSocket.
Install
npm install @fatagnus/dink-webQuick 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
