Prototype for websockspec

This commit is contained in:
Hein
2025-12-12 16:14:47 +02:00
parent b22792bad6
commit 1b2b0d8f0b
15 changed files with 4600 additions and 0 deletions

530
resolvespec-js/WEBSOCKET.md Normal file
View File

@@ -0,0 +1,530 @@
# WebSocketSpec JavaScript Client
A TypeScript/JavaScript client for connecting to WebSocketSpec servers with full support for real-time subscriptions, CRUD operations, and automatic reconnection.
## Installation
```bash
npm install @warkypublic/resolvespec-js
# or
yarn add @warkypublic/resolvespec-js
# or
pnpm add @warkypublic/resolvespec-js
```
## Quick Start
```typescript
import { WebSocketClient } from '@warkypublic/resolvespec-js';
// Create client
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws',
reconnect: true,
debug: true
});
// Connect
await client.connect();
// Read records
const users = await client.read('users', {
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'active' }
],
limit: 10
});
// Subscribe to changes
const subscriptionId = await client.subscribe('users', (notification) => {
console.log('User changed:', notification.operation, notification.data);
}, { schema: 'public' });
// Clean up
await client.unsubscribe(subscriptionId);
client.disconnect();
```
## Features
- **Real-Time Updates**: Subscribe to entity changes and receive instant notifications
- **Full CRUD Support**: Create, read, update, and delete operations
- **TypeScript Support**: Full type definitions included
- **Auto Reconnection**: Automatic reconnection with configurable retry logic
- **Heartbeat**: Built-in keepalive mechanism
- **Event System**: Listen to connection, error, and message events
- **Promise-based API**: All async operations return promises
- **Filter & Sort**: Advanced querying with filters, sorting, and pagination
- **Preloading**: Load related entities in a single query
## Configuration
```typescript
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws', // WebSocket server URL
reconnect: true, // Enable auto-reconnection
reconnectInterval: 3000, // Reconnection delay (ms)
maxReconnectAttempts: 10, // Max reconnection attempts
heartbeatInterval: 30000, // Heartbeat interval (ms)
debug: false // Enable debug logging
});
```
## API Reference
### Connection Management
#### `connect(): Promise<void>`
Connect to the WebSocket server.
```typescript
await client.connect();
```
#### `disconnect(): void`
Disconnect from the server.
```typescript
client.disconnect();
```
#### `isConnected(): boolean`
Check if currently connected.
```typescript
if (client.isConnected()) {
console.log('Connected!');
}
```
#### `getState(): ConnectionState`
Get current connection state: `'connecting'`, `'connected'`, `'disconnecting'`, `'disconnected'`, or `'reconnecting'`.
```typescript
const state = client.getState();
console.log('State:', state);
```
### CRUD Operations
#### `read<T>(entity: string, options?): Promise<T>`
Read records from an entity.
```typescript
// Read all active users
const users = await client.read('users', {
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'active' }
],
columns: ['id', 'name', 'email'],
sort: [
{ column: 'name', direction: 'asc' }
],
limit: 10,
offset: 0
});
// Read single record by ID
const user = await client.read('users', {
schema: 'public',
record_id: '123'
});
// Read with preloading
const posts = await client.read('posts', {
schema: 'public',
preload: [
{
relation: 'user',
columns: ['id', 'name', 'email']
},
{
relation: 'comments',
filters: [
{ column: 'status', operator: 'eq', value: 'approved' }
]
}
]
});
```
#### `create<T>(entity: string, data: any, options?): Promise<T>`
Create a new record.
```typescript
const newUser = await client.create('users', {
name: 'John Doe',
email: 'john@example.com',
status: 'active'
}, {
schema: 'public'
});
```
#### `update<T>(entity: string, id: string, data: any, options?): Promise<T>`
Update an existing record.
```typescript
const updatedUser = await client.update('users', '123', {
name: 'John Updated',
email: 'john.new@example.com'
}, {
schema: 'public'
});
```
#### `delete(entity: string, id: string, options?): Promise<void>`
Delete a record.
```typescript
await client.delete('users', '123', {
schema: 'public'
});
```
#### `meta<T>(entity: string, options?): Promise<T>`
Get metadata for an entity.
```typescript
const metadata = await client.meta('users', {
schema: 'public'
});
console.log('Columns:', metadata.columns);
console.log('Primary key:', metadata.primary_key);
```
### Subscriptions
#### `subscribe(entity: string, callback: Function, options?): Promise<string>`
Subscribe to entity changes.
```typescript
const subscriptionId = await client.subscribe(
'users',
(notification) => {
console.log('Operation:', notification.operation); // 'create', 'update', or 'delete'
console.log('Data:', notification.data);
console.log('Timestamp:', notification.timestamp);
},
{
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'active' }
]
}
);
```
#### `unsubscribe(subscriptionId: string): Promise<void>`
Unsubscribe from entity changes.
```typescript
await client.unsubscribe(subscriptionId);
```
#### `getSubscriptions(): Subscription[]`
Get list of active subscriptions.
```typescript
const subscriptions = client.getSubscriptions();
console.log('Active subscriptions:', subscriptions.length);
```
### Event Handling
#### `on(event: string, callback: Function): void`
Add event listener.
```typescript
// Connection events
client.on('connect', () => {
console.log('Connected!');
});
client.on('disconnect', (event) => {
console.log('Disconnected:', event.code, event.reason);
});
client.on('error', (error) => {
console.error('Error:', error);
});
// State changes
client.on('stateChange', (state) => {
console.log('State:', state);
});
// All messages
client.on('message', (message) => {
console.log('Message:', message);
});
```
#### `off(event: string): void`
Remove event listener.
```typescript
client.off('connect');
```
## Filter Operators
- `eq` - Equal (=)
- `neq` - Not Equal (!=)
- `gt` - Greater Than (>)
- `gte` - Greater Than or Equal (>=)
- `lt` - Less Than (<)
- `lte` - Less Than or Equal (<=)
- `like` - LIKE (case-sensitive)
- `ilike` - ILIKE (case-insensitive)
- `in` - IN (array of values)
## Examples
### Basic CRUD
```typescript
const client = new WebSocketClient({ url: 'ws://localhost:8080/ws' });
await client.connect();
// Create
const user = await client.create('users', {
name: 'Alice',
email: 'alice@example.com'
});
// Read
const users = await client.read('users', {
filters: [{ column: 'status', operator: 'eq', value: 'active' }]
});
// Update
await client.update('users', user.id, { name: 'Alice Updated' });
// Delete
await client.delete('users', user.id);
client.disconnect();
```
### Real-Time Subscriptions
```typescript
const client = new WebSocketClient({ url: 'ws://localhost:8080/ws' });
await client.connect();
// Subscribe to all user changes
const subId = await client.subscribe('users', (notification) => {
switch (notification.operation) {
case 'create':
console.log('New user:', notification.data);
break;
case 'update':
console.log('User updated:', notification.data);
break;
case 'delete':
console.log('User deleted:', notification.data);
break;
}
});
// Later: unsubscribe
await client.unsubscribe(subId);
```
### React Integration
```typescript
import { useEffect, useState } from 'react';
import { WebSocketClient } from '@warkypublic/resolvespec-js';
function useWebSocket(url: string) {
const [client] = useState(() => new WebSocketClient({ url }));
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
client.on('connect', () => setIsConnected(true));
client.on('disconnect', () => setIsConnected(false));
client.connect();
return () => client.disconnect();
}, [client]);
return { client, isConnected };
}
function UsersComponent() {
const { client, isConnected } = useWebSocket('ws://localhost:8080/ws');
const [users, setUsers] = useState([]);
useEffect(() => {
if (!isConnected) return;
const loadUsers = async () => {
// Subscribe to changes
await client.subscribe('users', (notification) => {
if (notification.operation === 'create') {
setUsers(prev => [...prev, notification.data]);
} else if (notification.operation === 'update') {
setUsers(prev => prev.map(u =>
u.id === notification.data.id ? notification.data : u
));
} else if (notification.operation === 'delete') {
setUsers(prev => prev.filter(u => u.id !== notification.data.id));
}
});
// Load initial data
const data = await client.read('users');
setUsers(data);
};
loadUsers();
}, [client, isConnected]);
return (
<div>
<h2>Users {isConnected ? '🟢' : '🔴'}</h2>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
```
### TypeScript with Typed Models
```typescript
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
}
interface Post {
id: number;
title: string;
content: string;
user_id: number;
user?: User;
}
const client = new WebSocketClient({ url: 'ws://localhost:8080/ws' });
await client.connect();
// Type-safe operations
const users = await client.read<User[]>('users', {
filters: [{ column: 'status', operator: 'eq', value: 'active' }]
});
const newUser = await client.create<User>('users', {
name: 'Bob',
email: 'bob@example.com',
status: 'active'
});
// Type-safe subscriptions
await client.subscribe(
'posts',
(notification) => {
const post = notification.data as Post;
console.log('Post:', post.title);
}
);
```
### Error Handling
```typescript
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws',
reconnect: true,
maxReconnectAttempts: 5
});
client.on('error', (error) => {
console.error('Connection error:', error);
});
client.on('stateChange', (state) => {
console.log('State:', state);
if (state === 'reconnecting') {
console.log('Attempting to reconnect...');
}
});
try {
await client.connect();
try {
const user = await client.read('users', { record_id: '999' });
} catch (error) {
console.error('Record not found:', error);
}
try {
await client.create('users', { /* invalid data */ });
} catch (error) {
console.error('Validation failed:', error);
}
} catch (error) {
console.error('Connection failed:', error);
}
```
### Multiple Subscriptions
```typescript
const client = new WebSocketClient({ url: 'ws://localhost:8080/ws' });
await client.connect();
// Subscribe to multiple entities
const userSub = await client.subscribe('users', (n) => {
console.log('[Users]', n.operation, n.data);
});
const postSub = await client.subscribe('posts', (n) => {
console.log('[Posts]', n.operation, n.data);
}, {
filters: [{ column: 'status', operator: 'eq', value: 'published' }]
});
const commentSub = await client.subscribe('comments', (n) => {
console.log('[Comments]', n.operation, n.data);
});
// Check active subscriptions
console.log('Active:', client.getSubscriptions().length);
// Clean up
await client.unsubscribe(userSub);
await client.unsubscribe(postSub);
await client.unsubscribe(commentSub);
```
## Best Practices
1. **Always Clean Up**: Call `disconnect()` when done to close the connection properly
2. **Use TypeScript**: Leverage type definitions for better type safety
3. **Handle Errors**: Always wrap operations in try-catch blocks
4. **Limit Subscriptions**: Don't create too many subscriptions per connection
5. **Use Filters**: Apply filters to subscriptions to reduce unnecessary notifications
6. **Connection State**: Check `isConnected()` before operations
7. **Event Listeners**: Remove event listeners when no longer needed with `off()`
8. **Reconnection**: Enable auto-reconnection for production apps
## Browser Support
- Chrome/Edge 88+
- Firefox 85+
- Safari 14+
- Node.js 14.16+
## License
MIT

View File

@@ -0,0 +1,7 @@
// Types
export * from './types';
export * from './websocket-types';
// WebSocket Client
export { WebSocketClient } from './websocket-client';
export type { WebSocketClient as default } from './websocket-client';

View File

@@ -0,0 +1,487 @@
import { v4 as uuidv4 } from 'uuid';
import type {
WebSocketClientConfig,
WSMessage,
WSRequestMessage,
WSResponseMessage,
WSNotificationMessage,
WSOperation,
WSOptions,
Subscription,
SubscriptionOptions,
ConnectionState,
WebSocketClientEvents
} from './websocket-types';
export class WebSocketClient {
private ws: WebSocket | null = null;
private config: Required<WebSocketClientConfig>;
private messageHandlers: Map<string, (message: WSResponseMessage) => void> = new Map();
private subscriptions: Map<string, Subscription> = new Map();
private eventListeners: Partial<WebSocketClientEvents> = {};
private state: ConnectionState = 'disconnected';
private reconnectAttempts = 0;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private isManualClose = false;
constructor(config: WebSocketClientConfig) {
this.config = {
url: config.url,
reconnect: config.reconnect ?? true,
reconnectInterval: config.reconnectInterval ?? 3000,
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
heartbeatInterval: config.heartbeatInterval ?? 30000,
debug: config.debug ?? false
};
}
/**
* Connect to WebSocket server
*/
async connect(): Promise<void> {
if (this.ws?.readyState === WebSocket.OPEN) {
this.log('Already connected');
return;
}
this.isManualClose = false;
this.setState('connecting');
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.config.url);
this.ws.onopen = () => {
this.log('Connected to WebSocket server');
this.setState('connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('connect');
resolve();
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onerror = (event) => {
this.log('WebSocket error:', event);
const error = new Error('WebSocket connection error');
this.emit('error', error);
reject(error);
};
this.ws.onclose = (event) => {
this.log('WebSocket closed:', event.code, event.reason);
this.stopHeartbeat();
this.setState('disconnected');
this.emit('disconnect', event);
// Attempt reconnection if enabled and not manually closed
if (this.config.reconnect && !this.isManualClose && this.reconnectAttempts < this.config.maxReconnectAttempts) {
this.reconnectAttempts++;
this.log(`Reconnection attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`);
this.setState('reconnecting');
this.reconnectTimer = setTimeout(() => {
this.connect().catch((err) => {
this.log('Reconnection failed:', err);
});
}, this.config.reconnectInterval);
}
};
} catch (error) {
reject(error);
}
});
}
/**
* Disconnect from WebSocket server
*/
disconnect(): void {
this.isManualClose = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.stopHeartbeat();
if (this.ws) {
this.setState('disconnecting');
this.ws.close();
this.ws = null;
}
this.setState('disconnected');
this.messageHandlers.clear();
}
/**
* Send a CRUD request and wait for response
*/
async request<T = any>(
operation: WSOperation,
entity: string,
options?: {
schema?: string;
record_id?: string;
data?: any;
options?: WSOptions;
}
): Promise<T> {
this.ensureConnected();
const id = uuidv4();
const message: WSRequestMessage = {
id,
type: 'request',
operation,
entity,
schema: options?.schema,
record_id: options?.record_id,
data: options?.data,
options: options?.options
};
return new Promise((resolve, reject) => {
// Set up response handler
this.messageHandlers.set(id, (response: WSResponseMessage) => {
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error?.message || 'Request failed'));
}
});
// Send message
this.send(message);
// Timeout after 30 seconds
setTimeout(() => {
if (this.messageHandlers.has(id)) {
this.messageHandlers.delete(id);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
/**
* Read records
*/
async read<T = any>(entity: string, options?: {
schema?: string;
record_id?: string;
filters?: import('./types').FilterOption[];
columns?: string[];
sort?: import('./types').SortOption[];
preload?: import('./types').PreloadOption[];
limit?: number;
offset?: number;
}): Promise<T> {
return this.request<T>('read', entity, {
schema: options?.schema,
record_id: options?.record_id,
options: {
filters: options?.filters,
columns: options?.columns,
sort: options?.sort,
preload: options?.preload,
limit: options?.limit,
offset: options?.offset
}
});
}
/**
* Create a record
*/
async create<T = any>(entity: string, data: any, options?: {
schema?: string;
}): Promise<T> {
return this.request<T>('create', entity, {
schema: options?.schema,
data
});
}
/**
* Update a record
*/
async update<T = any>(entity: string, id: string, data: any, options?: {
schema?: string;
}): Promise<T> {
return this.request<T>('update', entity, {
schema: options?.schema,
record_id: id,
data
});
}
/**
* Delete a record
*/
async delete(entity: string, id: string, options?: {
schema?: string;
}): Promise<void> {
await this.request('delete', entity, {
schema: options?.schema,
record_id: id
});
}
/**
* Get metadata for an entity
*/
async meta<T = any>(entity: string, options?: {
schema?: string;
}): Promise<T> {
return this.request<T>('meta', entity, {
schema: options?.schema
});
}
/**
* Subscribe to entity changes
*/
async subscribe(
entity: string,
callback: (notification: WSNotificationMessage) => void,
options?: {
schema?: string;
filters?: import('./types').FilterOption[];
}
): Promise<string> {
this.ensureConnected();
const id = uuidv4();
const message: WSMessage = {
id,
type: 'subscription',
operation: 'subscribe',
entity,
schema: options?.schema,
options: {
filters: options?.filters
}
};
return new Promise((resolve, reject) => {
this.messageHandlers.set(id, (response: WSResponseMessage) => {
if (response.success && response.data?.subscription_id) {
const subscriptionId = response.data.subscription_id;
// Store subscription
this.subscriptions.set(subscriptionId, {
id: subscriptionId,
entity,
schema: options?.schema,
options: { filters: options?.filters },
callback
});
this.log(`Subscribed to ${entity} with ID: ${subscriptionId}`);
resolve(subscriptionId);
} else {
reject(new Error(response.error?.message || 'Subscription failed'));
}
});
this.send(message);
// Timeout
setTimeout(() => {
if (this.messageHandlers.has(id)) {
this.messageHandlers.delete(id);
reject(new Error('Subscription timeout'));
}
}, 10000);
});
}
/**
* Unsubscribe from entity changes
*/
async unsubscribe(subscriptionId: string): Promise<void> {
this.ensureConnected();
const id = uuidv4();
const message: WSMessage = {
id,
type: 'subscription',
operation: 'unsubscribe',
subscription_id: subscriptionId
};
return new Promise((resolve, reject) => {
this.messageHandlers.set(id, (response: WSResponseMessage) => {
if (response.success) {
this.subscriptions.delete(subscriptionId);
this.log(`Unsubscribed from ${subscriptionId}`);
resolve();
} else {
reject(new Error(response.error?.message || 'Unsubscribe failed'));
}
});
this.send(message);
// Timeout
setTimeout(() => {
if (this.messageHandlers.has(id)) {
this.messageHandlers.delete(id);
reject(new Error('Unsubscribe timeout'));
}
}, 10000);
});
}
/**
* Get list of active subscriptions
*/
getSubscriptions(): Subscription[] {
return Array.from(this.subscriptions.values());
}
/**
* Get connection state
*/
getState(): ConnectionState {
return this.state;
}
/**
* Check if connected
*/
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
/**
* Add event listener
*/
on<K extends keyof WebSocketClientEvents>(event: K, callback: WebSocketClientEvents[K]): void {
this.eventListeners[event] = callback as any;
}
/**
* Remove event listener
*/
off<K extends keyof WebSocketClientEvents>(event: K): void {
delete this.eventListeners[event];
}
// Private methods
private handleMessage(data: string): void {
try {
const message: WSMessage = JSON.parse(data);
this.log('Received message:', message);
this.emit('message', message);
// Handle different message types
switch (message.type) {
case 'response':
this.handleResponse(message as WSResponseMessage);
break;
case 'notification':
this.handleNotification(message as WSNotificationMessage);
break;
case 'pong':
// Heartbeat response
break;
default:
this.log('Unknown message type:', message.type);
}
} catch (error) {
this.log('Error parsing message:', error);
}
}
private handleResponse(message: WSResponseMessage): void {
const handler = this.messageHandlers.get(message.id);
if (handler) {
handler(message);
this.messageHandlers.delete(message.id);
}
}
private handleNotification(message: WSNotificationMessage): void {
const subscription = this.subscriptions.get(message.subscription_id);
if (subscription?.callback) {
subscription.callback(message);
}
}
private send(message: WSMessage): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket is not connected');
}
const data = JSON.stringify(message);
this.log('Sending message:', message);
this.ws.send(data);
}
private startHeartbeat(): void {
if (this.heartbeatTimer) {
return;
}
this.heartbeatTimer = setInterval(() => {
if (this.isConnected()) {
const pingMessage: WSMessage = {
id: uuidv4(),
type: 'ping'
};
this.send(pingMessage);
}
}, this.config.heartbeatInterval);
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
private setState(state: ConnectionState): void {
if (this.state !== state) {
this.state = state;
this.emit('stateChange', state);
}
}
private ensureConnected(): void {
if (!this.isConnected()) {
throw new Error('WebSocket is not connected. Call connect() first.');
}
}
private emit<K extends keyof WebSocketClientEvents>(
event: K,
...args: Parameters<WebSocketClientEvents[K]>
): void {
const listener = this.eventListeners[event];
if (listener) {
(listener as any)(...args);
}
}
private log(...args: any[]): void {
if (this.config.debug) {
console.log('[WebSocketClient]', ...args);
}
}
}
export default WebSocketClient;

View File

@@ -0,0 +1,427 @@
import { WebSocketClient } from './websocket-client';
import type { WSNotificationMessage } from './websocket-types';
/**
* Example 1: Basic Usage
*/
export async function basicUsageExample() {
// Create client
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws',
reconnect: true,
debug: true
});
// Connect
await client.connect();
// Read users
const users = await client.read('users', {
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'active' }
],
limit: 10,
sort: [
{ column: 'name', direction: 'asc' }
]
});
console.log('Users:', users);
// Create a user
const newUser = await client.create('users', {
name: 'John Doe',
email: 'john@example.com',
status: 'active'
}, { schema: 'public' });
console.log('Created user:', newUser);
// Update user
const updatedUser = await client.update('users', '123', {
name: 'John Updated'
}, { schema: 'public' });
console.log('Updated user:', updatedUser);
// Delete user
await client.delete('users', '123', { schema: 'public' });
// Disconnect
client.disconnect();
}
/**
* Example 2: Real-time Subscriptions
*/
export async function subscriptionExample() {
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws',
debug: true
});
await client.connect();
// Subscribe to user changes
const subscriptionId = await client.subscribe(
'users',
(notification: WSNotificationMessage) => {
console.log('User changed:', notification.operation, notification.data);
switch (notification.operation) {
case 'create':
console.log('New user created:', notification.data);
break;
case 'update':
console.log('User updated:', notification.data);
break;
case 'delete':
console.log('User deleted:', notification.data);
break;
}
},
{
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'active' }
]
}
);
console.log('Subscribed with ID:', subscriptionId);
// Later: unsubscribe
setTimeout(async () => {
await client.unsubscribe(subscriptionId);
console.log('Unsubscribed');
client.disconnect();
}, 60000);
}
/**
* Example 3: Event Handling
*/
export async function eventHandlingExample() {
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws'
});
// Listen to connection events
client.on('connect', () => {
console.log('Connected!');
});
client.on('disconnect', (event) => {
console.log('Disconnected:', event.code, event.reason);
});
client.on('error', (error) => {
console.error('WebSocket error:', error);
});
client.on('stateChange', (state) => {
console.log('State changed to:', state);
});
client.on('message', (message) => {
console.log('Received message:', message);
});
await client.connect();
// Your operations here...
}
/**
* Example 4: Multiple Subscriptions
*/
export async function multipleSubscriptionsExample() {
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws',
debug: true
});
await client.connect();
// Subscribe to users
const userSubId = await client.subscribe(
'users',
(notification) => {
console.log('[Users]', notification.operation, notification.data);
},
{ schema: 'public' }
);
// Subscribe to posts
const postSubId = await client.subscribe(
'posts',
(notification) => {
console.log('[Posts]', notification.operation, notification.data);
},
{
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'published' }
]
}
);
// Subscribe to comments
const commentSubId = await client.subscribe(
'comments',
(notification) => {
console.log('[Comments]', notification.operation, notification.data);
},
{ schema: 'public' }
);
console.log('Active subscriptions:', client.getSubscriptions());
// Clean up after 60 seconds
setTimeout(async () => {
await client.unsubscribe(userSubId);
await client.unsubscribe(postSubId);
await client.unsubscribe(commentSubId);
client.disconnect();
}, 60000);
}
/**
* Example 5: Advanced Queries
*/
export async function advancedQueriesExample() {
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws'
});
await client.connect();
// Complex query with filters, sorting, pagination, and preloading
const posts = await client.read('posts', {
schema: 'public',
filters: [
{ column: 'status', operator: 'eq', value: 'published' },
{ column: 'views', operator: 'gte', value: 100 }
],
columns: ['id', 'title', 'content', 'user_id', 'created_at'],
sort: [
{ column: 'created_at', direction: 'desc' },
{ column: 'views', direction: 'desc' }
],
preload: [
{
relation: 'user',
columns: ['id', 'name', 'email']
},
{
relation: 'comments',
columns: ['id', 'content', 'user_id'],
filters: [
{ column: 'status', operator: 'eq', value: 'approved' }
]
}
],
limit: 20,
offset: 0
});
console.log('Posts:', posts);
// Get single record by ID
const post = await client.read('posts', {
schema: 'public',
record_id: '123'
});
console.log('Single post:', post);
client.disconnect();
}
/**
* Example 6: Error Handling
*/
export async function errorHandlingExample() {
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws',
reconnect: true,
maxReconnectAttempts: 5
});
client.on('error', (error) => {
console.error('Connection error:', error);
});
client.on('stateChange', (state) => {
console.log('Connection state:', state);
});
try {
await client.connect();
try {
// Try to read non-existent entity
await client.read('nonexistent', { schema: 'public' });
} catch (error) {
console.error('Read error:', error);
}
try {
// Try to create invalid record
await client.create('users', {
// Missing required fields
}, { schema: 'public' });
} catch (error) {
console.error('Create error:', error);
}
} catch (error) {
console.error('Connection failed:', error);
} finally {
client.disconnect();
}
}
/**
* Example 7: React Integration
*/
export function reactIntegrationExample() {
const exampleCode = `
import { useEffect, useState } from 'react';
import { WebSocketClient } from '@warkypublic/resolvespec-js';
export function useWebSocket(url: string) {
const [client] = useState(() => new WebSocketClient({ url }));
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
client.on('connect', () => setIsConnected(true));
client.on('disconnect', () => setIsConnected(false));
client.connect();
return () => {
client.disconnect();
};
}, [client]);
return { client, isConnected };
}
export function UsersComponent() {
const { client, isConnected } = useWebSocket('ws://localhost:8080/ws');
const [users, setUsers] = useState([]);
useEffect(() => {
if (!isConnected) return;
// Subscribe to user changes
const subscribeToUsers = async () => {
const subId = await client.subscribe('users', (notification) => {
if (notification.operation === 'create') {
setUsers(prev => [...prev, notification.data]);
} else if (notification.operation === 'update') {
setUsers(prev => prev.map(u =>
u.id === notification.data.id ? notification.data : u
));
} else if (notification.operation === 'delete') {
setUsers(prev => prev.filter(u => u.id !== notification.data.id));
}
}, { schema: 'public' });
// Load initial users
const initialUsers = await client.read('users', {
schema: 'public',
filters: [{ column: 'status', operator: 'eq', value: 'active' }]
});
setUsers(initialUsers);
return () => client.unsubscribe(subId);
};
subscribeToUsers();
}, [client, isConnected]);
const createUser = async (name: string, email: string) => {
await client.create('users', { name, email, status: 'active' }, {
schema: 'public'
});
};
return (
<div>
<h2>Users ({users.length})</h2>
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
{/* Render users... */}
</div>
);
}
`;
console.log(exampleCode);
}
/**
* Example 8: TypeScript with Typed Models
*/
export async function typedModelsExample() {
// Define your models
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
created_at: string;
}
interface Post {
id: number;
title: string;
content: string;
user_id: number;
status: 'draft' | 'published';
views: number;
user?: User;
}
const client = new WebSocketClient({
url: 'ws://localhost:8080/ws'
});
await client.connect();
// Type-safe operations
const users = await client.read<User[]>('users', {
schema: 'public',
filters: [{ column: 'status', operator: 'eq', value: 'active' }]
});
const newUser = await client.create<User>('users', {
name: 'Alice',
email: 'alice@example.com',
status: 'active'
}, { schema: 'public' });
const posts = await client.read<Post[]>('posts', {
schema: 'public',
preload: [
{
relation: 'user',
columns: ['id', 'name', 'email']
}
]
});
// Type-safe subscriptions
await client.subscribe(
'users',
(notification) => {
const user = notification.data as User;
console.log('User changed:', user.name, user.email);
},
{ schema: 'public' }
);
client.disconnect();
}

