mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-14 09:30:34 +00:00
531 lines
12 KiB
Markdown
531 lines
12 KiB
Markdown
# 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
|