Package Exports
- plug-n-play-ws
- plug-n-play-ws/adapters
- plug-n-play-ws/client
- plug-n-play-ws/nextjs
- plug-n-play-ws/react
- plug-n-play-ws/server
Readme
plug-n-play-ws
A plug-and-play WebSocket layer on top of Socket.IO with full TypeScript support, zero manual wiring, and production-ready features.
๐ Features
- ๐ Plug-and-Play: Single call to instantiate server and client
- ๐ Type-Safe: End-to-end TypeScript support with Zod validation
- ๐ก Real-Time Search: Built-in n-gram/edge-gram search with streaming results
- ๐พ Scalable Storage: Multiple adapter patterns (Memory, Redis, Upstash)
- โ๏ธ React Integration: Custom hooks for seamless React integration
- ๐ Next.js Ready: Built-in API route wrappers for Next.js 15
- ๐ Auto-Reconnection: Intelligent reconnection with backoff strategies
- ๐ Session Management: Multi-tab support with automatic cleanup
- ๐๏ธ Production Ready: Graceful shutdown, structured logging, cluster support
๐ฆ Installation
npm install plug-n-play-ws
# or
yarn add plug-n-play-ws
# or
pnpm add plug-n-play-wsPeer Dependencies
npm install socket.io socket.io-client react๐โโ๏ธ Quick Start
Server Setup
import { PlugNPlayServer } from 'plug-n-play-ws/server';
// Create server with automatic type inference
const server = new PlugNPlayServer({
port: 3001,
cors: { origin: true },
});
// Handle custom events
server.on('chat-message', (data) => {
console.log('New message:', data.message);
server.broadcast('chat-message', data);
});
// Start server
await server.listen();
console.log('๐ WebSocket server running on port 3001');Client Setup
import { PlugNPlayClient } from 'plug-n-play-ws/client';
// Create type-safe client
const client = new PlugNPlayClient({
url: 'http://localhost:3001',
autoConnect: true,
});
// Listen for messages
client.on('chat-message', (data) => {
console.log('Received:', data.message);
});
// Send messages
client.send('chat-message', {
user: 'Alice',
message: 'Hello World!',
timestamp: Date.now(),
});React Integration
import { usePlugNPlayWs } from 'plug-n-play-ws/react';
function ChatComponent() {
const ws = usePlugNPlayWs({
url: 'http://localhost:3001',
onConnect: (data) => console.log('Connected:', data.sessionId),
onDisconnect: (data) => console.log('Disconnected:', data.reason),
});
const sendMessage = () => {
ws.send('chat-message', {
user: 'Alice',
message: 'Hello from React!',
timestamp: Date.now(),
});
};
return (
<div>
<div>Status: {ws.status}</div>
<div>Connected: {ws.isConnected ? 'Yes' : 'No'}</div>
<button onClick={sendMessage} disabled={!ws.isConnected}>
Send Message
</button>
</div>
);
}๐ API Reference
Server API
PlugNPlayServer
interface ServerConfig {
port?: number;
cors?: CORSConfig;
heartbeatInterval?: number;
heartbeatTimeout?: number;
logger?: Logger;
adapter?: IAdapter;
redis?: RedisConfig;
gracefulShutdownTimeout?: number;
}
class PlugNPlayServer<T extends Record<string, unknown> = EventMap> {
constructor(config?: ServerConfig);
// Server lifecycle
async listen(port?: number): Promise<void>;
async close(): Promise<void>;
// Messaging
async sendToSession<K extends keyof T>(sessionId: string, event: K, data: T[K]): Promise<boolean>;
broadcast<K extends keyof T>(event: K, data: T[K]): void;
broadcastExcept<K extends keyof T>(senderSessionId: string, event: K, data: T[K]): void;
// Session management
async getActiveSessions(): Promise<SessionMetadata[]>;
async getSession(sessionId: string): Promise<SessionMetadata | null>;
async disconnectSession(sessionId: string, reason?: string): Promise<boolean>;
// Search functionality
async indexContent(id: string, content: string, metadata?: Record<string, unknown>): Promise<void>;
async removeContent(id: string): Promise<void>;
async search(query: SearchQuery, targetSessionId?: string): Promise<SearchResponse>;
// Event handling
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
emit<K extends keyof T>(event: K, data: T[K]): boolean;
once<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
removeAllListeners<K extends keyof T>(event?: K): this;
// Statistics
getStats(): ServerStats;
}Search API
interface SearchQuery {
query: string;
limit?: number;
offset?: number;
filters?: Record<string, unknown>;
streaming?: boolean;
}
interface SearchResponse<T = unknown> {
query: string;
results: SearchResult<T>[];
total: number;
took: number;
hasMore?: boolean;
}
interface SearchResult<T = unknown> {
id: string;
score: number;
data: T;
highlights?: string[];
}Client API
PlugNPlayClient
interface ClientConfig {
url: string;
autoConnect?: boolean;
reconnection?: boolean;
reconnectionAttempts?: number;
reconnectionDelay?: number;
reconnectionDelayMax?: number;
timeout?: number;
forceNew?: boolean;
logger?: Logger;
auth?: Record<string, unknown>;
}
class PlugNPlayClient<T extends Record<string, unknown> = EventMap> {
constructor(config: ClientConfig);
// Connection management
async connect(): Promise<void>;
disconnect(): void;
// Messaging
send<K extends keyof T>(event: K, data: T[K]): boolean;
async search(query: SearchQuery): Promise<SearchResponse | null>;
// Status
getStatus(): ConnectionStatus;
getSession(): { id?: string; metadata?: SessionMetadata };
isConnected(): boolean;
getStats(): ClientStats;
// Event handling
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
emit<K extends keyof T>(event: K, data: T[K]): boolean;
once<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
removeAllListeners<K extends keyof T>(event?: K): this;
}React Hooks
usePlugNPlayWs
interface UsePlugNPlayWsOptions<T> extends Omit<ClientConfig, 'autoConnect'> {
autoConnect?: boolean;
onConnect?: (data: { sessionId: string; metadata: SessionMetadata }) => void;
onDisconnect?: (data: { sessionId: string; reason: string }) => void;
onError?: (error: Error) => void;
onStatusChange?: (status: ConnectionStatus) => void;
}
function usePlugNPlayWs<T extends Record<string, unknown> = EventMap>(
options: UsePlugNPlayWsOptions<T>
): UsePlugNPlayWsReturn<T>;usePlugNPlaySearch
function usePlugNPlaySearch<T extends Record<string, unknown> = EventMap>(
client: UsePlugNPlayWsReturn<T>
): {
search: (query: SearchQuery) => Promise<void>;
clearResults: () => void;
isSearching: boolean;
results: SearchResponse | null;
streamingResults: unknown[];
error: string | null;
};๐๏ธ Storage Adapters
Memory Adapter (Development)
import { MemoryAdapter } from 'plug-n-play-ws/adapters';
const adapter = new MemoryAdapter();
const server = new PlugNPlayServer({ adapter });Redis Adapter (Production)
import { RedisAdapter } from 'plug-n-play-ws/adapters';
const adapter = new RedisAdapter({
host: 'localhost',
port: 6379,
password: 'your-password',
keyPrefix: 'myapp:',
});
const server = new PlugNPlayServer({ adapter });Upstash Redis Adapter (Serverless)
import { UpstashRedisAdapter } from 'plug-n-play-ws/adapters';
const adapter = new UpstashRedisAdapter({
url: 'https://your-redis.upstash.io',
token: 'your-token',
keyPrefix: 'myapp:',
});
const server = new PlugNPlayServer({ adapter });๐ Next.js Integration
API Route Setup
// app/api/ws/route.ts
import { PlugNPlayServer } from 'plug-n-play-ws/server';
import { createNextJSHandler } from 'plug-n-play-ws/nextjs';
const server = new PlugNPlayServer({
port: 3001,
cors: { origin: true },
});
// Start WebSocket server
server.listen().catch(console.error);
// Create API route handler
const handler = createNextJSHandler(server, {
corsOrigin: true,
enableHealthCheck: true,
});
export { handler as GET, handler as POST, handler as OPTIONS };Server Startup (instrumentation.ts)
// instrumentation.ts
import { startWebSocketServer } from 'plug-n-play-ws/nextjs';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await startWebSocketServer({
port: 3001,
cors: { origin: true },
});
}
}๐ Search Features
Document Indexing
// Index documents with metadata
await server.indexContent('doc1', 'TypeScript is amazing', {
category: 'programming',
tags: ['typescript', 'javascript'],
});
await server.indexContent('doc2', 'WebSockets enable real-time communication', {
category: 'networking',
tags: ['websockets', 'realtime'],
});Search with Streaming
// Regular search
const results = await server.search({
query: 'TypeScript programming',
limit: 10,
offset: 0,
});
// Streaming search
client.on('search-stream', ({ chunk, isLast }) => {
console.log('Streaming result:', chunk);
if (isLast) console.log('Search complete');
});
await server.search({
query: 'TypeScript programming',
streaming: true,
}, sessionId);Search in React
import { usePlugNPlayWs, usePlugNPlaySearch } from 'plug-n-play-ws/react';
function SearchComponent() {
const ws = usePlugNPlayWs({ url: 'http://localhost:3001' });
const search = usePlugNPlaySearch(ws);
const handleSearch = async () => {
await search.search({
query: 'TypeScript',
limit: 10,
streaming: false,
});
};
return (
<div>
<button onClick={handleSearch} disabled={search.isSearching}>
{search.isSearching ? 'Searching...' : 'Search'}
</button>
{search.results && (
<div>
<h3>Results ({search.results.total})</h3>
{search.results.results.map(result => (
<div key={result.id}>
<strong>{result.id}</strong> (score: {result.score})
{result.highlights?.map(highlight => (
<div dangerouslySetInnerHTML={{ __html: highlight }} />
))}
</div>
))}
</div>
)}
</div>
);
}๐๏ธ Production Deployment
Environment Variables
# WebSocket Configuration
WS_PORT=3001
WS_CORS_ORIGIN=https://yourdomain.com
# Redis Configuration (if using Redis adapter)
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=your-password
# Upstash Redis (if using Upstash adapter)
UPSTASH_REDIS_URL=https://your-redis.upstash.io
UPSTASH_REDIS_TOKEN=your-tokenDocker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3001
CMD ["npm", "start"]Graceful Shutdown
const server = new PlugNPlayServer({
gracefulShutdownTimeout: 10000, // 10 seconds
});
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
await server.close();
process.exit(0);
});๐ Examples
The examples/ directory contains comprehensive examples demonstrating various features:
01-basic-echo.ts
Basic server-client communication with modern features:
- Connection limiting and heartbeat configuration
- User count tracking and broadcasts
- Enhanced error handling and reconnection
- Type-safe event handling
npm run example:echo02-redis-search.ts
Redis storage with search functionality:
- Document indexing with metadata
- Full-text search with relevance scoring
- Real-time result broadcasting
- Production Redis configuration
npm run example:redis03-nextjs-react.tsx
Complete Next.js integration example:
- API route setup with WebSocket server
- React component with search functionality
- Type-safe hooks integration
- Responsive UI with real-time updates
04-streaming-search.ts
Advanced streaming search capabilities:
- Real-time search result streaming
- Optimized search configuration
- Large dataset handling
- Progressive result display
npm run example:streaming05-upstash-serverless.ts
Serverless-ready Upstash Redis integration:
- Environment-based adapter selection
- Automatic fallback to regular Redis
- Optimized for edge computing
- Rate limiting and error handling
npm run example:upstashRunning Examples
- Basic Echo:
cd examples && npx tsx 01-basic-echo.ts - Redis Search: Ensure Redis is running, then
npx tsx 02-redis-search.ts - Streaming:
npx tsx 04-streaming-search.ts - Upstash: Set environment variables, then
npx tsx 05-upstash-serverless.ts
Each example includes detailed logging and demonstrates best practices for production usage.
๐งช Testing
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run specific test file
npm test -- server.test.tsThe package includes comprehensive tests for:
- Server and client functionality
- All storage adapters
- Search functionality
- Type validation
- React hooks
๐ License
MIT ยฉ Your Name
๐ค Contributing
Contributions are welcome! Please read our Contributing Guide for details.
๐ Support
- ๐ Documentation
- ๐ Issue Tracker
- ๐ฌ Discussions
Made with โค๏ธ for the TypeScript community