@hardlydifficult/worker-server 1.0.10 → 1.0.12
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 +159 -431
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/worker-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WebSocket-based remote worker server with health monitoring, message routing, and load balancing.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,565 +13,293 @@ npm install @hardlydifficult/worker-server
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
server.start().then(() => {
|
|
20
|
-
console.log("Worker server running on port 3000");
|
|
21
|
-
|
|
22
|
-
// Listen for worker connections
|
|
23
|
-
server.onWorkerConnected((worker) => {
|
|
24
|
-
console.log(`Worker ${worker.name} (${worker.id}) connected`);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
server.onWorkerDisconnected((worker) => {
|
|
28
|
-
console.log(`Worker ${worker.name} (${worker.id}) disconnected`);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Route messages by type
|
|
32
|
-
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
33
|
-
console.log(`Worker ${worker.id} completed request: ${message.requestId}`);
|
|
34
|
-
});
|
|
16
|
+
const server = new WorkerServer({
|
|
17
|
+
port: 19100,
|
|
18
|
+
authToken: "secret-token", // optional
|
|
35
19
|
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Core Concepts
|
|
39
|
-
|
|
40
|
-
### Worker Registration & Lifecycle
|
|
41
|
-
|
|
42
|
-
Workers connect via WebSocket and register with authentication (optional). The server tracks their status, heartbeat, and request load.
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
46
20
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
authToken: "secret-token" // Optional
|
|
21
|
+
server.onWorkerConnected((worker) => {
|
|
22
|
+
console.log(`Worker ${worker.name} connected`);
|
|
50
23
|
});
|
|
51
24
|
|
|
52
|
-
server.
|
|
53
|
-
|
|
54
|
-
console.log("Worker connected:", worker.id, worker.name);
|
|
25
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
26
|
+
console.log(`Worker ${worker.id} completed:`, message);
|
|
55
27
|
});
|
|
56
|
-
```
|
|
57
28
|
|
|
58
|
-
|
|
29
|
+
await server.start();
|
|
30
|
+
console.log("Server listening on port", server.port);
|
|
59
31
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"workerName": "GPU Worker",
|
|
65
|
-
"capabilities": {
|
|
66
|
-
"models": [
|
|
67
|
-
{
|
|
68
|
-
"modelId": "gpt-4",
|
|
69
|
-
"displayName": "GPT-4",
|
|
70
|
-
"maxContextTokens": 32768,
|
|
71
|
-
"maxOutputTokens": 4096,
|
|
72
|
-
"supportsStreaming": true
|
|
73
|
-
}
|
|
74
|
-
],
|
|
75
|
-
"maxConcurrentRequests": 2
|
|
76
|
-
},
|
|
77
|
-
"authToken": "secret-token"
|
|
32
|
+
// Send a message to a worker
|
|
33
|
+
const worker = server.getAvailableWorker("sonnet");
|
|
34
|
+
if (worker) {
|
|
35
|
+
server.send(worker.id, { type: "work_request", requestId: "req-123" });
|
|
78
36
|
}
|
|
79
37
|
```
|
|
80
38
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```json
|
|
84
|
-
{
|
|
85
|
-
"type": "worker_registration_ack",
|
|
86
|
-
"success": true,
|
|
87
|
-
"sessionId": "uuid-here",
|
|
88
|
-
"heartbeatIntervalMs": 15000
|
|
89
|
-
}
|
|
90
|
-
```
|
|
39
|
+
## Core Concepts
|
|
91
40
|
|
|
92
|
-
###
|
|
41
|
+
### WorkerServer
|
|
93
42
|
|
|
94
|
-
|
|
43
|
+
Main entry point for managing worker connections via WebSocket.
|
|
95
44
|
|
|
96
45
|
```typescript
|
|
97
|
-
|
|
98
|
-
console.log(`Worker ${worker.id} status: ${message.statusText}`);
|
|
99
|
-
});
|
|
46
|
+
import { WorkerServer, WorkerStatus } from "@hardlydifficult/worker-server";
|
|
100
47
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
48
|
+
const server = new WorkerServer({
|
|
49
|
+
port: 19100,
|
|
50
|
+
heartbeatTimeoutMs: 60_000,
|
|
51
|
+
healthCheckIntervalMs: 10_000,
|
|
52
|
+
heartbeatIntervalMs: 15_000,
|
|
106
53
|
});
|
|
107
54
|
|
|
108
|
-
|
|
109
|
-
server.
|
|
55
|
+
await server.start();
|
|
56
|
+
await server.stop();
|
|
110
57
|
```
|
|
111
58
|
|
|
112
|
-
|
|
59
|
+
#### Lifecycle Events
|
|
113
60
|
|
|
114
|
-
|
|
61
|
+
| Method | Description |
|
|
62
|
+
|--------|-------------|
|
|
63
|
+
| `onWorkerConnected(handler)` | Called when a worker registers successfully |
|
|
64
|
+
| `onWorkerDisconnected(handler)` | Called when a worker disconnects; includes pending request IDs |
|
|
115
65
|
|
|
116
66
|
```typescript
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
server.send(worker.id, { type: "execute", prompt: "..." });
|
|
121
|
-
}
|
|
67
|
+
server.onWorkerConnected((worker) => {
|
|
68
|
+
console.log(`Connected: ${worker.id} (${worker.name})`);
|
|
69
|
+
});
|
|
122
70
|
|
|
123
|
-
|
|
124
|
-
|
|
71
|
+
server.onWorkerDisconnected((worker, pendingRequestIds) => {
|
|
72
|
+
console.log(`Disconnected: ${worker.id} with ${pendingRequestIds.size} pending requests`);
|
|
73
|
+
});
|
|
125
74
|
```
|
|
126
75
|
|
|
127
|
-
|
|
76
|
+
#### Message Routing
|
|
77
|
+
|
|
78
|
+
Register handlers for message types sent by workers:
|
|
128
79
|
|
|
129
80
|
```typescript
|
|
130
|
-
|
|
131
|
-
|
|
81
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
82
|
+
const { requestId, result } = message;
|
|
83
|
+
console.log(`Worker ${worker.id} completed ${requestId}`);
|
|
84
|
+
});
|
|
132
85
|
|
|
133
|
-
//
|
|
134
|
-
server.
|
|
86
|
+
// Send messages to workers
|
|
87
|
+
const success = server.send(workerId, { type: "work_request", requestId: "req-1" });
|
|
88
|
+
server.broadcast({ type: "shutdown" });
|
|
135
89
|
```
|
|
136
90
|
|
|
137
|
-
|
|
91
|
+
#### Worker Selection & Pool Queries
|
|
138
92
|
|
|
139
|
-
|
|
93
|
+
| Method | Description |
|
|
94
|
+
|--------|-------------|
|
|
95
|
+
| `getAvailableWorker(model, category?)` | Least-loaded worker supporting the model |
|
|
96
|
+
| `getAnyAvailableWorker()` | Any available or busy worker (model-agnostic) |
|
|
97
|
+
| `getAvailableSlotCount(model, category?)` | Total free slots across all available workers |
|
|
98
|
+
| `getWorkerCount()` | Total connected workers |
|
|
99
|
+
| `getAvailableWorkerCount()` | Available workers count |
|
|
100
|
+
| `getWorkerInfo()` | Public info for all workers |
|
|
140
101
|
|
|
141
102
|
```typescript
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Server response
|
|
150
|
-
{
|
|
151
|
-
"type": "heartbeat_ack",
|
|
152
|
-
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
153
|
-
"nextHeartbeatDeadline": "2024-01-01T00:01:00.000Z"
|
|
103
|
+
// Get least-loaded worker supporting a model
|
|
104
|
+
const worker = server.getAvailableWorker("sonnet");
|
|
105
|
+
if (worker) {
|
|
106
|
+
server.trackRequest(worker.id, "req-123", "local");
|
|
154
107
|
}
|
|
155
|
-
```
|
|
156
108
|
|
|
157
|
-
|
|
109
|
+
// Slot counts with category-aware limits
|
|
110
|
+
console.log("Available slots:", server.getAvailableSlotCount("sonnet", "local"));
|
|
158
111
|
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
healthCheckIntervalMs: 10_000, // Check every 10 seconds
|
|
164
|
-
});
|
|
112
|
+
// View all workers
|
|
113
|
+
for (const info of server.getWorkerInfo()) {
|
|
114
|
+
console.log(`${info.name}: ${info.status} (${info.activeRequests}/${info.capabilities.maxConcurrentRequests})`);
|
|
115
|
+
}
|
|
165
116
|
```
|
|
166
117
|
|
|
167
|
-
|
|
118
|
+
#### Request Tracking
|
|
168
119
|
|
|
169
|
-
|
|
120
|
+
Track and release requests for accurate availability:
|
|
170
121
|
|
|
171
122
|
```typescript
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Worker registration includes concurrency limits
|
|
175
|
-
{
|
|
176
|
-
"capabilities": {
|
|
177
|
-
"models": [.],
|
|
178
|
-
"maxConcurrentRequests": 4,
|
|
179
|
-
"concurrencyLimits": {
|
|
180
|
-
"chat": 2,
|
|
181
|
-
"embedding": 3,
|
|
182
|
-
"tool_use": 1
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Track with category
|
|
188
|
-
server.trackRequest(worker.id, requestId, "chat");
|
|
123
|
+
// When assigning a request to a worker
|
|
124
|
+
server.trackRequest(workerId, requestId, "local");
|
|
189
125
|
|
|
190
|
-
//
|
|
191
|
-
server.releaseRequest(requestId);
|
|
126
|
+
// When the request completes
|
|
127
|
+
server.releaseRequest(requestId, { incrementCompleted: true });
|
|
192
128
|
```
|
|
193
129
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
### Custom HTTP Endpoints
|
|
130
|
+
#### Extensibility
|
|
197
131
|
|
|
198
|
-
Add HTTP
|
|
132
|
+
Add HTTP endpoints and custom WebSocket paths:
|
|
199
133
|
|
|
200
134
|
```typescript
|
|
135
|
+
// HTTP handler
|
|
201
136
|
server.addHttpHandler(async (req, res) => {
|
|
202
137
|
if (req.url === "/health") {
|
|
203
138
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
204
|
-
res.end(JSON.stringify({
|
|
139
|
+
res.end(JSON.stringify({ ok: true }));
|
|
205
140
|
return true;
|
|
206
141
|
}
|
|
207
142
|
return false;
|
|
208
143
|
});
|
|
209
|
-
```
|
|
210
144
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```typescript
|
|
216
|
-
server.addWebSocketEndpoint("/ws/admin", (ws) => {
|
|
217
|
-
ws.on("message", (data) => {
|
|
218
|
-
// Handle admin messages
|
|
219
|
-
});
|
|
145
|
+
// Custom WebSocket endpoint
|
|
146
|
+
server.addWebSocketEndpoint("/ws/dashboard", (ws) => {
|
|
147
|
+
ws.send(JSON.stringify({ type: "hello" }));
|
|
220
148
|
});
|
|
221
149
|
```
|
|
222
150
|
|
|
223
|
-
###
|
|
224
|
-
|
|
225
|
-
Public worker info (without WebSocket reference):
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
const worker = server.getAvailableWorker("gpt-4");
|
|
229
|
-
if (worker) {
|
|
230
|
-
console.log("Active requests:", worker.activeRequests);
|
|
231
|
-
console.log("Completed requests:", worker.completedRequests);
|
|
232
|
-
console.log("Pending request IDs:", [...worker.pendingRequestIds]);
|
|
233
|
-
console.log("Per-category active requests:", worker.categoryActiveRequests);
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## Core Components
|
|
238
|
-
|
|
239
|
-
### WorkerServer
|
|
240
|
-
|
|
241
|
-
Main server class managing WebSocket connections, HTTP endpoints, and worker pool.
|
|
242
|
-
|
|
243
|
-
#### Constructor
|
|
244
|
-
|
|
245
|
-
| Parameter | Type | Default | Description |
|
|
246
|
-
|-----------|------|---------|-------------|
|
|
247
|
-
| `port` | `number` | — | HTTP + WebSocket server port |
|
|
248
|
-
| `authToken` | `string` (optional) | — | Token required for worker registration |
|
|
249
|
-
| `heartbeatTimeoutMs` | `number` | 60000 | Timeout before marking worker unhealthy |
|
|
250
|
-
| `healthCheckIntervalMs` | `number` | 10000 | Interval for health checks |
|
|
251
|
-
| `heartbeatIntervalMs` | `number` | 15000 | Heartbeat interval communicated to workers |
|
|
252
|
-
| `logger` | `WorkerServerLogger` (optional) | No-op | Logger instance |
|
|
253
|
-
|
|
254
|
-
#### Lifecycle Management
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
const server = new WorkerServer({ port: 8080, authToken: "secret" });
|
|
258
|
-
|
|
259
|
-
// Start the server
|
|
260
|
-
await server.start();
|
|
261
|
-
|
|
262
|
-
// Stop the server gracefully
|
|
263
|
-
await server.stop();
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
#### Registration Handlers
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// Called when a worker successfully registers
|
|
270
|
-
const unsubscribeConnected = server.onWorkerConnected((worker) => {
|
|
271
|
-
console.log(`Worker connected: ${worker.name}`);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Called when a worker disconnects
|
|
275
|
-
const unsubscribeDisconnected = server.onWorkerDisconnected((worker, pending) => {
|
|
276
|
-
console.log(`Worker disconnected with ${pending.size} pending requests`);
|
|
277
|
-
});
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
#### Message Handling
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
// Register handlers for domain-specific messages by type
|
|
284
|
-
server.onWorkerMessage("work_request", (worker, message) => {
|
|
285
|
-
// Process work request from worker
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
server.onWorkerMessage("status_update", (worker, message) => {
|
|
289
|
-
// Handle status updates from worker
|
|
290
|
-
});
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
#### Sending Messages
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
// Send to a specific worker
|
|
297
|
-
const success = server.send("worker-1", { type: "stop", reason: "shutdown" });
|
|
298
|
-
|
|
299
|
-
// Broadcast to all connected workers
|
|
300
|
-
server.broadcast({ type: "maintenance_start" });
|
|
301
|
-
```
|
|
151
|
+
### WorkerPool
|
|
302
152
|
|
|
303
|
-
|
|
153
|
+
Low-level pool manager for worker state and selection.
|
|
304
154
|
|
|
305
155
|
```typescript
|
|
306
|
-
|
|
307
|
-
const worker = server.getAvailableWorker("sonnet-3.5");
|
|
156
|
+
import { WorkerPool, toWorkerInfo, WorkerStatus } from "@hardlydifficult/worker-server";
|
|
308
157
|
|
|
309
|
-
|
|
310
|
-
const anyWorker = server.getAnyAvailableWorker();
|
|
158
|
+
const pool = new WorkerPool(logger);
|
|
311
159
|
|
|
312
|
-
//
|
|
313
|
-
|
|
160
|
+
// Add/remove workers
|
|
161
|
+
pool.add(worker);
|
|
162
|
+
pool.remove(workerId);
|
|
163
|
+
const worker = pool.get(workerId);
|
|
314
164
|
```
|
|
315
165
|
|
|
316
|
-
####
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
// Track a request assigned to a worker
|
|
320
|
-
server.trackRequest("worker-1", "req-123", "inference");
|
|
321
|
-
|
|
322
|
-
// Release a completed request
|
|
323
|
-
server.releaseRequest("req-123", { incrementCompleted: true });
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Extensibility
|
|
166
|
+
#### Selection Logic
|
|
327
167
|
|
|
328
168
|
| Method | Description |
|
|
329
169
|
|--------|-------------|
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
|
|
333
|
-
|
|
170
|
+
| `getAvailableWorker(model, category?)` | Least-loaded worker supporting the model, respecting per-category concurrency limits |
|
|
171
|
+
| `getAnyAvailableWorker()` | Any available or busy worker (model-agnostic) |
|
|
172
|
+
| `getAvailableSlotCount(model, category?)` | Total free slots across all available workers for the model |
|
|
173
|
+
| `getCount()` | Total connected workers |
|
|
174
|
+
| `getAvailableCount()` | Available workers count |
|
|
175
|
+
| `getWorkerInfoList()` | Public info for all workers |
|
|
334
176
|
|
|
335
|
-
|
|
336
|
-
|--------|--------|-------------|
|
|
337
|
-
| `onWorkerConnected(handler)` | `() => void` | Called when worker registers |
|
|
338
|
-
| `onWorkerDisconnected(handler)` | `() => void` | Called when worker disconnects (includes pending requests) |
|
|
339
|
-
| `onWorkerMessage(type, handler)` | `() => void` | Register handler for a message type |
|
|
177
|
+
#### Request Management
|
|
340
178
|
|
|
341
|
-
|
|
179
|
+
| Method | Description |
|
|
180
|
+
|--------|-------------|
|
|
181
|
+
| `trackRequest(workerId, requestId, category?)` | Marks request as in-flight and updates status |
|
|
182
|
+
| `releaseRequest(requestId, options?)` | Decrements active count, optionally increments completed count |
|
|
342
183
|
|
|
343
|
-
|
|
184
|
+
#### Health Monitoring
|
|
344
185
|
|
|
345
186
|
| Method | Description |
|
|
346
187
|
|--------|-------------|
|
|
347
|
-
| `
|
|
348
|
-
| `getAnyAvailableWorker()` | Get any available/busy worker |
|
|
349
|
-
| `trackRequest(workerId, requestId, category?)` | Mark request as in-progress |
|
|
350
|
-
| `releaseRequest(requestId, { incrementCompleted? })` | Release tracked request |
|
|
351
|
-
| `getWorkerInfoList()` | Get public info for all workers |
|
|
352
|
-
| `checkHealth(timeoutMs)` | Return IDs of dead workers (heartbeat > 3x timeout) |
|
|
353
|
-
| `send(workerId, message)` | Send message to specific worker |
|
|
354
|
-
| `broadcast(message)` | Broadcast to all workers |
|
|
355
|
-
| `closeAll()` | Close all worker connections |
|
|
188
|
+
| `checkHealth(timeoutMs)` | Returns IDs of workers exceeding `3x` timeout; marks unhealthy ones |
|
|
356
189
|
|
|
357
190
|
### ConnectionHandler
|
|
358
191
|
|
|
359
|
-
Handles WebSocket
|
|
360
|
-
|
|
361
|
-
#### Message Routing
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
import { ConnectionHandler } from "@hardlydifficult/worker-server";
|
|
365
|
-
|
|
366
|
-
const handler = new ConnectionHandler(pool, config, logger);
|
|
367
|
-
|
|
368
|
-
// Register handlers for custom message types
|
|
369
|
-
const unregister = handler.onMessage("custom_type", (worker, message) => {
|
|
370
|
-
console.log(`Received from ${worker.id}:`, message);
|
|
371
|
-
});
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
#### Event Handlers
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
handler.onWorkerConnected((worker) => {
|
|
378
|
-
console.log("Worker connected:", worker.id);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
handler.onWorkerDisconnected((worker, pending) => {
|
|
382
|
-
console.log("Worker disconnected with pending:", pending.size);
|
|
383
|
-
});
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
## Advanced Features
|
|
387
|
-
|
|
388
|
-
### HTTP Endpoints
|
|
389
|
-
|
|
390
|
-
Custom HTTP handlers can be added:
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
server.addHttpHandler(async (req, res) => {
|
|
394
|
-
if (req.url === "/health") {
|
|
395
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
396
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
return false; // continue to next handler
|
|
400
|
-
});
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### Custom WebSocket Endpoints
|
|
404
|
-
|
|
405
|
-
Additional WebSocket paths can be handled:
|
|
406
|
-
|
|
407
|
-
```typescript
|
|
408
|
-
server.addWebSocketEndpoint("/ws/admin", (ws) => {
|
|
409
|
-
ws.on("message", (data) => {
|
|
410
|
-
// Handle admin WebSocket messages
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Authentication
|
|
192
|
+
Handles WebSocket lifecycle, registration, heartbeats, and message routing. Most consumers use `WorkerServer`, which encapsulates this.
|
|
416
193
|
|
|
417
|
-
|
|
194
|
+
### Message Protocol
|
|
418
195
|
|
|
419
|
-
|
|
420
|
-
const server = new WorkerServer({
|
|
421
|
-
port: 8080,
|
|
422
|
-
authToken: "your-secret-token"
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// Workers must send registration with matching authToken
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
### Load Balancing with Category Limits
|
|
429
|
-
|
|
430
|
-
Workers can declare per-category concurrency limits:
|
|
196
|
+
Workers send JSON messages with a `type` field:
|
|
431
197
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
models: [{ modelId: "sonnet", ... }],
|
|
435
|
-
maxConcurrentRequests: 10,
|
|
436
|
-
concurrencyLimits: {
|
|
437
|
-
inference: 5, // max 5 concurrent inference requests
|
|
438
|
-
embeddings: 2 // max 2 concurrent embedding requests
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
```
|
|
198
|
+
- `worker_registration` — Register with capabilities and optional `authToken`
|
|
199
|
+
- `heartbeat` — Send periodically to confirm liveness
|
|
442
200
|
|
|
443
|
-
|
|
201
|
+
The server responds with:
|
|
202
|
+
- `worker_registration_ack` — Success/failure with `sessionId` and `heartbeatIntervalMs`
|
|
203
|
+
- `heartbeat_ack` — Acknowledgment with `nextHeartbeatDeadline`
|
|
444
204
|
|
|
445
|
-
|
|
446
|
-
server.trackRequest("worker-1", "req-1", "inference");
|
|
447
|
-
server.releaseRequest("req-1"); // category looked up automatically
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
## Type Definitions
|
|
205
|
+
### Types & Interfaces
|
|
451
206
|
|
|
452
|
-
|
|
207
|
+
#### `WorkerStatus`
|
|
453
208
|
|
|
454
209
|
| Value | Description |
|
|
455
210
|
|-------|-------------|
|
|
456
211
|
| `available` | Worker can accept new requests |
|
|
457
|
-
| `busy` | Worker
|
|
458
|
-
| `draining` | Worker
|
|
459
|
-
| `unhealthy` | Worker heartbeat
|
|
212
|
+
| `busy` | Worker at capacity, but can accept model-agnostic tasks |
|
|
213
|
+
| `draining` | Worker finishing current work before shutdown |
|
|
214
|
+
| `unhealthy` | Worker failed heartbeat checks |
|
|
215
|
+
|
|
216
|
+
#### `WorkerInfo`
|
|
460
217
|
|
|
461
|
-
|
|
218
|
+
Public worker metadata (excludes raw WebSocket):
|
|
462
219
|
|
|
463
220
|
```typescript
|
|
464
|
-
interface
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
221
|
+
interface WorkerInfo {
|
|
222
|
+
readonly id: string;
|
|
223
|
+
readonly name: string;
|
|
224
|
+
readonly status: WorkerStatus;
|
|
225
|
+
readonly capabilities: WorkerCapabilities;
|
|
226
|
+
readonly sessionId: string;
|
|
227
|
+
readonly connectedAt: Date;
|
|
228
|
+
readonly lastHeartbeat: Date;
|
|
229
|
+
readonly activeRequests: number;
|
|
230
|
+
readonly completedRequests: number;
|
|
231
|
+
readonly pendingRequestIds: ReadonlySet<string>;
|
|
232
|
+
readonly categoryActiveRequests: ReadonlyMap<string, number>;
|
|
472
233
|
}
|
|
473
234
|
```
|
|
474
235
|
|
|
475
|
-
|
|
236
|
+
#### `WorkerCapabilities`
|
|
476
237
|
|
|
477
238
|
```typescript
|
|
478
239
|
interface WorkerCapabilities {
|
|
479
240
|
models: ModelInfo[];
|
|
480
241
|
maxConcurrentRequests: number;
|
|
481
242
|
metadata?: Record<string, unknown>;
|
|
482
|
-
concurrencyLimits?: Record<string, number>;
|
|
243
|
+
concurrencyLimits?: Record<string, number>; // per-category limits
|
|
483
244
|
}
|
|
484
245
|
```
|
|
485
246
|
|
|
486
|
-
|
|
247
|
+
#### `ModelInfo`
|
|
487
248
|
|
|
488
249
|
```typescript
|
|
489
|
-
interface
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
activeRequests: number;
|
|
498
|
-
completedRequests: number;
|
|
499
|
-
pendingRequestIds: ReadonlySet<string>;
|
|
500
|
-
categoryActiveRequests: ReadonlyMap<string, number>;
|
|
250
|
+
interface ModelInfo {
|
|
251
|
+
modelId: string;
|
|
252
|
+
displayName: string;
|
|
253
|
+
maxContextTokens: number;
|
|
254
|
+
maxOutputTokens: number;
|
|
255
|
+
supportsStreaming: boolean;
|
|
256
|
+
supportsVision?: boolean;
|
|
257
|
+
supportsTools?: boolean;
|
|
501
258
|
}
|
|
502
259
|
```
|
|
503
260
|
|
|
504
|
-
|
|
261
|
+
### Secure Authentication
|
|
505
262
|
|
|
506
|
-
|
|
263
|
+
Authentication tokens are compared using timing-safe comparison to prevent brute-force attacks:
|
|
507
264
|
|
|
508
265
|
```typescript
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
warn(message: string, context?: Record<string, unknown>): void;
|
|
513
|
-
error(message: string, context?: Record<string, unknown>): void;
|
|
514
|
-
}
|
|
266
|
+
import { safeCompare } from "@hardlydifficult/worker-server";
|
|
267
|
+
// Internally used by ConnectionHandler; exposed for testing
|
|
268
|
+
const valid = safeCompare("a", "b"); // false
|
|
515
269
|
```
|
|
516
270
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
const server = new WorkerServer({
|
|
521
|
-
port: 3000,
|
|
522
|
-
logger: {
|
|
523
|
-
debug: console.debug,
|
|
524
|
-
info: console.info,
|
|
525
|
-
warn: console.warn,
|
|
526
|
-
error: console.error,
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
## Appendix
|
|
532
|
-
|
|
533
|
-
### Protocol Messages
|
|
534
|
-
|
|
535
|
-
**Worker Registration (worker → server)**
|
|
271
|
+
Workers must send the token in registration:
|
|
536
272
|
|
|
537
273
|
```json
|
|
538
274
|
{
|
|
539
275
|
"type": "worker_registration",
|
|
540
|
-
"workerId": "
|
|
541
|
-
"workerName": "
|
|
542
|
-
"capabilities":
|
|
543
|
-
"authToken
|
|
276
|
+
"workerId": "worker-1",
|
|
277
|
+
"workerName": "My Worker",
|
|
278
|
+
"capabilities": { ... },
|
|
279
|
+
"authToken": "secret-token"
|
|
544
280
|
}
|
|
545
281
|
```
|
|
546
282
|
|
|
547
|
-
|
|
283
|
+
### Heartbeat Protocol
|
|
548
284
|
|
|
549
|
-
|
|
550
|
-
{
|
|
551
|
-
"type": "worker_registration_ack",
|
|
552
|
-
"success": "boolean",
|
|
553
|
-
"error?": "string",
|
|
554
|
-
"sessionId?": "string",
|
|
555
|
-
"heartbeatIntervalMs?": "number"
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
**Heartbeat (worker → server)**
|
|
285
|
+
Workers must send periodic heartbeat messages:
|
|
560
286
|
|
|
561
287
|
```json
|
|
562
288
|
{
|
|
563
289
|
"type": "heartbeat",
|
|
564
|
-
"workerId": "
|
|
565
|
-
"timestamp": "
|
|
290
|
+
"workerId": "worker-1",
|
|
291
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
566
292
|
}
|
|
567
293
|
```
|
|
568
294
|
|
|
569
|
-
|
|
295
|
+
The server responds with:
|
|
570
296
|
|
|
571
297
|
```json
|
|
572
298
|
{
|
|
573
299
|
"type": "heartbeat_ack",
|
|
574
|
-
"timestamp": "
|
|
575
|
-
"nextHeartbeatDeadline": "
|
|
300
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
301
|
+
"nextHeartbeatDeadline": "2024-01-01T00:01:15.000Z"
|
|
576
302
|
}
|
|
577
|
-
```
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A worker is considered unhealthy if its heartbeat exceeds `heartbeatTimeoutMs`. It is marked dead and disconnected after `3x` the timeout.
|