@hardlydifficult/worker-server 1.0.6 → 1.0.7

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 +193 -287
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -13,398 +13,304 @@ npm install @hardlydifficult/worker-server
13
13
  ```typescript
14
14
  import { WorkerServer } from "@hardlydifficult/worker-server";
15
15
 
16
- const server = new WorkerServer({
17
- port: 8080,
18
- authToken: "my-secret-token", // optional
19
- logger: console, // optional, defaults to no-op
20
- });
16
+ const server = new WorkerServer({ port: 8080 });
21
17
 
22
- // Handle worker registrations and messages
23
18
  server.onWorkerConnected((worker) => {
24
- console.log(`Worker ${worker.name} (${worker.id}) connected`);
19
+ console.log(`Worker ${worker.name} connected (${worker.id})`);
25
20
  });
26
21
 
27
- server.onWorkerDisconnected((worker, pendingRequestIds) => {
28
- console.log(`Worker ${worker.id} disconnected with ${pendingRequestIds.size} pending requests`);
22
+ server.onWorkerDisconnected((worker, pending) => {
23
+ console.log(`Worker ${worker.name} disconnected with ${pending.size} pending requests`);
29
24
  });
30
25
 
31
26
  server.onWorkerMessage("work_complete", (worker, message) => {
32
- console.log(`Work completed: ${message.requestId}`);
27
+ console.log(`Worker ${worker.id} completed request ${message.requestId}`);
33
28
  });
34
29
 
35
- // Start the server
36
30
  await server.start();
37
-
38
- // Get an available worker supporting a model
39
- const worker = server.getAvailableWorker("gpt-4");
40
- if (worker) {
41
- server.send(worker.id, { type: "work_request", requestId: "req-1" });
42
- }
43
-
44
- // Stop when done
45
- await server.stop();
31
+ console.log("Server listening on port 8080");
46
32
  ```
47
33
 
48
- ## Core Concepts
49
-
50
- ### Worker Lifecycle Management
34
+ ## Core Components
51
35
 
52
- The server handles worker connections, registrations, and disconnections with automatic health monitoring.
36
+ ### WorkerServer
53
37
 
54
- #### Registration and Authentication
38
+ WebSocket server managing remote worker connections with health checks, message routing, and pool management.
55
39
 
56
- Workers connect via WebSocket and send a `worker_registration` message with an optional `authToken`. If the server is configured with `authToken`, the worker must provide a matching token.
40
+ #### Lifecycle Management
57
41
 
58
42
  ```typescript
59
- import { WorkerServer } from "@hardlydifficult/worker-server";
60
-
61
- const server = new WorkerServer({
62
- port: 8080,
63
- authToken: "secret",
64
- });
65
-
66
- server.onWorkerConnected((worker) => {
67
- console.log(`Worker registered: ${worker.id}`);
68
- });
43
+ const server = new WorkerServer({ port: 8080, authToken: "secret" });
69
44
 
45
+ // Start the server
70
46
  await server.start();
71
- ```
72
47
 
73
- #### Heartbeat and Health Monitoring
48
+ // Stop the server gracefully
49
+ await server.stop();
50
+ ```
74
51
 
75
- Workers must send periodic `heartbeat` messages. The server tracks the last heartbeat timestamp and closes connections that miss the timeout.
52
+ #### Registration Handlers
76
53
 
77
54
  ```typescript
