@hardlydifficult/worker-server 1.0.9 → 1.0.10
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 +271 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/worker-server
|
|
2
2
|
|
|
3
|
-
A WebSocket-based
|
|
3
|
+
A WebSocket-based server for managing remote worker connections with health monitoring, message routing, and load balancing.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,64 +13,227 @@ npm install @hardlydifficult/worker-server
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
15
15
|
|
|
16
|
+
// Create and start the server
|
|
16
17
|
const server = new WorkerServer({ port: 3000 });
|
|
17
18
|
|
|
18
|
-
server.
|
|
19
|
-
console.log("Worker
|
|
20
|
-
|
|
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
|
+
});
|
|
21
26
|
|
|
22
|
-
server.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
27
|
+
server.onWorkerDisconnected((worker) => {
|
|
28
|
+
console.log(`Worker ${worker.name} (${worker.id}) disconnected`);
|
|
29
|
+
});
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
// Route messages by type
|
|
32
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
33
|
+
console.log(`Worker ${worker.id} completed request: ${message.requestId}`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
29
36
|
```
|
|
30
37
|
|
|
31
38
|
## Core Concepts
|
|
32
39
|
|
|
33
|
-
### Worker Registration
|
|
40
|
+
### Worker Registration & Lifecycle
|
|
34
41
|
|
|
35
|
-
Workers connect via WebSocket and register with
|
|
42
|
+
Workers connect via WebSocket and register with authentication (optional). The server tracks their status, heartbeat, and request load.
|
|
36
43
|
|
|
37
44
|
```typescript
|
|
45
|
+
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
46
|
+
|
|
47
|
+
const server = new WorkerServer({
|
|
48
|
+
port: 3000,
|
|
49
|
+
authToken: "secret-token" // Optional
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
server.start();
|
|
38
53
|
server.onWorkerConnected((worker) => {
|
|
39
|
-
|
|
40
|
-
console.log(
|
|
41
|
-
`Worker ${worker.name} supports: ${worker.capabilities.models.map(m => m.modelId).join(", ")}`
|
|
42
|
-
);
|
|
54
|
+
console.log("Worker connected:", worker.id, worker.name);
|
|
43
55
|
});
|
|
44
56
|
```
|
|
45
57
|
|
|
58
|
+
Workers send a `worker_registration` message:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"type": "worker_registration",
|
|
63
|
+
"workerId": "worker-1",
|
|
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
|
+
```
|
|
80
|
+
|
|
81
|
+
The server responds with a registration acknowledgment:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"type": "worker_registration_ack",
|
|
86
|
+
"success": true,
|
|
87
|
+
"sessionId": "uuid-here",
|
|
88
|
+
"heartbeatIntervalMs": 15000
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
46
92
|
### Message Routing
|
|
47
93
|
|
|
48
|
-
Messages are
|
|
94
|
+
Messages are routed by the `type` field. Handlers receive the worker info and message payload.
|
|
49
95
|
|
|
50
96
|
```typescript
|
|
51
|
-
server.onWorkerMessage("
|
|
52
|
-
console.log(
|
|
97
|
+
server.onWorkerMessage("status_update", (worker, message) => {
|
|
98
|
+
console.log(`Worker ${worker.id} status: ${message.statusText}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Send messages to workers
|
|
102
|
+
server.send(workerId, {
|
|
103
|
+
type: "execute",
|
|
104
|
+
requestId: "req-1",
|
|
105
|
+
prompt: "Hello, world!"
|
|
53
106
|
});
|
|
54
107
|
|
|
55
|
-
|
|
108
|
+
// Broadcast to all workers
|
|
56
109
|
server.broadcast({ type: "shutdown" });
|
|
57
110
|
```
|
|
58
111
|
|
|
59
|
-
###
|
|
112
|
+
### Worker Selection & Load Balancing
|
|
60
113
|
|
|
61
|
-
|
|
114
|
+
Select workers by model support or use any available worker. Workers are automatically assigned least-loaded.
|
|
62
115
|
|
|
63
116
|
```typescript
|
|
64
|
-
|
|
117
|
+
// Get the least-loaded worker that supports a specific model
|
|
118
|
+
const worker = server.getAvailableWorker("gpt-4");
|
|
65
119
|
if (worker) {
|
|
66
|
-
server.
|
|
67
|
-
server.send(worker.id, { type: "start", requestId });
|
|
120
|
+
server.send(worker.id, { type: "execute", prompt: "..." });
|
|
68
121
|
}
|
|
69
122
|
|
|
70
|
-
//
|
|
123
|
+
// Get any available worker (model-agnostic)
|
|
124
|
+
const anyWorker = server.getAnyAvailableWorker();
|
|
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);
|
|
132
|
+
|
|
133
|
+
// Release when the response is received (optionally increment completed count)
|
|
71
134
|
server.releaseRequest(requestId, { incrementCompleted: true });
|
|
72
135
|
```
|
|
73
136
|
|
|
137
|
+
### Health Monitoring
|
|
138
|
+
|
|
139
|
+
Workers must send periodic heartbeats. Unresponsive workers are marked unhealthy and eventually removed.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Heartbeat message format (worker → server)
|
|
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"
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Health checks run automatically at the configured interval (default: 10s). Workers missing heartbeats for >3× timeout are removed.
|
|
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
|
|
168
|
+
|
|
169
|
+
Workers can specify per-category concurrency limits for fine-grained control.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const server = new WorkerServer({ port: 3000 });
|
|
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");
|
|
189
|
+
|
|
190
|
+
// Release without specifying category (looked up automatically)
|
|
191
|
+
server.releaseRequest(requestId);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## HTTP & WebSocket Extensibility
|
|
195
|
+
|
|
196
|
+
### Custom HTTP Endpoints
|
|
197
|
+
|
|
198
|
+
Add HTTP handlers that return `true` when they handle the request.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
server.addHttpHandler(async (req, res) => {
|
|
202
|
+
if (req.url === "/health") {
|
|
203
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
204
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Additional WebSocket Endpoints
|
|
212
|
+
|
|
213
|
+
Register additional WebSocket paths for non-worker connections.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
server.addWebSocketEndpoint("/ws/admin", (ws) => {
|
|
217
|
+
ws.on("message", (data) => {
|
|
218
|
+
// Handle admin messages
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Worker Info
|
|
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
|
+
|
|
74
237
|
## Core Components
|
|
75
238
|
|
|
76
239
|
### WorkerServer
|
|
@@ -80,7 +243,7 @@ Main server class managing WebSocket connections, HTTP endpoints, and worker poo
|
|
|
80
243
|
#### Constructor
|
|
81
244
|
|
|
82
245
|
| Parameter | Type | Default | Description |
|
|
83
|
-
|
|
246
|
+
|-----------|------|---------|-------------|
|
|
84
247
|
| `port` | `number` | — | HTTP + WebSocket server port |
|
|
85
248
|
| `authToken` | `string` (optional) | — | Token required for worker registration |
|
|
86
249
|
| `heartbeatTimeoutMs` | `number` | 60000 | Timeout before marking worker unhealthy |
|
|
@@ -163,14 +326,14 @@ server.releaseRequest("req-123", { incrementCompleted: true });
|
|
|
163
326
|
#### Extensibility
|
|
164
327
|
|
|
165
328
|
| Method | Description |
|
|
166
|
-
|
|
329
|
+
|--------|-------------|
|
|
167
330
|
| `addHttpHandler(handler)` | Add an HTTP handler (called in order until one returns `true`) |
|
|
168
331
|
| `addWebSocketEndpoint(path, handler)` | Add a WebSocket endpoint at a custom path |
|
|
169
332
|
|
|
170
333
|
#### Event Handlers
|
|
171
334
|
|
|
172
335
|
| Method | Return | Description |
|
|
173
|
-
|
|
336
|
+
|--------|--------|-------------|
|
|
174
337
|
| `onWorkerConnected(handler)` | `() => void` | Called when worker registers |
|
|
175
338
|
| `onWorkerDisconnected(handler)` | `() => void` | Called when worker disconnects (includes pending requests) |
|
|
176
339
|
| `onWorkerMessage(type, handler)` | `() => void` | Register handler for a message type |
|
|
@@ -180,7 +343,7 @@ server.releaseRequest("req-123", { incrementCompleted: true });
|
|
|
180
343
|
Internal class managing worker state and selection. Exposed via `WorkerServer`.
|
|
181
344
|
|
|
182
345
|
| Method | Description |
|
|
183
|
-
|
|
346
|
+
|--------|-------------|
|
|
184
347
|
| `getAvailableWorker(model, category?)` | Get least-loaded available worker supporting model |
|
|
185
348
|
| `getAnyAvailableWorker()` | Get any available/busy worker |
|
|
186
349
|
| `trackRequest(workerId, requestId, category?)` | Mark request as in-progress |
|
|
@@ -284,66 +447,63 @@ server.trackRequest("worker-1", "req-1", "inference");
|
|
|
284
447
|
server.releaseRequest("req-1"); // category looked up automatically
|
|
285
448
|
```
|
|
286
449
|
|
|
287
|
-
##
|
|
450
|
+
## Type Definitions
|
|
288
451
|
|
|
289
|
-
###
|
|
290
|
-
|
|
291
|
-
| Field | Type | Description |
|
|
292
|
-
|--|------|---|
|
|
293
|
-
| `id` | `string` | Unique worker identifier |
|
|
294
|
-
| `name` | `string` | Worker-assigned name |
|
|
295
|
-
| `status` | `WorkerStatus` | Current status (`available`, `busy`, `draining`, `unhealthy`) |
|
|
296
|
-
| `capabilities` | `WorkerCapabilities` | Supported models and limits |
|
|
297
|
-
| `sessionId` | `string` | Unique session identifier |
|
|
298
|
-
| `connectedAt` | `Date` | Connection timestamp |
|
|
299
|
-
| `lastHeartbeat` | `Date` | Last heartbeat timestamp |
|
|
300
|
-
| `activeRequests` | `number` | Currently active requests |
|
|
301
|
-
| `completedRequests` | `number` | Completed request count |
|
|
302
|
-
| `pendingRequestIds` | `ReadonlySet<string>` | Pending request IDs |
|
|
303
|
-
| `categoryActiveRequests` | `ReadonlyMap<string, number>` | Active requests per category |
|
|
304
|
-
|
|
305
|
-
### WorkerCapabilities
|
|
452
|
+
### WorkerStatus
|
|
306
453
|
|
|
307
|
-
|
|
|
308
|
-
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
454
|
+
| Value | Description |
|
|
455
|
+
|-------|-------------|
|
|
456
|
+
| `available` | Worker can accept new requests |
|
|
457
|
+
| `busy` | Worker is at max concurrent requests |
|
|
458
|
+
| `draining` | Worker is shutting down |
|
|
459
|
+
| `unhealthy` | Worker heartbeat has timed out |
|
|
313
460
|
|
|
314
461
|
### ModelInfo
|
|
315
462
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
463
|
+
```typescript
|
|
464
|
+
interface ModelInfo {
|
|
465
|
+
modelId: string;
|
|
466
|
+
displayName: string;
|
|
467
|
+
maxContextTokens: number;
|
|
468
|
+
maxOutputTokens: number;
|
|
469
|
+
supportsStreaming: boolean;
|
|
470
|
+
supportsVision?: boolean;
|
|
471
|
+
supportsTools?: boolean;
|
|
472
|
+
}
|
|
473
|
+
```
|
|
325
474
|
|
|
326
|
-
###
|
|
475
|
+
### WorkerCapabilities
|
|
327
476
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
477
|
+
```typescript
|
|
478
|
+
interface WorkerCapabilities {
|
|
479
|
+
models: ModelInfo[];
|
|
480
|
+
maxConcurrentRequests: number;
|
|
481
|
+
metadata?: Record<string, unknown>;
|
|
482
|
+
concurrencyLimits?: Record<string, number>;
|
|
483
|
+
}
|
|
484
|
+
```
|
|
332
485
|
|
|
333
|
-
###
|
|
486
|
+
### WorkerInfo
|
|
334
487
|
|
|
335
488
|
```typescript
|
|
336
|
-
interface
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
489
|
+
interface WorkerInfo {
|
|
490
|
+
id: string;
|
|
491
|
+
name: string;
|
|
492
|
+
status: WorkerStatus;
|
|
493
|
+
capabilities: WorkerCapabilities;
|
|
494
|
+
sessionId: string;
|
|
495
|
+
connectedAt: Date;
|
|
496
|
+
lastHeartbeat: Date;
|
|
497
|
+
activeRequests: number;
|
|
498
|
+
completedRequests: number;
|
|
499
|
+
pendingRequestIds: ReadonlySet<string>;
|
|
500
|
+
categoryActiveRequests: ReadonlyMap<string, number>;
|
|
343
501
|
}
|
|
344
502
|
```
|
|
345
503
|
|
|
346
|
-
|
|
504
|
+
## Logging
|
|
505
|
+
|
|
506
|
+
The server accepts a logger implementing `WorkerServerLogger`:
|
|
347
507
|
|
|
348
508
|
```typescript
|
|
349
509
|
interface WorkerServerLogger {
|
|
@@ -354,50 +514,64 @@ interface WorkerServerLogger {
|
|
|
354
514
|
}
|
|
355
515
|
```
|
|
356
516
|
|
|
517
|
+
Default is a no-op logger. To use a custom logger:
|
|
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
|
+
|
|
357
531
|
## Appendix
|
|
358
532
|
|
|
359
533
|
### Protocol Messages
|
|
360
534
|
|
|
361
535
|
**Worker Registration (worker → server)**
|
|
362
536
|
|
|
363
|
-
```
|
|
537
|
+
```json
|
|
364
538
|
{
|
|
365
|
-
type: "worker_registration",
|
|
366
|
-
workerId: string,
|
|
367
|
-
workerName: string,
|
|
368
|
-
capabilities: WorkerCapabilities,
|
|
369
|
-
authToken
|
|
539
|
+
"type": "worker_registration",
|
|
540
|
+
"workerId": "string",
|
|
541
|
+
"workerName": "string",
|
|
542
|
+
"capabilities": WorkerCapabilities,
|
|
543
|
+
"authToken?": "string"
|
|
370
544
|
}
|
|
371
545
|
```
|
|
372
546
|
|
|
373
547
|
**Registration Acknowledgment (server → worker)**
|
|
374
548
|
|
|
375
|
-
```
|
|
549
|
+
```json
|
|
376
550
|
{
|
|
377
|
-
type: "worker_registration_ack",
|
|
378
|
-
success: boolean,
|
|
379
|
-
error
|
|
380
|
-
sessionId
|
|
381
|
-
heartbeatIntervalMs
|
|
551
|
+
"type": "worker_registration_ack",
|
|
552
|
+
"success": "boolean",
|
|
553
|
+
"error?": "string",
|
|
554
|
+
"sessionId?": "string",
|
|
555
|
+
"heartbeatIntervalMs?": "number"
|
|
382
556
|
}
|
|
383
557
|
```
|
|
384
558
|
|
|
385
559
|
**Heartbeat (worker → server)**
|
|
386
560
|
|
|
387
|
-
```
|
|
561
|
+
```json
|
|
388
562
|
{
|
|
389
|
-
type: "heartbeat",
|
|
390
|
-
workerId: string,
|
|
391
|
-
timestamp: string
|
|
563
|
+
"type": "heartbeat",
|
|
564
|
+
"workerId": "string",
|
|
565
|
+
"timestamp": "string"
|
|
392
566
|
}
|
|
393
567
|
```
|
|
394
568
|
|
|
395
569
|
**Heartbeat Acknowledgment (server → worker)**
|
|
396
570
|
|
|
397
|
-
```
|
|
571
|
+
```json
|
|
398
572
|
{
|
|
399
|
-
type: "heartbeat_ack",
|
|
400
|
-
timestamp: string,
|
|
401
|
-
nextHeartbeatDeadline: string
|
|
573
|
+
"type": "heartbeat_ack",
|
|
574
|
+
"timestamp": "string",
|
|
575
|
+
"nextHeartbeatDeadline": "string"
|
|
402
576
|
}
|
|
403
577
|
```
|