View File

@@ -0,0 +1,110 @@
// WebSocket Message Types
export type MessageType = 'request' | 'response' | 'notification' | 'subscription' | 'error' | 'ping' | 'pong';
export type WSOperation = 'read' | 'create' | 'update' | 'delete' | 'subscribe' | 'unsubscribe' | 'meta';
// Re-export common types
export type { FilterOption, SortOption, PreloadOption, Operator, SortDirection } from './types';
export interface WSOptions {
filters?: import('./types').FilterOption[];
columns?: string[];
preload?: import('./types').PreloadOption[];
sort?: import('./types').SortOption[];
limit?: number;
offset?: number;
}
export interface WSMessage {
id?: string;
type: MessageType;
operation?: WSOperation;
schema?: string;
entity?: string;
record_id?: string;
data?: any;
options?: WSOptions;
subscription_id?: string;
success?: boolean;
error?: WSErrorInfo;
metadata?: Record<string, any>;
timestamp?: string;
}
export interface WSErrorInfo {
code: string;
message: string;
details?: Record<string, any>;
}
export interface WSRequestMessage {
id: string;
type: 'request';
operation: WSOperation;
schema?: string;
entity: string;
record_id?: string;
data?: any;
options?: WSOptions;
}
export interface WSResponseMessage {
id: string;
type: 'response';
success: boolean;
data?: any;
error?: WSErrorInfo;
metadata?: Record<string, any>;
timestamp: string;
}
export interface WSNotificationMessage {
type: 'notification';
operation: WSOperation;
subscription_id: string;
schema?: string;
entity: string;
data: any;
timestamp: string;
}
export interface WSSubscriptionMessage {
id: string;
type: 'subscription';
operation: 'subscribe' | 'unsubscribe';
schema?: string;
entity: string;
options?: WSOptions;
subscription_id?: string;
}
export interface SubscriptionOptions {
filters?: import('./types').FilterOption[];
onNotification?: (notification: WSNotificationMessage) => void;
}
export interface WebSocketClientConfig {
url: string;
reconnect?: boolean;
reconnectInterval?: number;
maxReconnectAttempts?: number;
heartbeatInterval?: number;
debug?: boolean;
}
export interface Subscription {
id: string;
entity: string;
schema?: string;
options?: WSOptions;
callback?: (notification: WSNotificationMessage) => void;
}
export type ConnectionState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'reconnecting';
export interface WebSocketClientEvents {
connect: () => void;
disconnect: (event: CloseEvent) => void;
error: (error: Error) => void;
message: (message: WSMessage) => void;
stateChange: (state: ConnectionState) => void;
}