@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.
Files changed (2) hide show
  1. package/README.md +271 -97
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/worker-server
2
2
 
3
- A WebSocket-based worker server for managing remote worker connections with health monitoring, request routing, and load balancing.
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.onWorkerConnected((worker) => {
19
- console.log("Worker connected:", worker.id);
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.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 });
25
- });
27
+ server.onWorkerDisconnected((worker) => {
28
+ console.log(`Worker ${worker.name} (${worker.id}) disconnected`);
29
+ });
26
30
 
27
- await server.start();
28
- console.log("Worker server running on port 3000");
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 identity and capabilities. The server supports optional authentication and tracks worker health via heartbeat protocol.
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
- // 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
- );
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 dispatched by `type` field to registered handlers.
94
+ Messages are routed by the `type` field. Handlers receive the worker info and message payload.
49
95
 
50
96
  ```typescript
51
- server.onWorkerMessage("work_complete", (worker, message) => {
52
- console.log("Work completed for", message.requestId);
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
- server.send(workerId, { type: "work_request", requestId: "req-1" });
108
+ // Broadcast to all workers
56
109
  server.broadcast({ type: "shutdown" });
57
110
  ```
58
111
 
59
- ### Request Tracking & Load Balancing
112
+ ### Worker Selection & Load Balancing
60
113
 
61
- Track active requests and select the least-loaded available worker.
114
+ Select workers by model support or use any available worker. Workers are automatically assigned least-loaded.
62
115
 
63
116
  ```typescript
64
- const worker = server.getAvailableWorker("sonnet", "inference");
117
+ // Get the least-loaded worker that supports a specific model
118
+ const worker = server.getAvailableWorker("gpt-4");
65
119
  if (worker) {
66
- server.trackRequest(worker.id, requestId, "inference");
67
- server.send(worker.id, { type: "start", requestId });
120
+ server.send(worker.id, { type: "execute", prompt: "..." });
68
121
  }
69
122
 
70
- // Later, release the request
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
- ## Types and Interfaces
450
+ ## Type Definitions
288
451
 
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
452
+ ### WorkerStatus
306
453
 
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 |
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
- | 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) |
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
- ### WorkerStatus
475
+ ### WorkerCapabilities
327
476
 
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
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
- ### Configuration Options
486
+ ### WorkerInfo
334
487
 
335
488
  ```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;
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
- ### Logger Interface
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
- ```typescript
537
+ ```json
364
538
  {
365
- type: "worker_registration",
366
- workerId: string,
367
- workerName: string,
368
- capabilities: WorkerCapabilities,
369
- authToken?: string
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
- ```typescript
549
+ ```json
376
550
  {
377
- type: "worker_registration_ack",
378
- success: boolean,
379
- error?: string,
380
- sessionId?: string,
381
- heartbeatIntervalMs?: number
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
- ```typescript
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
- ```typescript
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
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/worker-server",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [