@hardlydifficult/worker-server 1.0.9 → 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.
Files changed (2) hide show
  1. package/README.md +160 -286
  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
+ WebSocket-based remote worker server with health monitoring, message routing, and load balancing.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,393 +11,267 @@ 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
15
 
16
- const server = new WorkerServer({ port: 3000 });
16
+ const server = new WorkerServer({
17
+ port: 19100,
18
+ authToken: "secret-token", // optional
19
+ });
17
20
 
18
21
  server.onWorkerConnected((worker) => {
19
- console.log("Worker connected:", worker.id);
22
+ console.log(`Worker ${worker.name} connected with status ${worker.status}`);
20
23
  });
21
24
 
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
+ server.onWorkerMessage("work_result", (worker, message) => {
26
+ console.log(`Worker ${worker.id} completed request:`, message);
25
27
  });
26
28
 
27
29
  await server.start();
28
- console.log("Worker server running on port 3000");
30
+ console.log("Worker server listening on port", server.port);
29
31
  ```
30
32
 
31
33
  ## Core Concepts
32
34
 
33
- ### Worker Registration
35
+ ### WorkerServer
34
36
 
35
- Workers connect via WebSocket and register with identity and capabilities. The server supports optional authentication and tracks worker health via heartbeat protocol.
37
+ Main entry point for managing worker connections via WebSocket.
36
38
 
37
39
  ```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
- );
43
- });
44
- ```
45
-
46
- ### Message Routing
47
-
48
- Messages are dispatched by `type` field to registered handlers.
40
+ import { WorkerServer } from "@hardlydifficult/worker-server";
49
41
 
50
- ```typescript
51
- server.onWorkerMessage("work_complete", (worker, message) => {
52
- console.log("Work completed for", message.requestId);
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,
53
49
  });
54
50
 
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 });
72
- ```
73
-
74
- ## Core Components
75
-
76
- ### WorkerServer
77
-
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 |
90
-
91
- #### Lifecycle Management
92
-
93
- ```typescript
94
- const server = new WorkerServer({ port: 8080, authToken: "secret" });
95
-
96
- // Start the server
97
51
  await server.start();
98
-
99
- // Stop the server gracefully
52
+ // Handle connections, messages, and shutdown
100
53
  await server.stop();
101
54
  ```
102
55
 
103
- #### Registration Handlers
104
-
105
- ```typescript
106
- // Called when a worker successfully registers
107
- const unsubscribeConnected = server.onWorkerConnected((worker) => {
108
- console.log(`Worker connected: ${worker.name}`);
109
- });
56
+ #### Lifecycle Events
110
57
 
111
- // Called when a worker disconnects
112
- const unsubscribeDisconnected = server.onWorkerDisconnected((worker, pending) => {
113
- console.log(`Worker disconnected with ${pending.size} pending requests`);
114
- });
115
- ```
116
-
117
- #### Message Handling
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 |
118
62
 
119
63
  ```typescript
