@panyam/servicekit-client
v0.0.7
Published
TypeScript client for ServiceKit WebSocket and SSE protocols
Maintainers
Readme
@panyam/servicekit-client
TypeScript client library for ServiceKit WebSocket protocols.
Installation
npm install @panyam/servicekit-clientArchitecture
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 eventsClient 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:
- @bufbuild/protobuf - Modern, tree-shakeable
- protobuf-ts - Feature-rich
- ts-proto - Plain TypeScript interfaces
Example with buf:
buf generateThen 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
