@hardlydifficult/worker-server 1.0.10 → 1.0.11
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 +134 -434
- 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
|
|
|
@@ -11,468 +11,200 @@ npm install @hardlydifficult/worker-server
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
14
|
+
import { WorkerServer, WorkerStatus } from "@hardlydifficult/worker-server";
|
|
15
|
+
|
|
16
|
+
const server = new WorkerServer({
|
|
17
|
+
port: 19100,
|
|
18
|
+
authToken: "secret-token", // optional
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
server.onWorkerConnected((worker) => {
|
|
22
|
+
console.log(`Worker ${worker.name} connected with status ${worker.status}`);
|
|
23
|
+
});
|
|
15
24
|
|
|
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
|
-
});
|
|
25
|
+
server.onWorkerMessage("work_result", (worker, message) => {
|
|
26
|
+
console.log(`Worker ${worker.id} completed request:`, message);
|
|
35
27
|
});
|
|
28
|
+
|
|
29
|
+
await server.start();
|
|
30
|
+
console.log("Worker server listening on port", server.port);
|
|
36
31
|
```
|
|
37
32
|
|
|
38
33
|
## Core Concepts
|
|
39
34
|
|
|
40
|
-
###
|
|
35
|
+
### WorkerServer
|
|
41
36
|
|
|
42
|
-
|
|
37
|
+
Main entry point for managing worker connections via WebSocket.
|
|
43
38
|
|
|
44
39
|
```typescript
|
|
45
40
|
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
46
41
|
|
|
47
|
-
const server = new WorkerServer({
|
|
48
|
-
port:
|
|
49
|
-
authToken: "secret
|
|
42
|
+
const server = new WorkerServer({
|
|
43
|
+
port: 19100,
|
|
44
|
+
authToken: "secret", // optional
|
|
45
|
+
heartbeatTimeoutMs: 60_000,
|
|
46
|
+
healthCheckIntervalMs: 10_000,
|
|
47
|
+
heartbeatIntervalMs: 15_000,
|
|
48
|
+
logger: myLogger,
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
server.start();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
51
|
+
await server.start();
|
|
52
|
+
// Handle connections, messages, and shutdown
|
|
53
|
+
await server.stop();
|
|
56
54
|
```
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
#### Lifecycle Events
|
|
59
57
|
|
|
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"
|
|
78
|
-
}
|
|
79
|
-
```
|
|
58
|
+
| Method | Description |
|
|
59
|
+
|--------|-------------|
|
|
60
|
+
| `onWorkerConnected(handler)` | Called when a worker registers successfully |
|
|
61
|
+
| `onWorkerDisconnected(handler)` | Called when a worker disconnects; includes pending request IDs |
|
|
80
62
|
|
|
81
|
-
|
|
63
|
+
```typescript
|
|
64
|
+
server.onWorkerConnected((worker) => {
|
|
65
|
+
console.log(`Connected: ${worker.id} (${worker.name})`);
|
|
66
|
+
});
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
"success": true,
|
|
87
|
-
"sessionId": "uuid-here",
|
|
88
|
-
"heartbeatIntervalMs": 15000
|
|
89
|
-
}
|
|
68
|
+
server.onWorkerDisconnected((worker, pendingRequestIds) => {
|
|
69
|
+
console.log(`Disconnected: ${worker.id} with ${pendingRequestIds.size} pending requests`);
|
|
70
|
+
});
|
|
90
71
|
```
|
|
91
72
|
|
|
92
|
-
|
|
73
|
+
#### Message Routing
|
|
93
74
|
|
|
94
|
-
|
|
75
|
+
Register handlers for message types sent by workers:
|
|
95
76
|
|
|
96
77
|
```typescript
|
|
97
|
-
server.onWorkerMessage("
|
|
98
|
-
|
|
78
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
79
|
+
const { requestId, result } = message;
|
|
80
|
+
console.log(`Worker ${worker.id} completed ${requestId}`);
|
|
99
81
|
});
|
|
100
82
|
|
|
101
83
|
// Send messages to workers
|
|
102
|
-
server.send(workerId, {
|
|
103
|
-
type: "execute",
|
|
104
|
-
requestId: "req-1",
|
|
105
|
-
prompt: "Hello, world!"
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Broadcast to all workers
|
|
84
|
+
const success = server.send(workerId, { type: "work_request", requestId: "req-1" });
|
|
109
85
|
server.broadcast({ type: "shutdown" });
|
|
110
86
|
```
|
|
111
87
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Select workers by model support or use any available worker. Workers are automatically assigned least-loaded.
|
|
88
|
+
#### Worker Selection & Pool Queries
|
|
115
89
|
|
|
116
90
|
```typescript
|
|
117
|
-
// Get
|
|
118
|
-
const worker = server.getAvailableWorker("
|
|
119
|
-
if (worker) {
|
|
120
|
-
server.send(worker.id, { type: "execute", prompt: "..." });
|
|
121
|
-
}
|
|
91
|
+
// Get least-loaded worker supporting a model
|
|
92
|
+
const worker = server.getAvailableWorker("sonnet");
|
|
122
93
|
|
|
123
94
|
// Get any available worker (model-agnostic)
|
|
124
|
-
const
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Request tracking ensures accurate load reporting:
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
// Track when a request is assigned
|
|
131
|
-
server.trackRequest(worker.id, requestId);
|
|
95
|
+
const any = server.getAnyAvailableWorker();
|
|
132
96
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Health Monitoring
|
|
138
|
-
|
|
139
|
-
Workers must send periodic heartbeats. Unresponsive workers are marked unhealthy and eventually removed.
|
|
97
|
+
// Slot counts
|
|
98
|
+
console.log("Available slots:", server.getAvailableSlotCount("sonnet", "local"));
|
|
140
99
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
"type": "heartbeat",
|
|
145
|
-
"workerId": "worker-1",
|
|
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"
|
|
100
|
+
// Worker info
|
|
101
|
+
for (const info of server.getWorkerInfo()) {
|
|
102
|
+
console.log(`${info.name}: ${info.status} (${info.activeRequests}/${info.capabilities.maxConcurrentRequests})`);
|
|
154
103
|
}
|
|
155
104
|
```
|
|
156
105
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
const server = new WorkerServer({
|
|
161
|
-
port: 3000,
|
|
162
|
-
heartbeatTimeoutMs: 60_000, // 60 seconds before unhealthy
|
|
163
|
-
healthCheckIntervalMs: 10_000, // Check every 10 seconds
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Category-Aware Concurrency
|
|
106
|
+
#### Request Tracking
|
|
168
107
|
|
|
169
|
-
|
|
108
|
+
Track and release requests for accurate availability:
|
|
170
109
|
|
|
171
110
|
```typescript
|
|
172
|
-
|
|
111
|
+
// When assigning a request to a worker
|
|
112
|
+
server.trackRequest(workerId, requestId, "local");
|
|
173
113
|
|
|
174
|
-
//
|
|
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");
|
|
189
|
-
|
|
190
|
-
// Release without specifying category (looked up automatically)
|
|
191
|
-
server.releaseRequest(requestId);
|
|
114
|
+
// When the request completes
|
|
115
|
+
server.releaseRequest(requestId, { incrementCompleted: true });
|
|
192
116
|
```
|
|
193
117
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
### Custom HTTP Endpoints
|
|
118
|
+
#### Extensibility
|
|
197
119
|
|
|
198
|
-
Add HTTP
|
|
120
|
+
Add HTTP endpoints and custom WebSocket paths:
|
|
199
121
|
|
|
200
122
|
```typescript
|
|
123
|
+
// HTTP handler
|
|
201
124
|
server.addHttpHandler(async (req, res) => {
|
|
202
125
|
if (req.url === "/health") {
|
|
203
126
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
204
|
-
res.end(JSON.stringify({
|
|
127
|
+
res.end(JSON.stringify({ ok: true }));
|
|
205
128
|
return true;
|
|
206
129
|
}
|
|
207
130
|
return false;
|
|
208
131
|
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Additional WebSocket Endpoints
|
|
212
132
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
server.addWebSocketEndpoint("/ws/admin", (ws) => {
|
|
217
|
-
ws.on("message", (data) => {
|
|
218
|
-
// Handle admin messages
|
|
219
|
-
});
|
|
133
|
+
// Custom WebSocket endpoint
|
|
134
|
+
server.addWebSocketEndpoint("/ws/dashboard", (ws) => {
|
|
135
|
+
ws.send(JSON.stringify({ type: "hello" }));
|
|
220
136
|
});
|
|
221
137
|
```
|
|
222
138
|
|
|
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
|
-
```
|
|
139
|
+
### WorkerPool
|
|
302
140
|
|
|
303
|
-
|
|
141
|
+
Low-level pool manager for worker state and selection.
|
|
304
142
|
|
|
305
143
|
```typescript
|
|
306
|
-
|
|
307
|
-
const worker = server.getAvailableWorker("sonnet-3.5");
|
|
144
|
+
import { WorkerPool, WorkerStatus } from "@hardlydifficult/worker-server";
|
|
308
145
|
|
|
309
|
-
|
|
310
|
-
const anyWorker = server.getAnyAvailableWorker();
|
|
146
|
+
const pool = new WorkerPool(logger);
|
|
311
147
|
|
|
312
|
-
|
|
313
|
-
|
|
148
|
+
pool.add(worker);
|
|
149
|
+
pool.remove(workerId);
|
|
150
|
+
const worker = pool.get(workerId);
|
|
314
151
|
```
|
|
315
152
|
|
|
316
|
-
####
|
|
153
|
+
#### Selection Logic
|
|
317
154
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
155
|
+
- `getAvailableWorker(model, category?)`: Returns least-loaded worker supporting the model, respecting per-category concurrency limits
|
|
156
|
+
- `getAnyAvailableWorker()`: Returns any worker regardless of model (both Available and Busy)
|
|
157
|
+
- `getAvailableSlotCount(model, category?)`: Total free slots across all available workers for the model
|
|
321
158
|
|
|
322
|
-
|
|
323
|
-
server.releaseRequest("req-123", { incrementCompleted: true });
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Extensibility
|
|
159
|
+
#### Request Management
|
|
327
160
|
|
|
328
161
|
| Method | Description |
|
|
329
162
|
|--------|-------------|
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
|
|
333
|
-
#### Event Handlers
|
|
163
|
+
| `trackRequest(workerId, requestId, category?)` | Marks request as in-flight and updates status |
|
|
164
|
+
| `releaseRequest(requestId, options?)` | Decrements active count, optionally increments completed count |
|
|
334
165
|
|
|
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 |
|
|
340
|
-
|
|
341
|
-
### WorkerPool
|
|
342
|
-
|
|
343
|
-
Internal class managing worker state and selection. Exposed via `WorkerServer`.
|
|
166
|
+
#### Health Monitoring
|
|
344
167
|
|
|
345
168
|
| Method | Description |
|
|
346
169
|
|--------|-------------|
|
|
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 |
|
|
170
|
+
| `checkHealth(timeoutMs)` | Returns IDs of workers exceeding `3x` timeout; marks unhealthy ones |
|
|
356
171
|
|
|
357
172
|
### ConnectionHandler
|
|
358
173
|
|
|
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);
|
|
174
|
+
Handles WebSocket lifecycle, registration, heartbeats, and message routing. Most consumers use `WorkerServer`, which encapsulates this.
|
|
367
175
|
|
|
368
|
-
|
|
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
|
-
});
|
|
176
|
+
### Types & Interfaces
|
|
380
177
|
|
|
381
|
-
|
|
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
|
|
416
|
-
|
|
417
|
-
Optionally require authentication tokens from workers:
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
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:
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
const capabilities = {
|
|
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
|
-
```
|
|
442
|
-
|
|
443
|
-
Requests are then tracked by category:
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
server.trackRequest("worker-1", "req-1", "inference");
|
|
447
|
-
server.releaseRequest("req-1"); // category looked up automatically
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
## Type Definitions
|
|
451
|
-
|
|
452
|
-
### WorkerStatus
|
|
178
|
+
#### `WorkerStatus`
|
|
453
179
|
|
|
454
180
|
| Value | Description |
|
|
455
181
|
|-------|-------------|
|
|
456
182
|
| `available` | Worker can accept new requests |
|
|
457
|
-
| `busy` | Worker
|
|
458
|
-
| `draining` | Worker
|
|
459
|
-
| `unhealthy` | Worker heartbeat
|
|
183
|
+
| `busy` | Worker at capacity, but can accept model-agnostic tasks |
|
|
184
|
+
| `draining` | Worker finishing current work before shutdown |
|
|
185
|
+
| `unhealthy` | Worker failed heartbeat checks |
|
|
186
|
+
|
|
187
|
+
#### `WorkerInfo`
|
|
460
188
|
|
|
461
|
-
|
|
189
|
+
Public worker metadata (excludes raw WebSocket):
|
|
462
190
|
|
|
463
191
|
```typescript
|
|
464
|
-
interface
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
192
|
+
interface WorkerInfo {
|
|
193
|
+
readonly id: string;
|
|
194
|
+
readonly name: string;
|
|
195
|
+
readonly status: WorkerStatus;
|
|
196
|
+
readonly capabilities: WorkerCapabilities;
|
|
197
|
+
readonly sessionId: string;
|
|
198
|
+
readonly connectedAt: Date;
|
|
199
|
+
readonly lastHeartbeat: Date;
|
|
200
|
+
readonly activeRequests: number;
|
|
201
|
+
readonly completedRequests: number;
|
|
202
|
+
readonly pendingRequestIds: ReadonlySet<string>;
|
|
203
|
+
readonly categoryActiveRequests: ReadonlyMap<string, number>;
|
|
472
204
|
}
|
|
473
205
|
```
|
|
474
206
|
|
|
475
|
-
|
|
207
|
+
#### `WorkerCapabilities`
|
|
476
208
|
|
|
477
209
|
```typescript
|
|
478
210
|
interface WorkerCapabilities {
|
|
@@ -483,95 +215,63 @@ interface WorkerCapabilities {
|
|
|
483
215
|
}
|
|
484
216
|
```
|
|
485
217
|
|
|
486
|
-
|
|
218
|
+
#### `ModelInfo`
|
|
487
219
|
|
|
488
220
|
```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>;
|
|
221
|
+
interface ModelInfo {
|
|
222
|
+
modelId: string;
|
|
223
|
+
displayName: string;
|
|
224
|
+
maxContextTokens: number;
|
|
225
|
+
maxOutputTokens: number;
|
|
226
|
+
supportsStreaming: boolean;
|
|
227
|
+
supportsVision?: boolean;
|
|
228
|
+
supportsTools?: boolean;
|
|
501
229
|
}
|
|
502
230
|
```
|
|
503
231
|
|
|
504
|
-
|
|
232
|
+
### Secure Authentication
|
|
505
233
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
interface WorkerServerLogger {
|
|
510
|
-
debug(message: string, context?: Record<string, unknown>): void;
|
|
511
|
-
info(message: string, context?: Record<string, unknown>): void;
|
|
512
|
-
warn(message: string, context?: Record<string, unknown>): void;
|
|
513
|
-
error(message: string, context?: Record<string, unknown>): void;
|
|
514
|
-
}
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
Default is a no-op logger. To use a custom logger:
|
|
234
|
+
Authentication tokens are compared using timing-safe comparison to prevent brute-force attacks:
|
|
518
235
|
|
|
519
236
|
```typescript
|
|
520
237
|
const server = new WorkerServer({
|
|
521
|
-
port:
|
|
522
|
-
|
|
523
|
-
debug: console.debug,
|
|
524
|
-
info: console.info,
|
|
525
|
-
warn: console.warn,
|
|
526
|
-
error: console.error,
|
|
527
|
-
},
|
|
238
|
+
port: 19100,
|
|
239
|
+
authToken: "secret-token",
|
|
528
240
|
});
|
|
529
241
|
```
|
|
530
242
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
### Protocol Messages
|
|
534
|
-
|
|
535
|
-
**Worker Registration (worker → server)**
|
|
243
|
+
Workers must send:
|
|
536
244
|
|
|
537
245
|
```json
|
|
538
246
|
{
|
|
539
247
|
"type": "worker_registration",
|
|
540
|
-
"workerId": "
|
|
541
|
-
"workerName": "
|
|
542
|
-
"capabilities":
|
|
543
|
-
"authToken
|
|
248
|
+
"workerId": "worker-1",
|
|
249
|
+
"workerName": "My Worker",
|
|
250
|
+
"capabilities": { ... },
|
|
251
|
+
"authToken": "secret-token"
|
|
544
252
|
}
|
|
545
253
|
```
|
|
546
254
|
|
|
547
|
-
|
|
255
|
+
### Heartbeat Protocol
|
|
548
256
|
|
|
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)**
|
|
257
|
+
Workers must send periodic heartbeat messages:
|
|
560
258
|
|
|
561
259
|
```json
|
|
562
260
|
{
|
|
563
261
|
"type": "heartbeat",
|
|
564
|
-
"workerId": "
|
|
565
|
-
"timestamp": "
|
|
262
|
+
"workerId": "worker-1",
|
|
263
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
566
264
|
}
|
|
567
265
|
```
|
|
568
266
|
|
|
569
|
-
|
|
267
|
+
The server responds with:
|
|
570
268
|
|
|
571
269
|
```json
|
|
572
270
|
{
|
|
573
271
|
"type": "heartbeat_ack",
|
|
574
|
-
"timestamp": "
|
|
575
|
-
"nextHeartbeatDeadline": "
|
|
272
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
273
|
+
"nextHeartbeatDeadline": "2024-01-01T00:01:15.000Z"
|
|
576
274
|
}
|
|
577
|
-
```
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
A worker is considered unhealthy if its heartbeat exceeds `heartbeatTimeoutMs`. It is marked dead and disconnected after `3x` the timeout.
|