120
- // Register handlers for domain-specific messages by type
121
- server.onWorkerMessage("work_request", (worker, message) => {
122
- // Process work request from worker
64
+ server.onWorkerConnected((worker) => {
65
+ console.log(`Connected: ${worker.id} (${worker.name})`);
123
66
  });
124
67
 
125
- server.onWorkerMessage("status_update", (worker, message) => {
126
- // Handle status updates from worker
68
+ server.onWorkerDisconnected((worker, pendingRequestIds) => {
69
+ console.log(`Disconnected: ${worker.id} with ${pendingRequestIds.size} pending requests`);
127
70
  });
128
71
  ```
129
72
 
130
- #### Sending Messages
73
+ #### Message Routing
74
+
75
+ Register handlers for message types sent by workers:
131
76
 
132
77
  ```typescript
133
- // Send to a specific worker
134
- const success = server.send("worker-1", { type: "stop", reason: "shutdown" });
78
+ server.onWorkerMessage("work_complete", (worker, message) => {
79
+ const { requestId, result } = message;
80
+ console.log(`Worker ${worker.id} completed ${requestId}`);
81
+ });
135
82
 
136
- // Broadcast to all connected workers
137
- server.broadcast({ type: "maintenance_start" });
83
+ // Send messages to workers
84
+ const success = server.send(workerId, { type: "work_request", requestId: "req-1" });
85
+ server.broadcast({ type: "shutdown" });
138
86
  ```
139
87
 
140
- #### Pool Queries
88
+ #### Worker Selection & Pool Queries
141
89
 
142
90
  ```typescript
143
- // Get least-loaded worker supporting a specific model
144
- const worker = server.getAvailableWorker("sonnet-3.5");
91
+ // Get least-loaded worker supporting a model
92
+ const worker = server.getAvailableWorker("sonnet");
145
93
 
146
94
  // Get any available worker (model-agnostic)
147
- const anyWorker = server.getAnyAvailableWorker();
95
+ const any = server.getAnyAvailableWorker();
96
+
97
+ // Slot counts
98
+ console.log("Available slots:", server.getAvailableSlotCount("sonnet", "local"));
148
99
 
149
- // Get all worker info
150
- const workers = server.getWorkerInfo(); // Returns WorkerInfo[]
100
+ // Worker info
101
+ for (const info of server.getWorkerInfo()) {
102
+ console.log(`${info.name}: ${info.status} (${info.activeRequests}/${info.capabilities.maxConcurrentRequests})`);
103
+ }
151
104
  ```
152
105
 
153
106
  #### Request Tracking
154
107
 
108
+ Track and release requests for accurate availability:
109
+
155
110
  ```typescript
156
- // Track a request assigned to a worker
157
- server.trackRequest("worker-1", "req-123", "inference");
111
+ // When assigning a request to a worker
112
+ server.trackRequest(workerId, requestId, "local");
158
113
 
159
- // Release a completed request
160
- server.releaseRequest("req-123", { incrementCompleted: true });
114
+ // When the request completes
115
+ server.releaseRequest(requestId, { incrementCompleted: true });
161
116
  ```
162
117
 
163
118
  #### Extensibility
164
119
 
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 |
169
-
170
- #### Event Handlers
171
-
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 |
177
-
178
- ### WorkerPool
179
-
180
- Internal class managing worker state and selection. Exposed via `WorkerServer`.
181
-
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 |
193
-
194
- ### ConnectionHandler
195
-
196
- Handles WebSocket connection lifecycle and protocol message routing.
197
-
198
- #### Message Routing
120
+ Add HTTP endpoints and custom WebSocket paths:
199
121
 
200
122
  ```typescript
201
- import { ConnectionHandler } from "@hardlydifficult/worker-server";
202
-
203
- const handler = new ConnectionHandler(pool, config, logger);
123
+ // HTTP handler
124
+ server.addHttpHandler(async (req, res) => {
125
+ if (req.url === "/health") {
126
+ res.writeHead(200, { "Content-Type": "application/json" });
127
+ res.end(JSON.stringify({ ok: true }));
128
+ return true;
129
+ }
130
+ return false;
131
+ });
204
132
 
205
- // Register handlers for custom message types
206
- const unregister = handler.onMessage("custom_type", (worker, message) => {
207
- console.log(`Received from ${worker.id}:`, message);
133
+ // Custom WebSocket endpoint
134
+ server.addWebSocketEndpoint("/ws/dashboard", (ws) => {
135
+ ws.send(JSON.stringify({ type: "hello" }));
208
136
  });
209
137
  ```
210
138
 
211
- #### Event Handlers
139
+ ### WorkerPool
140
+
141
+ Low-level pool manager for worker state and selection.
212
142
 
213
143
  ```typescript
214
- handler.onWorkerConnected((worker) => {
215
- console.log("Worker connected:", worker.id);
216
- });
144
+ import { WorkerPool, WorkerStatus } from "@hardlydifficult/worker-server";
217
145
 
218
- handler.onWorkerDisconnected((worker, pending) => {
219
- console.log("Worker disconnected with pending:", pending.size);
220
- });
146
+ const pool = new WorkerPool(logger);
147
+
148
+ pool.add(worker);
149
+ pool.remove(workerId);
150
+ const worker = pool.get(workerId);
221
151
  ```
222
152
 
223
- ## Advanced Features
153
+ #### Selection Logic
224
154
 
225
- ### HTTP Endpoints
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
226
158
 
227
- Custom HTTP handlers can be added:
159
+ #### Request Management
228
160
 
229
- ```typescript
230
- server.addHttpHandler(async (req, res) => {
231
- if (req.url === "/health") {
232
- res.writeHead(200, { "Content-Type": "application/json" });
233
- res.end(JSON.stringify({ status: "ok" }));
234
- return true;
235
- }
236
- return false; // continue to next handler
237
- });
238
- ```
161
+ | Method | Description |
162
+ |--------|-------------|
163
+ | `trackRequest(workerId, requestId, category?)` | Marks request as in-flight and updates status |
164
+ | `releaseRequest(requestId, options?)` | Decrements active count, optionally increments completed count |
239
165
 
240
- ### Custom WebSocket Endpoints
166
+ #### Health Monitoring
241
167
 
242
- Additional WebSocket paths can be handled:
168
+ | Method | Description |
169
+ |--------|-------------|
170
+ | `checkHealth(timeoutMs)` | Returns IDs of workers exceeding `3x` timeout; marks unhealthy ones |
243
171
 
244
- ```typescript
245
- server.addWebSocketEndpoint("/ws/admin", (ws) => {
246
- ws.on("message", (data) => {
247
- // Handle admin WebSocket messages
248
- });
249
- });
250
- ```
172
+ ### ConnectionHandler
251
173
 
252
- ### Authentication
174
+ Handles WebSocket lifecycle, registration, heartbeats, and message routing. Most consumers use `WorkerServer`, which encapsulates this.
253
175
 
254
- Optionally require authentication tokens from workers:
176
+ ### Types & Interfaces
255
177
 
256
- ```typescript
257
- const server = new WorkerServer({
258
- port: 8080,
259
- authToken: "your-secret-token"
260
- });
178
+ #### `WorkerStatus`
261
179
 
262
- // Workers must send registration with matching authToken
263
- ```
180
+ | Value | Description |
181
+ |-------|-------------|
182
+ | `available` | Worker can accept new requests |
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 |
264
186
 
265
- ### Load Balancing with Category Limits
187
+ #### `WorkerInfo`
266
188
 
267
- Workers can declare per-category concurrency limits:
189
+ Public worker metadata (excludes raw WebSocket):
268
190
 
269
191
  ```typescript
270
- const capabilities = {
271
- models: [{ modelId: "sonnet", ... }],
272
- maxConcurrentRequests: 10,
273
- concurrencyLimits: {
274
- inference: 5, // max 5 concurrent inference requests
275
- embeddings: 2 // max 2 concurrent embedding requests
276
- }
277
- };
278
- ```
279
-
280
- Requests are then tracked by category:
281
-
282
- ```typescript
283
- server.trackRequest("worker-1", "req-1", "inference");
284
- server.releaseRequest("req-1"); // category looked up automatically
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>;
204
+ }
285
205
  ```
286
206
 
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
207
+ #### `WorkerCapabilities`
334
208
 
335
209
  ```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;
210
+ interface WorkerCapabilities {
211
+ models: ModelInfo[];
212
+ maxConcurrentRequests: number;
213
+ metadata?: Record<string, unknown>;
214
+ concurrencyLimits?: Record<string, number>;
343
215
  }
344
216
  ```
345
217
 
346
- ### Logger Interface
218
+ #### `ModelInfo`
347
219
 
348
220
  ```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;
221
+ interface ModelInfo {
222
+ modelId: string;
223
+ displayName: string;
224
+ maxContextTokens: number;
225
+ maxOutputTokens: number;
226
+ supportsStreaming: boolean;
227
+ supportsVision?: boolean;
228
+ supportsTools?: boolean;
354
229
  }
355
230
  ```
356
231
 
357
- ## Appendix
232
+ ### Secure Authentication
358
233
 
359
- ### Protocol Messages
360
-
361
- **Worker Registration (worker → server)**
234
+ Authentication tokens are compared using timing-safe comparison to prevent brute-force attacks:
362
235
 
363
236
  ```typescript
364
- {
365
- type: "worker_registration",
366
- workerId: string,
367
- workerName: string,
368
- capabilities: WorkerCapabilities,
369
- authToken?: string
370
- }
237
+ const server = new WorkerServer({
238
+ port: 19100,
239
+ authToken: "secret-token",
240
+ });
371
241
  ```
372
242
 
373
- **Registration Acknowledgment (server → worker)**
243
+ Workers must send:
374
244
 
375
- ```typescript
245
+ ```json
376
246
  {
377
- type: "worker_registration_ack",
378
- success: boolean,
379
- error?: string,
380
- sessionId?: string,
381
- heartbeatIntervalMs?: number
247
+ "type": "worker_registration",
248
+ "workerId": "worker-1",
249
+ "workerName": "My Worker",
250
+ "capabilities": { ... },
251
+ "authToken": "secret-token"
382
252
  }
383
253
  ```
384
254
 
385
- **Heartbeat (worker → server)**
255
+ ### Heartbeat Protocol
386
256
 
387
- ```typescript
257
+ Workers must send periodic heartbeat messages:
258
+
259
+ ```json
388
260
  {
389
- type: "heartbeat",
390
- workerId: string,
391
- timestamp: string
261
+ "type": "heartbeat",
262
+ "workerId": "worker-1",
263
+ "timestamp": "2024-01-01T00:00:00.000Z"
392
264
  }
393
265
  ```
394
266
 
395
- **Heartbeat Acknowledgment (server worker)**
267
+ The server responds with:
396
268
 
397
- ```typescript
269
+ ```json
398
270
  {
399
- type: "heartbeat_ack",
400
- timestamp: string,
401
- nextHeartbeatDeadline: string
271
+ "type": "heartbeat_ack",
272
+ "timestamp": "2024-01-01T00:00:00.000Z",
273
+ "nextHeartbeatDeadline": "2024-01-01T00:01:15.000Z"
402
274
  }
403
- ```
275
+ ```
276
+
277
+ A worker is considered unhealthy if its heartbeat exceeds `heartbeatTimeoutMs`. It is marked dead and disconnected after `3x` the timeout.
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.11",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [