@hardlydifficult/worker-server 1.0.5 → 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 +206 -136
  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 that manages remote worker connections, health monitoring, request routing, and load balancing through a clean TypeScript interface.
3
+ WebSocket-based worker server with health monitoring, request routing, and load balancing.
4
4
 
5
5
  ## Installation
6
6
 
@@ -13,197 +13,209 @@ 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
- heartbeatTimeoutMs: 60000, // 60 seconds
19
- healthCheckIntervalMs: 10000, // 10 seconds
20
- });
16
+ const server = new WorkerServer({ port: 8080 });
21
17
 
22
18
  server.onWorkerConnected((worker) => {
23
- console.log(`Worker connected: ${worker.name} (${worker.id})`);
19
+ console.log(`Worker ${worker.name} connected (${worker.id})`);
24
20
  });
25
21
 
26
- server.onWorkerDisconnected((worker, pendingRequests) => {
27
- console.log(`Worker disconnected: ${worker.name} (${worker.id})`);
28
- if (pendingRequests.size > 0) {
29
- console.log(`Rescheduling ${pendingRequests.size} pending requests`);
30
- }
22
+ server.onWorkerDisconnected((worker, pending) => {
23
+ console.log(`Worker ${worker.name} disconnected with ${pending.size} pending requests`);
31
24
  });
32
25
 
33
26
  server.onWorkerMessage("work_complete", (worker, message) => {
34
- console.log(`Work complete: ${message.requestId}`);
35
- });
36
-
37
- server.addHttpHandler(async (req, res) => {
38
- if (req.url === "/health") {
39
- res.writeHead(200, { "Content-Type": "application/json" });
40
- res.end(JSON.stringify({ status: "ok" }));
41
- return true;
42
- }
43
- return false;
27
+ console.log(`Worker ${worker.id} completed request ${message.requestId}`);
44
28
  });
45
29
 
46
30
  await server.start();
47
- console.log("Worker server running on port 8080");
31
+ console.log("Server listening on port 8080");
48
32
  ```
49
33
 
50
- ## Core Concepts
34
+ ## Core Components
51
35
 
52
- ### Worker Management
53
-
54
- Workers connect via WebSocket, register with capabilities (supported models, concurrency limits), and send periodic heartbeats. The server tracks their status (available, busy, draining, unhealthy) and makes routing decisions based on load and model compatibility.
55
-
56
- ### Request Tracking
57
-
58
- Requests are tracked per-worker and can be released when complete. Optional categories enable per-category concurrency limits when workers declare `concurrencyLimits` in their capabilities.
36
+ ### WorkerServer
59
37
 
60
- ### Load Balancing
38
+ WebSocket server managing remote worker connections with health checks, message routing, and pool management.
61
39
 
62
- Workers are selected based on:
63
- - Model support (exact or substring match)
64
- - Current load (least-loaded algorithm)
65
- - Category-specific limits (when `category` is provided)
40
+ #### Lifecycle Management
66
41
 
67
- ## API Reference
42
+ ```typescript
43
+ const server = new WorkerServer({ port: 8080, authToken: "secret" });
68
44
 
69
- ### WorkerServer
45
+ // Start the server
46
+ await server.start();
70
47
 
71
- Main entry point for managing worker connections.
48
+ // Stop the server gracefully
49
+ await server.stop();
50
+ ```
72
51
 
73
- #### Constructor
52
+ #### Registration Handlers
74
53
 
75
54
  ```typescript
76
- constructor(options: WorkerServerOptions)
77
- ```
55
+ // Called when a worker successfully registers
56
+ const unsubscribeConnected = server.onWorkerConnected((worker) => {
57
+ console.log(`Worker connected: ${worker.name}`);
58
+ });
78
59
 
79
- | Option | Type | Default | Description |
80
- |--------|------|---------|-------------|
81
- | `port` | `number` | | HTTP + WebSocket server port |
82
- | `authToken` | `string` | `undefined` | Optional token required for worker registration |
83
- | `heartbeatTimeoutMs` | `number` | `60000` | Milliseconds before a missed heartbeat marks a worker unhealthy |
84
- | `healthCheckIntervalMs` | `number` | `10000` | Interval (ms) for health checks |
85
- | `heartbeatIntervalMs` | `number` | `15000` | Heartbeat interval communicated to workers |
86
- | `logger` | `WorkerServerLogger` | `undefined` | Logger instance (defaults to no-op) |
60
+ // Called when a worker disconnects
61
+ const unsubscribeDisconnected = server.onWorkerDisconnected((worker, pending) => {
62
+ console.log(`Worker disconnected with ${pending.size} pending requests`);
63
+ });
64
+ ```
87
65
 
88
- #### Lifecycle Events
66
+ #### Message Handling
89
67
 
90
68
  ```typescript
91
- // Called when a worker successfully registers
92
- onWorkerConnected(handler: WorkerConnectedHandler): () => void;
93
-
94
- // Called when a worker disconnects with pending request IDs
95
- onWorkerDisconnected(handler: WorkerDisconnectedHandler): () => void;
69
+ // Register handlers for domain-specific messages by type
70
+ server.onWorkerMessage("work_request", (worker, message) => {
71
+ // Process work request from worker
72
+ });
96
73
 
97
- // Register a handler for a specific message type (dispatched by 'type' field)
98
- onWorkerMessage<T = Record<string, unknown>>(
99
- type: string,
100
- handler: WorkerMessageHandler<T>
101
- ): () => void;
74
+ server.onWorkerMessage("status_update", (worker, message) => {
75
+ // Handle status updates from worker
76
+ });
102
77
  ```
103
78
 
104
- #### Message Operations
79
+ #### Sending Messages
105
80
 
106
81
  ```typescript
107
- // Send a JSON message to a specific worker (false if failed)
108
- send(workerId: string, message: Record<string, unknown>): boolean;
82
+ // Send to a specific worker
83
+ const success = server.send("worker-1", { type: "stop", reason: "shutdown" });
109
84
 
110
85
  // Broadcast to all connected workers
111
- broadcast(message: Record<string, unknown>): void;
86
+ server.broadcast({ type: "maintenance_start" });
112
87
  ```
113
88
 
114
89
  #### Pool Queries
115
90
 
116
91
  ```typescript
117
- // Get least-loaded worker supporting the given model
118
- getAvailableWorker(model: string, category?: string): WorkerInfo | null;
92
+ // Get least-loaded worker supporting a specific model
93
+ const worker = server.getAvailableWorker("sonnet-3.5");
119
94
 
120
95
  // Get any available worker (model-agnostic)
121
- getAnyAvailableWorker(): WorkerInfo | null;
96
+ const anyWorker = server.getAnyAvailableWorker();
97
+
98
+ // Get all worker info
99
+ const workers = server.getWorkerInfo(); // Returns WorkerInfo[]
100
+ ```
122
101
 
123
- // Total connected worker count
124
- getWorkerCount(): number;
102
+ #### Request Tracking
125
103
 
126
- // Available worker count
127
- getAvailableWorkerCount(): number;
104
+ ```typescript
105
+ // Track a request assigned to a worker
106
+ server.trackRequest("worker-1", "req-123", "inference");
128
107
 
129
- // Get info about all connected workers
130
- getWorkerInfo(): WorkerInfo[];
108
+ // Release a completed request
109
+ server.releaseRequest("req-123", { incrementCompleted: true });
131
110
  ```
132
111
 
133
- #### Request Tracking
112
+ ### WorkerPool
113
+
114
+ Manages worker lifecycle, request tracking, health checks, and message routing.
115
+
116
+ #### Worker Status
117
+
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
123
+
124
+ #### Pool Operations
134
125
 
135
126
  ```typescript
136
- // Track a request assigned to a worker (optional category)
137
- trackRequest(workerId: string, requestId: string, category?: string): void;
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();
138
140
 
139
- // Release a tracked request (optionally increment completed count)
140
- releaseRequest(
141
- requestId: string,
142
- options?: { incrementCompleted?: boolean }
143
- ): void;
141
+ // Get worker info (without WebSocket reference)
142
+ const workers = pool.getWorkerInfoList();
143
+
144
+ // Track and release requests
145
+ pool.trackRequest("worker-1", "req-123");
146
+ pool.releaseRequest("req-123");
147
+
148
+ // Check for unhealthy workers
149
+ const deadWorkerIds = pool.checkHealth(60_000); // 60-second timeout
150
+
151
+ // Send and broadcast messages
152
+ pool.send("worker-1", { type: "ping" });
153
+ pool.broadcast({ type: "shutdown" });
144
154
  ```
145
155
 
146
- #### HTTP & WebSocket Extensibility
156
+ ### ConnectionHandler
157
+
158
+ Handles WebSocket connection lifecycle and protocol message routing.
159
+
160
+ #### Message Routing
147
161
 
148
162
  ```typescript
149
- // Add an HTTP handler (called in order until one returns true)
150
- addHttpHandler(handler: HttpRequestHandler): void;
163
+ import { ConnectionHandler } from "@hardlydifficult/worker-server";
151
164
 
152
- // Add an additional WebSocket endpoint at a path
153
- addWebSocketEndpoint(
154
- path: string,
155
- handler: WebSocketConnectionHandler
156
- ): void;
165
+ const handler = new ConnectionHandler(pool, config, logger);
166
+
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
+ });
157
171
  ```
158
172
 
159
- #### Server Lifecycle
173
+ #### Event Handlers
160
174
 
161
175
  ```typescript
162
- // Start the HTTP + WebSocket server
163
- start(): Promise<void>;
176
+ handler.onWorkerConnected((worker) => {
177
+ console.log("Worker connected:", worker.id);
178
+ });
164
179
 
165
- // Stop the server and close all connections
166
- stop(): Promise<void>;
180
+ handler.onWorkerDisconnected((worker, pending) => {
181
+ console.log("Worker disconnected with pending:", pending.size);
182
+ });
167
183
  ```
168
184
 
185
+ ## Types and Interfaces
186
+
169
187
  ### WorkerInfo
170
188
 
171
- Public interface representing a connected worker:
189
+ Public worker metadata exposed to consumers:
172
190
 
173
191
  ```typescript
174
192
  interface WorkerInfo {
175
- readonly id: string;
176
- readonly name: string;
177
- readonly status: WorkerStatus; // "available" | "busy" | "draining" | "unhealthy"
178
- readonly capabilities: WorkerCapabilities;
179
- readonly sessionId: string;
180
- readonly connectedAt: Date;
181
- readonly lastHeartbeat: Date;
182
- readonly activeRequests: number;
183
- readonly completedRequests: number;
184
- readonly pendingRequestIds: ReadonlySet<string>;
185
- readonly categoryActiveRequests: ReadonlyMap<string, number>;
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>;
186
204
  }
187
205
  ```
188
206
 
189
207
  ### WorkerCapabilities
190
208
 
191
- Describes a worker's capabilities:
209
+ Describes what a worker can do:
192
210
 
193
211
  ```typescript
194
212
  interface WorkerCapabilities {
195
213
  models: ModelInfo[];
196
214
  maxConcurrentRequests: number;
197
215
  metadata?: Record<string, unknown>;
198
- concurrencyLimits?: Record<string, number>;
216
+ concurrencyLimits?: Record<string, number>; // per-category limits
199
217
  }
200
- ```
201
218
 
202
- ### ModelInfo
203
-
204
- Describes a supported model:
205
-
206
- ```typescript
207
219
  interface ModelInfo {
208
220
  modelId: string;
209
221
  displayName: string;
@@ -215,32 +227,90 @@ interface ModelInfo {
215
227
  }
216
228
  ```
217
229
 
218
- ### WorkerStatus
230
+ ### Configuration Options
219
231
 
220
- Worker state enumeration:
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
+ ```
221
242
 
222
- | Status | Description |
223
- |--------|-------------|
224
- | `available` | Worker is idle and can accept new requests |
225
- | `busy` | Worker is at max concurrent requests but may accept model-agnostic tasks |
226
- | `draining` | Worker is being gracefully decommissioned |
227
- | `unhealthy` | Worker has missed heartbeats and is presumed degraded |
243
+ ### Logger Interface
228
244
 
229
- ## Worker Protocol
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
+ ```
230
253
 
231
- Workers communicate using JSON messages with a `type` field:
254
+ ## Advanced Features
232
255
 
233
- | Message | Direction | Description |
234
- |---------|-----------|-------------|
235
- | `worker_registration` | Worker → Server | Register with capabilities |
236
- | `worker_registration_ack` | Server → Worker | Acknowledgment with session ID |
237
- | `heartbeat` | Worker → Server | Periodic health check |
238
- | `heartbeat_ack` | Server → Worker | Acknowledgment with next deadline |
256
+ ### HTTP Endpoints
239
257
 
240
- All other message types are routed to registered handlers via `onWorkerMessage()`.
258
+ Custom HTTP handlers can be added:
241
259
 
242
- ## Health Monitoring
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
+ });
269
+ ```
243
270
 
244
- - Workers missing heartbeats for `heartbeatTimeoutMs` are marked `unhealthy`
245
- - Workers missing heartbeats for `3 × heartbeatTimeoutMs` are disconnected
246
- - Health checks run every `healthCheckIntervalMs`
271
+ ### Custom WebSocket Endpoints
272
+
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
+ });
281
+ ```
282
+
283
+ ### Authentication
284
+
285
+ Optionally require authentication tokens from workers:
286
+
287
+ ```typescript
288
+ const server = new WorkerServer({
289
+ port: 8080,
290
+ authToken: "your-secret-token"
291
+ });
292
+
293
+ // Workers must send registration with matching authToken
294
+ ```
295
+
296
+ ### Load Balancing with Category Limits
297
+
298
+ Workers can declare per-category concurrency limits:
299
+
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:
312
+
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.5",
3
+ "version": "1.0.7",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [