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

@panyam/servicekit-client

v0.0.7

Published

TypeScript client for ServiceKit WebSocket and SSE protocols

Readme

@panyam/servicekit-client

TypeScript client library for ServiceKit WebSocket protocols.

Installation

npm install @panyam/servicekit-client

Architecture

The client mirrors the server-side architecture with clear separation of concerns:

┌─────────────────────────────────────────────────────────────┐
│                    Transport Layer                          │
│  • WebSocket connection management                          │
│  • Ping/Pong heartbeats (always JSON)                       │
│  • Error messages (always JSON)                             │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Codec Layer                            │
│  • Data message encoding/decoding                           │
│  • JSONCodec (default) - matches server JSONCodec           │
│  • BinaryCodec - matches server BinaryProtoCodec            │
└─────────────────────────────────────────────────────────────┘

Key principle: Control messages (ping/pong/error) are always JSON text frames at the transport layer, regardless of what codec is used for data messages. This ensures clients can always communicate even when using binary protocols.

Clients

| Client | Use Case | Protocol | |--------|----------|----------| | BaseWSClient | Low-level WebSocket with auto ping/pong | Configurable codec | | GRPCWSClient | gRPC-style streaming | Envelope: {type: "data", data: ...} | | TypedGRPCWSClient<TIn, TOut> | Type-safe wrapper for GRPCWSClient | Same as GRPCWSClient |

Codecs

| Codec | Server Codec | Wire Format | |-------|--------------|-------------| | JSONCodec (default) | JSONCodec, TypedJSONCodec | JSON text | | BinaryCodec | BinaryProtoCodec | Binary protobuf |

Important: Client and server must use matching codecs!

Quick Start

Basic WebSocket (http/JSONConn)

Use BaseWSClient for plain WebSocket connections with automatic ping/pong handling:

import { BaseWSClient } from '@panyam/servicekit-client';

const client = new BaseWSClient();

client.onMessage = (data) => {
  console.log('Received:', data);
};

client.onClose = () => {
  console.log('Disconnected');
};

await client.connect('ws://localhost:8080/ws');
client.send({ hello: 'world' });

gRPC-WebSocket Streaming (grpcws)

Use GRPCWSClient for gRPC-style streaming with the envelope protocol:

import { GRPCWSClient } from '@panyam/servicekit-client';

const client = new GRPCWSClient();

client.onMessage = (data) => {
  console.log('Event:', data);
};

client.onStreamEnd = () => {
  console.log('Stream completed');
};

client.onError = (error) => {
  console.error('Error:', error);
};

await client.connect('ws://localhost:8080/ws/v1/subscribe?game_id=123');

Type-Safe with Protobuf Types

Use TypedGRPCWSClient with your protobuf-generated TypeScript types:

import { TypedGRPCWSClient } from '@panyam/servicekit-client';
import { PlayerAction, GameState } from './gen/game_pb';

const client = new TypedGRPCWSClient<PlayerAction, GameState>();

client.onMessage = (state: GameState) => {
  console.log('Players:', state.players);
};

await client.connect('ws://localhost:8080/ws/v1/sync');
client.send({ actionId: '1', move: { x: 10, y: 20 } });

Binary Protocol (BinaryProtoCodec)

For high-throughput scenarios using binary protobuf (matches server BinaryProtoCodec):

import { BaseWSClient, BinaryCodec } from '@panyam/servicekit-client';
import { MyRequest, MyResponse } from './gen/my_pb';

// Create a binary codec using your protobuf library's encode/decode functions
const codec = new BinaryCodec<MyResponse, MyRequest>(
  // Decode: ArrayBuffer -> MyResponse
  (data) => MyResponse.decode(new Uint8Array(data)),
  // Encode: MyRequest -> Uint8Array
  (msg) => MyRequest.encode(msg).finish()
);

const client = new BaseWSClient({ codec });

client.onMessage = (response: MyResponse) => {
  console.log('Received:', response);
};

await client.connect('ws://localhost:8080/ws/binary');
client.send(MyRequest.create({ id: 1, action: 'test' }));

Note: Even with binary data protocol, pings/pongs/errors are still JSON text frames. The transport layer handles this automatically.

Streaming Patterns

Server Streaming

Server sends multiple messages; client just listens:

const client = new GRPCWSClient();
client.onMessage = (event) => console.log('Event:', event);
client.onStreamEnd = () => console.log('Done');

await client.connect('ws://localhost:8080/ws/v1/subscribe');
// Just listen - server pushes events

Client Streaming

Client sends multiple messages; server responds once at the end:

const client = new GRPCWSClient();
client.onMessage = (summary) => console.log('Result:', summary);

await client.connect('ws://localhost:8080/ws/v1/commands');

// Send multiple commands
client.send({ commandId: '1', commandType: 'move' });
client.send({ commandId: '2', commandType: 'attack' });

// Signal done sending - server will respond
client.endSend();

Bidirectional Streaming

Both sides send messages concurrently:

const client = new GRPCWSClient();
client.onMessage = (state) => updateUI(state);

await client.connect('ws://localhost:8080/ws/v1/sync');

// Send actions whenever
client.send({ actionId: '1', move: { x: 10, y: 20 } });

// Receive responses concurrently
// When done sending:
client.endSend();

API Reference

BaseWSClient

Low-level WebSocket client with automatic ping/pong and configurable codec.

class BaseWSClient<I = unknown, O = unknown> {
  // Constructor
  constructor(options?: {
    autoPong?: boolean;           // Auto-respond to pings (default: true)
    WebSocket?: typeof WebSocket; // Custom WebSocket (for Node.js)
    codec?: Codec<I, O>;          // Codec for data messages (default: JSONCodec)
  })

  // Connection
  connect(url: string): Promise<void>
  close(): void

  // Sending
  send(data: O): void                       // Encoded via codec
  sendRaw(message: string | ArrayBuffer): void  // Bypass codec

  // Events
  onMessage: (data: I) => void
  onPing: (pingId: number) => void
  onClose: () => void
  onError: (error: string) => void

  // State
  readonly codec: Codec<I, O>
  readonly isConnected: boolean
  readonly readyState: number
}

Codec Interface

interface Codec<I, O> {
  decode(data: string | ArrayBuffer): I;
  encode(msg: O): string | ArrayBuffer;
}

JSONCodec

Default codec for JSON text messages:

class JSONCodec<I = unknown, O = unknown> implements Codec<I, O> {
  decode(data: string | ArrayBuffer): I;  // JSON.parse
  encode(msg: O): string;                  // JSON.stringify
}

BinaryCodec

For binary protobuf messages:

class BinaryCodec<I, O> implements Codec<I, O> {
  constructor(
    decodeFunc: (data: ArrayBuffer) => I,
    encodeFunc: (msg: O) => Uint8Array
  );
  decode(data: string | ArrayBuffer): I;   // Calls decodeFunc
  encode(msg: O): ArrayBuffer;             // Calls encodeFunc
}

GRPCWSClient

gRPC-WebSocket client with envelope protocol.

class GRPCWSClient {
  // Connection
  connect(url: string): Promise<void>
  close(): void

  // Sending (wrapped in {type: "data", data: ...})
  send(data: unknown): void
  endSend(): void  // Half-close
  cancel(): void   // Cancel stream

  // Events
  onMessage: (data: unknown) => void
  onStreamEnd: () => void
  onError: (error: string) => void
  onClose: () => void
  onPing: (pingId: number) => void

  // State
  readonly isConnected: boolean
  readonly readyState: number

  // Testing
  static createMock(): { client: GRPCWSClient; controller: MockController }
}

TypedGRPCWSClient<TIn, TOut>

Type-safe wrapper for GRPCWSClient.

class TypedGRPCWSClient<TIn, TOut> {
  // Same API as GRPCWSClient, but with typed send/onMessage
  send(data: TIn): void
  onMessage: (data: TOut) => void
  // ... other methods same as GRPCWSClient
}

Testing

Mock GRPCWSClient

Use GRPCWSClient.createMock() to test code that uses GRPCWSClient without real WebSocket connections. The controller auto-wraps/unwraps the envelope protocol so tests only deal with application-level data.

import { GRPCWSClient } from '@panyam/servicekit-client';

const { client, controller } = GRPCWSClient.createMock();

// Wire up callbacks as normal
const received: unknown[] = [];
client.onMessage = (data) => received.push(data);
client.onClose = () => { /* handle */ };

// Connect and simulate server behavior
client.connect('ws://test');
controller.simulateOpen();

// Simulate server sending a message
controller.simulateMessage({ event: { case: 'roomJoined', value: { roomId: '123' } } });
expect(received).toHaveLength(1);

// Inspect what the client sent (already unwrapped from envelope)
client.send({ action: { case: 'join' } });
expect(controller.sentMessages[0]).toMatchObject({ action: { case: 'join' } });

// Simulate errors and close
controller.simulateError('connection failed');
controller.simulateClose(1006);

Mock BaseWSClient

For testing code that uses BaseWSClient directly, use the lower-level createMockWSPair():

import { BaseWSClient, createMockWSPair } from '@panyam/servicekit-client';

const { WebSocket, controller } = createMockWSPair();
const client = new BaseWSClient({ WebSocket });

client.onMessage = (data) => console.log('Received:', data);
client.connect('ws://test');
controller.simulateOpen();

// Send raw messages (no envelope wrapping)
controller.simulateRawMessage('{"hello":"world"}');

// Check what was sent
console.log(controller.sentRaw);

Protocol Details

Ping/Pong Heartbeat

Both clients automatically respond to server pings:

  • Server sends: {type: "ping", pingId: N}
  • Client responds: {type: "pong", pingId: N}

gRPC-WS Envelope Format

GRPCWSClient uses the following message envelope:

Client → Server:

{"type": "data", "data": <payload>}
{"type": "end_send"}
{"type": "cancel"}

Server → Client:

{"type": "data", "data": <payload>}
{"type": "stream_end"}
{"type": "error", "error": "message"}

Configuration

Custom WebSocket Implementation

For Node.js or testing, provide a custom WebSocket:

import WebSocket from 'ws';

const client = new GRPCWSClient({
  WebSocket: WebSocket as any,
});

Disable Auto Pong

const client = new BaseWSClient({
  autoPong: false,
});

client.onPing = (pingId) => {
  // Handle ping manually
  client.send({ type: 'pong', pingId });
};

Protobuf Type Generation

This client works with any TypeScript protoc plugin. Popular options:

Example with buf:

buf generate

Then use the generated types:

import { TypedGRPCWSClient } from '@panyam/servicekit-client';
import { PlayerAction, GameState } from './gen/game_pb';

const client = new TypedGRPCWSClient<PlayerAction, GameState>();

License

MIT