78
- const server = new WorkerServer({
79
- port: 8080,
80
- heartbeatTimeoutMs: 60_000, // Missed for 60s → unhealthy
81
- healthCheckIntervalMs: 10_000, // Check every 10s
82
- heartbeatIntervalMs: 15_000, // Communicate 15s interval to workers
55
+ // Called when a worker successfully registers
56
+ const unsubscribeConnected = server.onWorkerConnected((worker) => {
57
+ console.log(`Worker connected: ${worker.name}`);
83
58
  });
84
- ```
85
-
86
- | Option | Default | Description |
87
- |--------|---------|-------------|
88
- | `heartbeatTimeoutMs` | 60000 | Time before worker is marked unhealthy |
89
- | `healthCheckIntervalMs` | 10000 | Frequency of health checks |
90
- | `heartbeatIntervalMs` | 15000 | Heartbeat interval communicated to workers |
91
-
92
- #### Disconnection Handling
93
59
 
94
- When a worker disconnects, the `onWorkerDisconnected` handler is called with its pending request IDs.
95
-
96
- ```typescript
97
- server.onWorkerDisconnected((worker, pendingRequestIds) => {
98
- // Reassign pending requests as needed
99
- console.log(`Pending: ${[...pendingRequestIds].join(", ")}`);
60
+ // Called when a worker disconnects
61
+ const unsubscribeDisconnected = server.onWorkerDisconnected((worker, pending) => {
62
+ console.log(`Worker disconnected with ${pending.size} pending requests`);
100
63
  });
101
64
  ```
102
65
 
103
- ### Request Tracking and Load Balancing
66
+ #### Message Handling
104
67
 
105
- Requests are tracked per-worker to avoid overloading and to support category-specific limits.
68
+ ```typescript
69
+ // Register handlers for domain-specific messages by type
70
+ server.onWorkerMessage("work_request", (worker, message) => {
71
+ // Process work request from worker
72
+ });
106
73
 
107
- #### Request Lifecycle
74
+ server.onWorkerMessage("status_update", (worker, message) => {
75
+ // Handle status updates from worker
76
+ });
77
+ ```
108
78
 
109
- Track a request as assigned to a worker, then release it when complete.
79
+ #### Sending Messages
110
80
 
111
81
  ```typescript
112
- // Assign request to worker
113
- const worker = server.getAvailableWorker("gpt-4");
114
- if (worker) {
115
- server.trackRequest(worker.id, "req-1");
116
- server.send(worker.id, { type: "work_request", requestId: "req-1" });
117
- }
82
+ // Send to a specific worker
83
+ const success = server.send("worker-1", { type: "stop", reason: "shutdown" });
118
84
 
119
- // Worker sends completion message
120
- server.onWorkerMessage("work_complete", (worker, message) => {
121
- server.releaseRequest(message.requestId, { incrementCompleted: true });
122
- });
85
+ // Broadcast to all connected workers
86
+ server.broadcast({ type: "maintenance_start" });
123
87
  ```
124
88
 
125
- #### Available Worker Selection
126
-
127
- Workers are selected based on capacity and model support.
89
+ #### Pool Queries
128
90
 
129
91
  ```typescript
130
- // Get least-loaded worker supporting a model
131
- const worker = server.getAvailableWorker("gpt-4");
132
- // → least-loaded worker that supports "gpt-4"
92
+ // Get least-loaded worker supporting a specific model
93
+ const worker = server.getAvailableWorker("sonnet-3.5");
133
94
 
134
95
  // Get any available worker (model-agnostic)
135
96
  const anyWorker = server.getAnyAvailableWorker();
136
- // → any worker (Available or Busy status)
137
- ```
138
-
139
- Workers are marked `Busy` when `activeRequests >= maxConcurrentRequests`.
140
97
 
141
- #### Per-Category Concurrency Limits
98
+ // Get all worker info
99
+ const workers = server.getWorkerInfo(); // Returns WorkerInfo[]
100
+ ```
142
101
 
143
- Workers can define per-category limits in their capabilities. The pool enforces these when `trackRequest` is called with a category.
102
+ #### Request Tracking
144
103
 
145
104
  ```typescript
146
- // Worker capabilities include:
147
- {
148
- models: [{ modelId: "gpt-4", ... }],
149
- maxConcurrentRequests: 5,
150
- concurrencyLimits: {
151
- inference: 2,
152
- embedding: 4,
153
- }
154
- }
105
+ // Track a request assigned to a worker
106
+ server.trackRequest("worker-1", "req-123", "inference");
155
107
 
156
- // Track request in a category
157
- server.trackRequest(worker.id, "req-1", "inference");
108
+ // Release a completed request
109
+ server.releaseRequest("req-123", { incrementCompleted: true });
158
110
  ```
159
111
 
160
- ### Message Routing
112
+ ### WorkerPool
161
113
 
162
- Messages are routed by the `type` field to registered handlers.
114
+ Manages worker lifecycle, request tracking, health checks, and message routing.
163
115
 
164
- ```typescript
165
- server.onWorkerMessage("work_complete", (worker, message) => {
166
- console.log(`Worker ${worker.id} completed ${message.requestId}`);
167
- });
116
+ #### Worker Status
168
117
 
169
- server.onWorkerMessage("metrics", (worker, message) => {
170
- console.log(`Worker ${worker.id} metrics:`, message.metrics);
171
- });
172
- ```
118
+ Workers transition automatically between states:
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
173
123
 
174
- Returns an unsubscribe function:
124
+ #### Pool Operations
175
125
 
176
126
  ```typescript
177
- const unsubscribe = server.onWorkerMessage("status", handler);
178
- // later...
179
- unsubscribe();
180
- ```
181
-
182
- ### Sending Messages
127
+ import { WorkerPool, WorkerStatus, type ConnectedWorker } from "@hardlydifficult/worker-server";
183
128
 
184
- #### Targeted Send
129
+ const pool = new WorkerPool();
185
130
 
186
- ```typescript
187
- const success = server.send(workerId, { type: "ping" });
188
- // Returns false if worker not found or WebSocket not open
189
- ```
131
+ // Get the least-loaded available worker supporting a model
132
+ const worker = pool.getAvailableWorker("sonnet-3.5", "inference");
190
133
 
191
- #### Broadcast
134
+ // Get any available or busy worker
135
+ const anyWorker = pool.getAnyAvailableWorker();
192
136
 
193
- ```typescript
194
- server.broadcast({ type: "shutdown" });
195
- // Sends to all connected workers with open sockets
196
- ```
137
+ // Get count statistics
138
+ const total = pool.getCount();
139
+ const available = pool.getAvailableCount();
197
140
 
198
- ### Server Extensibility
141
+ // Get worker info (without WebSocket reference)
142
+ const workers = pool.getWorkerInfoList();
199
143
 
200
- #### Additional WebSocket Endpoints
144
+ // Track and release requests
145
+ pool.trackRequest("worker-1", "req-123");
146
+ pool.releaseRequest("req-123");
201
147
 
202
- ```typescript
203
- // Create a custom WebSocket endpoint
204
- server.addWebSocketEndpoint("/ws/metrics", (ws) => {
205
- ws.on("message", (data) => {
206
- console.log("Metrics client message:", data.toString());
207
- });
208
- });
148
+ // Check for unhealthy workers
149
+ const deadWorkerIds = pool.checkHealth(60_000); // 60-second timeout
209
150
 
210
- // Clients connect to ws://localhost:8080/ws/metrics
151
+ // Send and broadcast messages
152
+ pool.send("worker-1", { type: "ping" });
153
+ pool.broadcast({ type: "shutdown" });
211
154
  ```
212
155
 
213
- #### HTTP Handlers
156
+ ### ConnectionHandler
157
+
158
+ Handles WebSocket connection lifecycle and protocol message routing.
159
+
160
+ #### Message Routing
214
161
 
215
162
  ```typescript
216
- server.addHttpHandler(async (req, res) => {
217
- if (req.url === "/health") {
218
- res.writeHead(200, { "Content-Type": "application/json" });
219
- res.end(JSON.stringify({ status: "ok" }));
220
- return true;
221
- }
222
- return false; // continue to next handler or 404
223
- });
163
+ import { ConnectionHandler } from "@hardlydifficult/worker-server";
164
+
165
+ const handler = new ConnectionHandler(pool, config, logger);
224
166
 
225
- // Custom HTTP responses take precedence over 404
167
+ // Register handlers for custom message types
168
+ const unregister = handler.onMessage("custom_type", (worker, message) => {
169
+ console.log(`Received from ${worker.id}:`, message);
170
+ });
226
171
  ```
227
172
 
228
- ## Public API
229
-
230
- ### `WorkerServer`
231
-
232
- Main server class for managing worker connections.
233
-
234
- | Method | Description |
235
- |--------|-------------|
236
- | `onWorkerConnected(handler)` | Register handler for worker registration events |
237
- | `onWorkerDisconnected(handler)` | Register handler for worker disconnection events |
238
- | `onWorkerMessage(type, handler)` | Register handler for a specific message type |
239
- | `send(workerId, message)` | Send a JSON message to a specific worker |
240
- | `broadcast(message)` | Broadcast a JSON message to all workers |
241
- | `getAvailableWorker(model, category?)` | Get least-loaded worker supporting model |
242
- | `getAnyAvailableWorker()` | Get any available/Busy worker |
243
- | `getWorkerCount()` | Total connected worker count |
244
- | `getAvailableWorkerCount()` | Available worker count |
245
- | `getWorkerInfo()` | Get public info about all workers |
246
- | `trackRequest(workerId, requestId, category?)` | Track request as in-progress |
247
- | `releaseRequest(requestId, options?)` | Release tracked request |
248
- | `addHttpHandler(handler)` | Add HTTP request handler |
249
- | `addWebSocketEndpoint(path, handler)` | Add custom WebSocket endpoint |
250
- | `start()` | Start HTTP + WebSocket server |
251
- | `stop()` | Stop server and close all connections |
252
-
253
- ### `WorkerPool`
254
-
255
- Internal pool manager with public helpers.
256
-
257
- | Method | Description |
258
- |--------|-------------|
259
- | `add(worker)` | Add a connected worker to the pool |
260
- | `remove(id)` | Remove worker by ID |
261
- | `get(id)` | Get worker by ID |
262
- | `has(id)` | Check if worker is in pool |
263
- | `getAvailableWorker(model, category?)` | Get available worker by model |
264
- | `getAnyAvailableWorker()` | Get any available/Busy worker |
265
- | `getCount()` | Total worker count |
266
- | `getAvailableCount()` | Available worker count |
267
- | `getWorkerInfoList()` | Get public info for all workers |
268
- | `checkHealth(timeoutMs)` | Check worker health and return dead IDs |
269
- | `send(workerId, message)` | Send message to worker |
270
- | `broadcast(message)` | Broadcast to all workers |
271
- | `closeAll()` | Close all worker connections |
272
-
273
- ### `toWorkerInfo(worker)`
274
-
275
- Converts internal `ConnectedWorker` to public `WorkerInfo`.
173
+ #### Event Handlers
276
174
 
277
175
  ```typescript
278
- import { toWorkerInfo, type ConnectedWorker } from "@hardlydifficult/worker-server";
176
+ handler.onWorkerConnected((worker) => {
177
+ console.log("Worker connected:", worker.id);
178
+ });
279
179
 
280
- const internal: ConnectedWorker = /* ... */;
281
- const publicInfo = toWorkerInfo(internal);
282
- // No websocket reference in publicInfo
180
+ handler.onWorkerDisconnected((worker, pending) => {
181
+ console.log("Worker disconnected with pending:", pending.size);
182
+ });
283
183
  ```
284
184
 
285
- ### Types
286
-
287
- | Type | Description |
288
- |------|-------------|
289
- | `WorkerStatus` | `available`, `busy`, `draining`, `unhealthy` |
290
- | `ModelInfo` | Model capabilities and metadata |
291
- | `WorkerCapabilities` | Worker capacity, models, and concurrency limits |
292
- | `WorkerInfo` | Public worker state |
293
- | `ConnectedWorker` | Internal state (includes WebSocket) |
294
- | `WorkerServerOptions` | Configuration for `WorkerServer` |
295
- | `WorkerServerLogger` | Logger interface |
296
- | `HttpRequestHandler` | HTTP request handler type |
297
- | `WorkerMessageHandler<T>` | Typed message handler |
298
- | `WorkerConnectedHandler` | Worker connected event handler |
299
- | `WorkerDisconnectedHandler` | Worker disconnected event handler |
300
- | `WebSocketConnectionHandler` | Custom WebSocket endpoint handler |
185
+ ## Types and Interfaces
301
186
 
302
- ### Constants and Defaults
187
+ ### WorkerInfo
303
188
 
304
- Default timeouts (milliseconds):
189
+ Public worker metadata exposed to consumers:
305
190
 
306
191
  ```typescript
307
- {
308
- heartbeatTimeoutMs: 60_000,
309
- healthCheckIntervalMs: 10_000,
310
- heartbeatIntervalMs: 15_000,
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>;
311
204
  }
312
205
  ```
313
206
 
314
- ### Utility Functions
315
-
316
- #### `safeCompare(a, b)`
207
+ ### WorkerCapabilities
317
208
 
318
- Timing-safe string comparison.
209
+ Describes what a worker can do:
319
210
 
320
211
  ```typescript
321
- import { safeCompare } from "@hardlydifficult/worker-server";
212
+ interface WorkerCapabilities {
213
+ models: ModelInfo[];
214
+ maxConcurrentRequests: number;
215
+ metadata?: Record<string, unknown>;
216
+ concurrencyLimits?: Record<string, number>; // per-category limits
217
+ }
322
218
 
323
- const isValid = safeCompare(inputToken, secretToken);
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
+ }
324
228
  ```
325
229
 
326
- ## Appendix
327
-
328
- ### Worker Registration Protocol
329
-
330
- Workers send:
331
-
332
- ```json
333
- {
334
- "type": "worker_registration",
335
- "workerId": "worker-1",
336
- "workerName": "My Worker",
337
- "capabilities": {
338
- "models": [{
339
- "modelId": "gpt-4",
340
- "displayName": "GPT-4",
341
- "maxContextTokens": 8192,
342
- "maxOutputTokens": 4096,
343
- "supportsStreaming": true
344
- }],
345
- "maxConcurrentRequests": 5,
346
- "concurrencyLimits": {
347
- "inference": 2,
348
- "embedding": 4
349
- }
350
- },
351
- "authToken": "optional"
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;
352
240
  }
353
241
  ```
354
242
 
355
- Server responds:
243
+ ### Logger Interface
356
244
 
357
- ```json
358
- {
359
- "type": "worker_registration_ack",
360
- "success": true,
361
- "sessionId": "uuid",
362
- "heartbeatIntervalMs": 15000
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;
363
251
  }
364
252
  ```
365
253
 
366
- ### Worker Heartbeat Protocol
254
+ ## Advanced Features
367
255
 
368
- Workers send:
256
+ ### HTTP Endpoints
369
257
 
370
- ```json
371
- {
372
- "type": "heartbeat",
373
- "workerId": "worker-1",
374
- "timestamp": "2024-01-01T00:00:00.000Z"
375
- }
258
+ Custom HTTP handlers can be added:
259
+
260
+ ```typescript
261
+ server.addHttpHandler(async (req, res) => {
262
+ if (req.url === "/health") {
263
+ res.writeHead(200, { "Content-Type": "application/json" });
264
+ res.end(JSON.stringify({ status: "ok" }));
265
+ return true;
266
+ }
267
+ return false; // continue to next handler
268
+ });
376
269
  ```
377
270
 
378
- Server responds:
271
+ ### Custom WebSocket Endpoints
379
272
 
380
- ```json
381
- {
382
- "type": "heartbeat_ack",
383
- "timestamp": "2024-01-01T00:00:00.000Z",
384
- "nextHeartbeatDeadline": "2024-01-01T00:01:00.000Z"
385
- }
273
+ Additional WebSocket paths can be handled:
274
+
275
+ ```typescript
276
+ server.addWebSocketEndpoint("/ws/admin", (ws) => {
277
+ ws.on("message", (data) => {
278
+ // Handle admin WebSocket messages
279
+ });
280
+ });
386
281
  ```
387
282
 
388
- ### Status Transitions
283
+ ### Authentication
389
284
 
390
- - `Available` `Busy` when `activeRequests >= maxConcurrentRequests`
391
- - `Busy` → `Available` when `activeRequests < maxConcurrentRequests`
392
- - Any → `Unhealthy` on heartbeat timeout
393
- - `Unhealthy` → `Available/Busy` on heartbeat recovery
285
+ Optionally require authentication tokens from workers:
394
286
 
395
- ### Concurrent Request Tracking
287
+ ```typescript
288
+ const server = new WorkerServer({
289
+ port: 8080,
290
+ authToken: "your-secret-token"
291
+ });
396
292
 
397
- The pool tracks requests per-worker and per-category (if provided). It automatically decrements the category count when releasing a tracked request.
293
+ // Workers must send registration with matching authToken
294
+ ```
398
295
 
399
- ### Worker Protocol Summary
296
+ ### Load Balancing with Category Limits
400
297
 
401
- Workers communicate using JSON messages with a `type` field:
298
+ Workers can declare per-category concurrency limits:
402
299
 
403
- | Message | Direction | Description |
404
- |---------|-----------|-------------|
405
- | `worker_registration` | Worker Server | Register with capabilities |
406
- | `worker_registration_ack` | Server → Worker | Acknowledgment with session ID |
407
- | `heartbeat` | Worker → Server | Periodic health check |
408
- | `heartbeat_ack` | Server Worker | Acknowledgment with next deadline |
300
+ ```typescript
301
+ const capabilities = {
302
+ models: [{ modelId: "sonnet", ... }],
303
+ maxConcurrentRequests: 10,
304
+ concurrencyLimits: {
305
+ inference: 5, // max 5 concurrent inference requests
306
+ embeddings: 2 // max 2 concurrent embedding requests
307
+ }
308
+ };
309
+ ```
310
+
311
+ Requests are then tracked by category:
409
312
 
410
- All other message types are routed to registered handlers via `onWorkerMessage()`.
313
+ ```typescript
314
+ server.trackRequest("worker-1", "req-1", "inference");
315
+ server.releaseRequest("req-1"); // category looked up automatically
316
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/worker-server",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [