@hardlydifficult/worker-server 1.0.7 → 1.0.8
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 +202 -115
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/worker-server
|
|
2
2
|
|
|
3
|
-
WebSocket-based worker server with health monitoring, request routing, and load balancing.
|
|
3
|
+
A WebSocket-based worker server for managing remote worker connections with health monitoring, request routing, and load balancing.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,29 +13,80 @@ npm install @hardlydifficult/worker-server
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
15
15
|
|
|
16
|
-
const server = new WorkerServer({ port:
|
|
16
|
+
const server = new WorkerServer({ port: 3000 });
|
|
17
17
|
|
|
18
18
|
server.onWorkerConnected((worker) => {
|
|
19
|
-
console.log(
|
|
19
|
+
console.log("Worker connected:", worker.id);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.onWorkerMessage("work_request", async (worker, message) => {
|
|
23
|
+
console.log("Received request from", worker.id, message);
|
|
24
|
+
server.send(worker.id, { type: "work_complete", requestId: message.requestId });
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
server.
|
|
23
|
-
|
|
27
|
+
await server.start();
|
|
28
|
+
console.log("Worker server running on port 3000");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
### Worker Registration
|
|
34
|
+
|
|
35
|
+
Workers connect via WebSocket and register with identity and capabilities. The server supports optional authentication and tracks worker health via heartbeat protocol.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
server.onWorkerConnected((worker) => {
|
|
39
|
+
// Worker capabilities include supported models and concurrency limits
|
|
40
|
+
console.log(
|
|
41
|
+
`Worker ${worker.name} supports: ${worker.capabilities.models.map(m => m.modelId).join(", ")}`
|
|
42
|
+
);
|
|
24
43
|
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Message Routing
|
|
47
|
+
|
|
48
|
+
Messages are dispatched by `type` field to registered handlers.
|
|
25
49
|
|
|
50
|
+
```typescript
|
|
26
51
|
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
27
|
-
console.log(
|
|
52
|
+
console.log("Work completed for", message.requestId);
|
|
28
53
|
});
|
|
29
54
|
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
server.send(workerId, { type: "work_request", requestId: "req-1" });
|
|
56
|
+
server.broadcast({ type: "shutdown" });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Request Tracking & Load Balancing
|
|
60
|
+
|
|
61
|
+
Track active requests and select the least-loaded available worker.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const worker = server.getAvailableWorker("sonnet", "inference");
|
|
65
|
+
if (worker) {
|
|
66
|
+
server.trackRequest(worker.id, requestId, "inference");
|
|
67
|
+
server.send(worker.id, { type: "start", requestId });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Later, release the request
|
|
71
|
+
server.releaseRequest(requestId, { incrementCompleted: true });
|
|
32
72
|
```
|
|
33
73
|
|
|
34
74
|
## Core Components
|
|
35
75
|
|
|
36
76
|
### WorkerServer
|
|
37
77
|
|
|
38
|
-
|
|
78
|
+
Main server class managing WebSocket connections, HTTP endpoints, and worker pool.
|
|
79
|
+
|
|
80
|
+
#### Constructor
|
|
81
|
+
|
|
82
|
+
| Parameter | Type | Default | Description |
|
|
83
|
+
|--|------|---------|-----|
|
|
84
|
+
| `port` | `number` | — | HTTP + WebSocket server port |
|
|
85
|
+
| `authToken` | `string` (optional) | — | Token required for worker registration |
|
|
86
|
+
| `heartbeatTimeoutMs` | `number` | 60000 | Timeout before marking worker unhealthy |
|
|
87
|
+
| `healthCheckIntervalMs` | `number` | 10000 | Interval for health checks |
|
|
88
|
+
| `heartbeatIntervalMs` | `number` | 15000 | Heartbeat interval communicated to workers |
|
|
89
|
+
| `logger` | `WorkerServerLogger` (optional) | No-op | Logger instance |
|
|
39
90
|
|
|
40
91
|
#### Lifecycle Management
|
|
41
92
|
|
|
@@ -109,49 +160,36 @@ server.trackRequest("worker-1", "req-123", "inference");
|
|
|
109
160
|
server.releaseRequest("req-123", { incrementCompleted: true });
|
|
110
161
|
```
|
|
111
162
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Manages worker lifecycle, request tracking, health checks, and message routing.
|
|
163
|
+
#### Extensibility
|
|
115
164
|
|
|
116
|
-
|
|
165
|
+
| Method | Description |
|
|
166
|
+
|--|---|
|
|
167
|
+
| `addHttpHandler(handler)` | Add an HTTP handler (called in order until one returns `true`) |
|
|
168
|
+
| `addWebSocketEndpoint(path, handler)` | Add a WebSocket endpoint at a custom path |
|
|
117
169
|
|
|
118
|
-
|
|
119
|
-
- `available`: Ready to accept new requests
|
|
120
|
-
- `busy`: At maximum concurrent request capacity
|
|
121
|
-
- `draining`: In the process of shutting down
|
|
122
|
-
- `unhealthy`: Heartbeat missed beyond timeout threshold
|
|
123
|
-
|
|
124
|
-
#### Pool Operations
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
import { WorkerPool, WorkerStatus, type ConnectedWorker } from "@hardlydifficult/worker-server";
|
|
128
|
-
|
|
129
|
-
const pool = new WorkerPool();
|
|
130
|
-
|
|
131
|
-
// Get the least-loaded available worker supporting a model
|
|
132
|
-
const worker = pool.getAvailableWorker("sonnet-3.5", "inference");
|
|
133
|
-
|
|
134
|
-
// Get any available or busy worker
|
|
135
|
-
const anyWorker = pool.getAnyAvailableWorker();
|
|
136
|
-
|
|
137
|
-
// Get count statistics
|
|
138
|
-
const total = pool.getCount();
|
|
139
|
-
const available = pool.getAvailableCount();
|
|
170
|
+
#### Event Handlers
|
|
140
171
|
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
| Method | Return | Description |
|
|
173
|
+
|--|--|---|
|
|
174
|
+
| `onWorkerConnected(handler)` | `() => void` | Called when worker registers |
|
|
175
|
+
| `onWorkerDisconnected(handler)` | `() => void` | Called when worker disconnects (includes pending requests) |
|
|
176
|
+
| `onWorkerMessage(type, handler)` | `() => void` | Register handler for a message type |
|
|
143
177
|
|
|
144
|
-
|
|
145
|
-
pool.trackRequest("worker-1", "req-123");
|
|
146
|
-
pool.releaseRequest("req-123");
|
|
178
|
+
### WorkerPool
|
|
147
179
|
|
|
148
|
-
|
|
149
|
-
const deadWorkerIds = pool.checkHealth(60_000); // 60-second timeout
|
|
180
|
+
Internal class managing worker state and selection. Exposed via `WorkerServer`.
|
|
150
181
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
| Method | Description |
|
|
183
|
+
|--|---|
|
|
184
|
+
| `getAvailableWorker(model, category?)` | Get least-loaded available worker supporting model |
|
|
185
|
+
| `getAnyAvailableWorker()` | Get any available/busy worker |
|
|
186
|
+
| `trackRequest(workerId, requestId, category?)` | Mark request as in-progress |
|
|
187
|
+
| `releaseRequest(requestId, { incrementCompleted? })` | Release tracked request |
|
|
188
|
+
| `getWorkerInfoList()` | Get public info for all workers |
|
|
189
|
+
| `checkHealth(timeoutMs)` | Return IDs of dead workers (heartbeat > 3x timeout) |
|
|
190
|
+
| `send(workerId, message)` | Send message to specific worker |
|
|
191
|
+
| `broadcast(message)` | Broadcast to all workers |
|
|
192
|
+
| `closeAll()` | Close all worker connections |
|
|
155
193
|
|
|
156
194
|
### ConnectionHandler
|
|
157
195
|
|
|
@@ -182,75 +220,6 @@ handler.onWorkerDisconnected((worker, pending) => {
|
|
|
182
220
|
});
|
|
183
221
|
```
|
|
184
222
|
|
|
185
|
-
## Types and Interfaces
|
|
186
|
-
|
|
187
|
-
### WorkerInfo
|
|
188
|
-
|
|
189
|
-
Public worker metadata exposed to consumers:
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
interface WorkerInfo {
|
|
193
|
-
id: string;
|
|
194
|
-
name: string;
|
|
195
|
-
status: WorkerStatus;
|
|
196
|
-
capabilities: WorkerCapabilities;
|
|
197
|
-
sessionId: string;
|
|
198
|
-
connectedAt: Date;
|
|
199
|
-
lastHeartbeat: Date;
|
|
200
|
-
activeRequests: number;
|
|
201
|
-
completedRequests: number;
|
|
202
|
-
pendingRequestIds: ReadonlySet<string>;
|
|
203
|
-
categoryActiveRequests: ReadonlyMap<string, number>;
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### WorkerCapabilities
|
|
208
|
-
|
|
209
|
-
Describes what a worker can do:
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
interface WorkerCapabilities {
|
|
213
|
-
models: ModelInfo[];
|
|
214
|
-
maxConcurrentRequests: number;
|
|
215
|
-
metadata?: Record<string, unknown>;
|
|
216
|
-
concurrencyLimits?: Record<string, number>; // per-category limits
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
interface ModelInfo {
|
|
220
|
-
modelId: string;
|
|
221
|
-
displayName: string;
|
|
222
|
-
maxContextTokens: number;
|
|
223
|
-
maxOutputTokens: number;
|
|
224
|
-
supportsStreaming: boolean;
|
|
225
|
-
supportsVision?: boolean;
|
|
226
|
-
supportsTools?: boolean;
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Configuration Options
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
interface WorkerServerOptions {
|
|
234
|
-
port: number;
|
|
235
|
-
authToken?: string;
|
|
236
|
-
heartbeatTimeoutMs?: number; // default: 60000
|
|
237
|
-
healthCheckIntervalMs?: number; // default: 10000
|
|
238
|
-
heartbeatIntervalMs?: number; // default: 15000
|
|
239
|
-
logger?: WorkerServerLogger;
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Logger Interface
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
interface WorkerServerLogger {
|
|
247
|
-
debug(message: string, context?: Record<string, unknown>): void;
|
|
248
|
-
info(message: string, context?: Record<string, unknown>): void;
|
|
249
|
-
warn(message: string, context?: Record<string, unknown>): void;
|
|
250
|
-
error(message: string, context?: Record<string, unknown>): void;
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
223
|
## Advanced Features
|
|
255
224
|
|
|
256
225
|
### HTTP Endpoints
|
|
@@ -313,4 +282,122 @@ Requests are then tracked by category:
|
|
|
313
282
|
```typescript
|
|
314
283
|
server.trackRequest("worker-1", "req-1", "inference");
|
|
315
284
|
server.releaseRequest("req-1"); // category looked up automatically
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Types and Interfaces
|
|
288
|
+
|
|
289
|
+
### WorkerInfo
|
|
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
|
|
306
|
+
|
|
307
|
+
| Field | Type | Description |
|
|
308
|
+
|--|------|---|
|
|
309
|
+
| `models` | `ModelInfo[]` | Supported models |
|
|
310
|
+
| `maxConcurrentRequests` | `number` | Overall concurrency limit |
|
|
311
|
+
| `metadata?` | `Record<string, unknown>` | Optional metadata |
|
|
312
|
+
| `concurrencyLimits?` | `Record<string, number>` | Per-category concurrency limits |
|
|
313
|
+
|
|
314
|
+
### ModelInfo
|
|
315
|
+
|
|
316
|
+
| Field | Type | Description |
|
|
317
|
+
|--|------|---|
|
|
318
|
+
| `modelId` | `string` | Model identifier |
|
|
319
|
+
| `displayName` | `string` | Human-readable name |
|
|
320
|
+
| `maxContextTokens` | `number` | Maximum context window |
|
|
321
|
+
| `maxOutputTokens` | `number` | Maximum output length |
|
|
322
|
+
| `supportsStreaming` | `boolean` | Streaming support |
|
|
323
|
+
| `supportsVision?` | `boolean` | Vision support (optional) |
|
|
324
|
+
| `supportsTools?` | `boolean` | Tool use support (optional) |
|
|
325
|
+
|
|
326
|
+
### WorkerStatus
|
|
327
|
+
|
|
328
|
+
- `"available"` — Worker can accept new requests
|
|
329
|
+
- `"busy"` — Worker at max concurrency
|
|
330
|
+
- `"draining"` — Worker shutting down, no new requests
|
|
331
|
+
- `"unhealthy"` — Missed heartbeats
|
|
332
|
+
|
|
333
|
+
### Configuration Options
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
interface WorkerServerOptions {
|
|
337
|
+
port: number;
|
|
338
|
+
authToken?: string;
|
|
339
|
+
heartbeatTimeoutMs?: number; // default: 60000
|
|
340
|
+
healthCheckIntervalMs?: number; // default: 10000
|
|
341
|
+
heartbeatIntervalMs?: number; // default: 15000
|
|
342
|
+
logger?: WorkerServerLogger;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Logger Interface
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
interface WorkerServerLogger {
|
|
350
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
351
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
352
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
353
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Appendix
|
|
358
|
+
|
|
359
|
+
### Protocol Messages
|
|
360
|
+
|
|
361
|
+
**Worker Registration (worker → server)**
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
{
|
|
365
|
+
type: "worker_registration",
|
|
366
|
+
workerId: string,
|
|
367
|
+
workerName: string,
|
|
368
|
+
capabilities: WorkerCapabilities,
|
|
369
|
+
authToken?: string
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Registration Acknowledgment (server → worker)**
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
{
|
|
377
|
+
type: "worker_registration_ack",
|
|
378
|
+
success: boolean,
|
|
379
|
+
error?: string,
|
|
380
|
+
sessionId?: string,
|
|
381
|
+
heartbeatIntervalMs?: number
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Heartbeat (worker → server)**
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
{
|
|
389
|
+
type: "heartbeat",
|
|
390
|
+
workerId: string,
|
|
391
|
+
timestamp: string
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Heartbeat Acknowledgment (server → worker)**
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
{
|
|
399
|
+
type: "heartbeat_ack",
|
|
400
|
+
timestamp: string,
|
|
401
|
+
nextHeartbeatDeadline: string
|
|
402
|
+
}
|
|
316
403
|
```
|