@hardlydifficult/worker-server 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# @hardlydifficult/worker-server
|
|
2
|
+
|
|
3
|
+
A WebSocket-based worker server that manages remote worker connections, health monitoring, request routing, and load balancing through a clean TypeScript interface.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hardlydifficult/worker-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
15
|
+
|
|
16
|
+
const server = new WorkerServer({
|
|
17
|
+
port: 8080,
|
|
18
|
+
heartbeatTimeoutMs: 60000, // 60 seconds
|
|
19
|
+
healthCheckIntervalMs: 10000, // 10 seconds
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.onWorkerConnected((worker) => {
|
|
23
|
+
console.log(`Worker connected: ${worker.name} (${worker.id})`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
server.onWorkerDisconnected((worker, pendingRequests) => {
|
|
27
|
+
console.log(`Worker disconnected: ${worker.name} (${worker.id})`);
|
|
28
|
+
if (pendingRequests.size > 0) {
|
|
29
|
+
console.log(`Rescheduling ${pendingRequests.size} pending requests`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
34
|
+
console.log(`Work complete: ${message.requestId}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.addHttpHandler(async (req, res) => {
|
|
38
|
+
if (req.url === "/health") {
|
|
39
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await server.start();
|
|
47
|
+
console.log("Worker server running on port 8080");
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Core Concepts
|
|
51
|
+
|
|
52
|
+
### Worker Management
|
|
53
|
+
|
|
54
|
+
Workers connect via WebSocket, register with capabilities (supported models, concurrency limits), and send periodic heartbeats. The server tracks their status (available, busy, draining, unhealthy) and makes routing decisions based on load and model compatibility.
|
|
55
|
+
|
|
56
|
+
### Request Tracking
|
|
57
|
+
|
|
58
|
+
Requests are tracked per-worker and can be released when complete. Optional categories enable per-category concurrency limits when workers declare `concurrencyLimits` in their capabilities.
|
|
59
|
+
|
|
60
|
+
### Load Balancing
|
|
61
|
+
|
|
62
|
+
Workers are selected based on:
|
|
63
|
+
- Model support (exact or substring match)
|
|
64
|
+
- Current load (least-loaded algorithm)
|
|
65
|
+
- Category-specific limits (when `category` is provided)
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
### WorkerServer
|
|
70
|
+
|
|
71
|
+
Main entry point for managing worker connections.
|
|
72
|
+
|
|
73
|
+
#### Constructor
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
constructor(options: WorkerServerOptions)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Option | Type | Default | Description |
|
|
80
|
+
|--------|------|---------|-------------|
|
|
81
|
+
| `port` | `number` | — | HTTP + WebSocket server port |
|
|
82
|
+
| `authToken` | `string` | `undefined` | Optional token required for worker registration |
|
|
83
|
+
| `heartbeatTimeoutMs` | `number` | `60000` | Milliseconds before a missed heartbeat marks a worker unhealthy |
|
|
84
|
+
| `healthCheckIntervalMs` | `number` | `10000` | Interval (ms) for health checks |
|
|
85
|
+
| `heartbeatIntervalMs` | `number` | `15000` | Heartbeat interval communicated to workers |
|
|
86
|
+
| `logger` | `WorkerServerLogger` | `undefined` | Logger instance (defaults to no-op) |
|
|
87
|
+
|
|
88
|
+
#### Lifecycle Events
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Called when a worker successfully registers
|
|
92
|
+
onWorkerConnected(handler: WorkerConnectedHandler): () => void;
|
|
93
|
+
|
|
94
|
+
// Called when a worker disconnects with pending request IDs
|
|
95
|
+
onWorkerDisconnected(handler: WorkerDisconnectedHandler): () => void;
|
|
96
|
+
|
|
97
|
+
// Register a handler for a specific message type (dispatched by 'type' field)
|
|
98
|
+
onWorkerMessage<T = Record<string, unknown>>(
|
|
99
|
+
type: string,
|
|
100
|
+
handler: WorkerMessageHandler<T>
|
|
101
|
+
): () => void;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Message Operations
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Send a JSON message to a specific worker (false if failed)
|
|
108
|
+
send(workerId: string, message: Record<string, unknown>): boolean;
|
|
109
|
+
|
|
110
|
+
// Broadcast to all connected workers
|
|
111
|
+
broadcast(message: Record<string, unknown>): void;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Pool Queries
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Get least-loaded worker supporting the given model
|
|
118
|
+
getAvailableWorker(model: string, category?: string): WorkerInfo | null;
|
|
119
|
+
|
|
120
|
+
// Get any available worker (model-agnostic)
|
|
121
|
+
getAnyAvailableWorker(): WorkerInfo | null;
|
|
122
|
+
|
|
123
|
+
// Total connected worker count
|
|
124
|
+
getWorkerCount(): number;
|
|
125
|
+
|
|
126
|
+
// Available worker count
|
|
127
|
+
getAvailableWorkerCount(): number;
|
|
128
|
+
|
|
129
|
+
// Get info about all connected workers
|
|
130
|
+
getWorkerInfo(): WorkerInfo[];
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Request Tracking
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Track a request assigned to a worker (optional category)
|
|
137
|
+
trackRequest(workerId: string, requestId: string, category?: string): void;
|
|
138
|
+
|
|
139
|
+
// Release a tracked request (optionally increment completed count)
|
|
140
|
+
releaseRequest(
|
|
141
|
+
requestId: string,
|
|
142
|
+
options?: { incrementCompleted?: boolean }
|
|
143
|
+
): void;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### HTTP & WebSocket Extensibility
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// Add an HTTP handler (called in order until one returns true)
|
|
150
|
+
addHttpHandler(handler: HttpRequestHandler): void;
|
|
151
|
+
|
|
152
|
+
// Add an additional WebSocket endpoint at a path
|
|
153
|
+
addWebSocketEndpoint(
|
|
154
|
+
path: string,
|
|
155
|
+
handler: WebSocketConnectionHandler
|
|
156
|
+
): void;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Server Lifecycle
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Start the HTTP + WebSocket server
|
|
163
|
+
start(): Promise<void>;
|
|
164
|
+
|
|
165
|
+
// Stop the server and close all connections
|
|
166
|
+
stop(): Promise<void>;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### WorkerInfo
|
|
170
|
+
|
|
171
|
+
Public interface representing a connected worker:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
interface WorkerInfo {
|
|
175
|
+
readonly id: string;
|
|
176
|
+
readonly name: string;
|
|
177
|
+
readonly status: WorkerStatus; // "available" | "busy" | "draining" | "unhealthy"
|
|
178
|
+
readonly capabilities: WorkerCapabilities;
|
|
179
|
+
readonly sessionId: string;
|
|
180
|
+
readonly connectedAt: Date;
|
|
181
|
+
readonly lastHeartbeat: Date;
|
|
182
|
+
readonly activeRequests: number;
|
|
183
|
+
readonly completedRequests: number;
|
|
184
|
+
readonly pendingRequestIds: ReadonlySet<string>;
|
|
185
|
+
readonly categoryActiveRequests: ReadonlyMap<string, number>;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### WorkerCapabilities
|
|
190
|
+
|
|
191
|
+
Describes a worker's capabilities:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface WorkerCapabilities {
|
|
195
|
+
models: ModelInfo[];
|
|
196
|
+
maxConcurrentRequests: number;
|
|
197
|
+
metadata?: Record<string, unknown>;
|
|
198
|
+
concurrencyLimits?: Record<string, number>;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### ModelInfo
|
|
203
|
+
|
|
204
|
+
Describes a supported model:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
interface ModelInfo {
|
|
208
|
+
modelId: string;
|
|
209
|
+
displayName: string;
|
|
210
|
+
maxContextTokens: number;
|
|
211
|
+
maxOutputTokens: number;
|
|
212
|
+
supportsStreaming: boolean;
|
|
213
|
+
supportsVision?: boolean;
|
|
214
|
+
supportsTools?: boolean;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### WorkerStatus
|
|
219
|
+
|
|
220
|
+
Worker state enumeration:
|
|
221
|
+
|
|
222
|
+
| Status | Description |
|
|
223
|
+
|--------|-------------|
|
|
224
|
+
| `available` | Worker is idle and can accept new requests |
|
|
225
|
+
| `busy` | Worker is at max concurrent requests but may accept model-agnostic tasks |
|
|
226
|
+
| `draining` | Worker is being gracefully decommissioned |
|
|
227
|
+
| `unhealthy` | Worker has missed heartbeats and is presumed degraded |
|
|
228
|
+
|
|
229
|
+
## Worker Protocol
|
|
230
|
+
|
|
231
|
+
Workers communicate using JSON messages with a `type` field:
|
|
232
|
+
|
|
233
|
+
| Message | Direction | Description |
|
|
234
|
+
|---------|-----------|-------------|
|
|
235
|
+
| `worker_registration` | Worker → Server | Register with capabilities |
|
|
236
|
+
| `worker_registration_ack` | Server → Worker | Acknowledgment with session ID |
|
|
237
|
+
| `heartbeat` | Worker → Server | Periodic health check |
|
|
238
|
+
| `heartbeat_ack` | Server → Worker | Acknowledgment with next deadline |
|
|
239
|
+
|
|
240
|
+
All other message types are routed to registered handlers via `onWorkerMessage()`.
|
|
241
|
+
|
|
242
|
+
## Health Monitoring
|
|
243
|
+
|
|
244
|
+
- Workers missing heartbeats for `heartbeatTimeoutMs` are marked `unhealthy`
|
|
245
|
+
- Workers missing heartbeats for `3 × heartbeatTimeoutMs` are disconnected
|
|
246
|
+
- Health checks run every `healthCheckIntervalMs`
|