@hardlydifficult/worker-server 1.0.15 → 1.0.17

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 CHANGED
@@ -22,6 +22,10 @@ server.onWorkerConnected((worker) => {
22
22
  console.log(`Worker ${worker.name} connected`);
23
23
  });
24
24
 
25
+ server.onWorkerDisconnected((worker, pendingRequestIds) => {
26
+ console.log(`Worker ${worker.id} disconnected with ${pendingRequestIds.size} pending requests`);
27
+ });
28
+
25
29
  server.onWorkerMessage("work_complete", (worker, message) => {
26
30
  console.log(`Worker ${worker.id} completed:`, message);
27
31
  });
@@ -36,215 +40,220 @@ if (worker) {
36
40
  }
37
41
  ```
38
42
 
39
- ## Core Concepts
43
+ ## Worker Lifecycle
40
44
 
41
- ### WorkerServer
45
+ ### Registration
42
46
 
43
- Main entry point for managing worker connections via WebSocket.
47
+ Workers register by sending a `worker_registration` message with `workerId`, `workerName`, and `capabilities`. Optionally include an `authToken` if authentication is enabled.
44
48
 
45
49
  ```typescript
46
- import { WorkerServer, WorkerStatus } from "@hardlydifficult/worker-server";
50
+ // Worker-side registration example
51
+ const ws = new WebSocket("ws://localhost:19100/ws");
52
+
53
+ ws.onopen = () => {
54
+ ws.send(JSON.stringify({
55
+ type: "worker_registration",
56
+ workerId: "worker-1",
57
+ workerName: "My Worker",
58
+ capabilities: {
59
+ models: [{
60
+ modelId: "gpt-4",
61
+ displayName: "GPT-4",
62
+ maxContextTokens: 32768,
63
+ maxOutputTokens: 8192,
64
+ supportsStreaming: true
65
+ }],
66
+ maxConcurrentRequests: 4,
67
+ concurrencyLimits: {
68
+ local: 2,
69
+ remote: 4
70
+ }
71
+ },
72
+ authToken: "secret-token"
73
+ }));
74
+ };
75
+ ```
47
76
 
48
- const server = new WorkerServer({
49
- port: 19100,
50
- heartbeatTimeoutMs: 60_000,
51
- healthCheckIntervalMs: 10_000,
52
- heartbeatIntervalMs: 15_000,
53
- });
77
+ ### Heartbeat Monitoring
54
78
 
55
- await server.start();
56
- await server.stop();
57
- ```
79
+ Workers must send periodic heartbeats to remain healthy. The server automatically marks workers unhealthy if heartbeats are missed and disconnects them after `3x` the timeout period.
58
80
 
59
- #### Lifecycle Events
81
+ ## Message Operations
60
82
 
61
- | Method | Description |
62
- |--------|-------------|
63
- | `onWorkerConnected(handler)` | Called when a worker registers successfully |
64
- | `onWorkerDisconnected(handler)` | Called when a worker disconnects; includes pending request IDs |
83
+ ### Sending Messages to Workers
65
84
 
66
85
  ```typescript
67
- server.onWorkerConnected((worker) => {
68
- console.log(`Connected: ${worker.id} (${worker.name})`);
86
+ // Send a message to a specific worker
87
+ const success = server.send(workerId, {
88
+ type: "work_request",
89
+ requestId: "req-123",
90
+ input: "Process this data"
69
91
  });
70
92
 
71
- server.onWorkerDisconnected((worker, pendingRequestIds) => {
72
- console.log(`Disconnected: ${worker.id} with ${pendingRequestIds.size} pending requests`);
73
- });
93
+ // Broadcast to all connected workers
94
+ server.broadcast({ type: "shutdown" });
74
95
  ```
75
96
 
76
- #### Message Routing
77
-
78
- Register handlers for message types sent by workers:
97
+ ### Registering Message Handlers
79
98
 
80
99
  ```typescript
81
100
  server.onWorkerMessage("work_complete", (worker, message) => {
82
- const { requestId, result } = message;
83
- console.log(`Worker ${worker.id} completed ${requestId}`);
101
+ console.log(`Result for request ${message.requestId}`);
84
102
  });
85
-
86
- // Send messages to workers
87
- const success = server.send(workerId, { type: "work_request", requestId: "req-1" });
88
- server.broadcast({ type: "shutdown" });
89
103
  ```
90
104
 
91
- #### Worker Selection & Pool Queries
105
+ ## Worker Pool Queries
92
106
 
93
- | Method | Description |
94
- |--------|-------------|
95
- | `getAvailableWorker(model, category?)` | Least-loaded worker supporting the model |
96
- | `getAnyAvailableWorker()` | Any available or busy worker (model-agnostic) |
97
- | `getAvailableSlotCount(model, category?)` | Total free slots across all available workers |
98
- | `getWorkerCount()` | Total connected workers |
99
- | `getAvailableWorkerCount()` | Available workers count |
100
- | `getWorkerInfo()` | Public info for all workers |
107
+ ### Get Available Workers
101
108
 
102
109
  ```typescript
103
- // Get least-loaded worker supporting a model
104
- const worker = server.getAvailableWorker("sonnet");
110
+ // Get least-loaded worker supporting a specific model
111
+ const worker = server.getAvailableWorker("gpt-4");
105
112
  if (worker) {
106
113
  server.trackRequest(worker.id, "req-123", "local");
114
+ server.send(worker.id, { type: "work_request", data: "..." });
107
115
  }
108
116
 
109
- // Slot counts with category-aware limits
110
- console.log("Available slots:", server.getAvailableSlotCount("sonnet", "local"));
117
+ // Get any available worker (model-agnostic)
118
+ const anyWorker = server.getAnyAvailableWorker();
119
+ ```
111
120
 
112
- // View all workers
113
- for (const info of server.getWorkerInfo()) {
114
- console.log(`${info.name}: ${info.status} (${info.activeRequests}/${info.capabilities.maxConcurrentRequests})`);
115
- }
121
+ ### Slot Counting
122
+
123
+ ```typescript
124
+ // Total free slots for a model
125
+ const slots = server.getAvailableSlotCount("gpt-4");
126
+ console.log(`Can accept ${slots} more requests`);
127
+
128
+ // With category-based limits
129
+ const categorySlots = server.getAvailableSlotCount("gpt-4", "local");
116
130
  ```
117
131
 
118
- #### Request Tracking
132
+ ## Request Tracking
119
133
 
120
- Track and release requests for accurate availability:
134
+ Track requests to maintain accurate worker load statistics.
121
135
 
122
136
  ```typescript
123
- // When assigning a request to a worker
137
+ // Mark a request as in-progress
124
138
  server.trackRequest(workerId, requestId, "local");
125
139
 
126
- // When the request completes
140
+ // Release the request when complete
127
141
  server.releaseRequest(requestId, { incrementCompleted: true });
128
142
  ```
129
143
 
130
- #### Extensibility
144
+ ## Authentication
131
145
 
132
- Add HTTP endpoints and custom WebSocket paths:
146
+ Configure an authentication token to require workers to provide credentials during registration.
133
147
 
134
148
  ```typescript
135
- // HTTP handler
136
- server.addHttpHandler(async (req, res) => {
137
- if (req.url === "/health") {
138
- res.writeHead(200, { "Content-Type": "application/json" });
139
- res.end(JSON.stringify({ ok: true }));
140
- return true;
141
- }
142
- return false;
149
+ const server = new WorkerServer({
150
+ port: 19100,
151
+ authToken: "my-secret-token"
143
152
  });
144
153
 
145
- // Custom WebSocket endpoint
146
- server.addWebSocketEndpoint("/ws/dashboard", (ws) => {
147
- ws.send(JSON.stringify({ type: "hello" }));
148
- });
154
+ // Worker registration must include matching token
155
+ ws.send(JSON.stringify({
156
+ type: "worker_registration",
157
+ workerId: "worker-1",
158
+ workerName: "My Worker",
159
+ capabilities: { ... },
160
+ authToken: "my-secret-token"
161
+ }));
149
162
  ```
150
163
 
151
- ### WorkerPool
164
+ ## Event Handlers
152
165
 
153
- Low-level pool manager for worker state and selection.
166
+ ### Connection Events
154
167
 
155
168
  ```typescript
156
- import { WorkerPool, toWorkerInfo, WorkerStatus } from "@hardlydifficult/worker-server";
157
-
158
- const pool = new WorkerPool(logger);
169
+ server.onWorkerConnected((worker) => {
170
+ console.log(`Worker connected: ${worker.id} (${worker.name})`);
171
+ });
159
172
 
160
- // Add/remove workers
161
- pool.add(worker);
162
- pool.remove(workerId);
163
- const worker = pool.get(workerId);
173
+ server.onWorkerDisconnected((worker, pendingRequestIds) => {
174
+ console.log(`Worker ${worker.id} disconnected`);
175
+ if (pendingRequestIds.size > 0) {
176
+ console.log(`Pending requests: ${[...pendingRequestIds].join(", ")}`);
177
+ }
178
+ });
164
179
  ```
165
180
 
166
- #### Selection Logic
167
-
168
- | Method | Description |
169
- |--------|-------------|
170
- | `getAvailableWorker(model, category?)` | Least-loaded worker supporting the model, respecting per-category concurrency limits |
171
- | `getAnyAvailableWorker()` | Any available or busy worker (model-agnostic) |
172
- | `getAvailableSlotCount(model, category?)` | Total free slots across all available workers for the model |
173
- | `getCount()` | Total connected workers |
174
- | `getAvailableCount()` | Available workers count |
175
- | `getWorkerInfoList()` | Public info for all workers |
176
-
177
- #### Request Management
181
+ ## HTTP Endpoints
178
182
 
179
- | Method | Description |
180
- |--------|-------------|
181
- | `trackRequest(workerId, requestId, category?)` | Marks request as in-flight and updates status |
182
- | `releaseRequest(requestId, options?)` | Decrements active count, optionally increments completed count |
183
+ ### Custom HTTP Handlers
183
184
 
184
- #### Health Monitoring
185
-
186
- | Method | Description |
187
- |--------|-------------|
188
- | `checkHealth(timeoutMs)` | Returns IDs of workers exceeding `3x` timeout; marks unhealthy ones |
189
-
190
- ### ConnectionHandler
185
+ ```typescript
186
+ server.addHttpHandler(async (req, res) => {
187
+ if (req.url === "/health") {
188
+ res.writeHead(200, { "Content-Type": "application/json" });
189
+ res.end(JSON.stringify({ status: "ok" }));
190
+ return true;
191
+ }
192
+ return false; // Continue to next handler or return 404
193
+ });
194
+ ```
191
195
 
192
- Handles WebSocket lifecycle, registration, heartbeats, and message routing. Most consumers use `WorkerServer`, which encapsulates this.
196
+ ### Custom WebSocket Endpoints
193
197
 
194
- ### Message Protocol
198
+ ```typescript
199
+ server.addWebSocketEndpoint("/ws/metrics", (ws) => {
200
+ ws.on("message", (msg) => {
201
+ // Handle metrics-specific messages
202
+ });
203
+ });
204
+ ```
195
205
 
196
- Workers send JSON messages with a `type` field:
206
+ ## Server Lifecycle
197
207
 
198
- - `worker_registration` — Register with capabilities and optional `authToken`
199
- - `heartbeat` Send periodically to confirm liveness
208
+ ```typescript
209
+ // Start the server
210
+ await server.start();
200
211
 
201
- The server responds with:
202
- - `worker_registration_ack` — Success/failure with `sessionId` and `heartbeatIntervalMs`
203
- - `heartbeat_ack` — Acknowledgment with `nextHeartbeatDeadline`
212
+ // Stop gracefully
213
+ await server.stop();
214
+ ```
204
215
 
205
- ### Types & Interfaces
216
+ ## Types and Interfaces
206
217
 
207
- #### `WorkerStatus`
218
+ ### WorkerStatus
208
219
 
209
- | Value | Description |
210
- |-------|-------------|
211
- | `available` | Worker can accept new requests |
212
- | `busy` | Worker at capacity, but can accept model-agnostic tasks |
213
- | `draining` | Worker finishing current work before shutdown |
214
- | `unhealthy` | Worker failed heartbeat checks |
220
+ Worker states:
215
221
 
216
- #### `WorkerInfo`
222
+ - `available`: Ready to accept new work
223
+ - `busy`: At capacity, no new requests
224
+ - `draining`: Rejecting new requests, finishing existing
225
+ - `unhealthy`: Heartbeat timeout exceeded
217
226
 
218
- Public worker metadata (excludes raw WebSocket):
227
+ ### WorkerInfo
219
228
 
220
229
  ```typescript
221
230
  interface WorkerInfo {
222
- readonly id: string;
223
- readonly name: string;
224
- readonly status: WorkerStatus;
225
- readonly capabilities: WorkerCapabilities;
226
- readonly sessionId: string;
227
- readonly connectedAt: Date;
228
- readonly lastHeartbeat: Date;
229
- readonly activeRequests: number;
230
- readonly completedRequests: number;
231
- readonly pendingRequestIds: ReadonlySet<string>;
232
- readonly categoryActiveRequests: ReadonlyMap<string, number>;
231
+ id: string;
232
+ name: string;
233
+ status: WorkerStatus;
234
+ capabilities: WorkerCapabilities;
235
+ sessionId: string;
236
+ connectedAt: Date;
237
+ lastHeartbeat: Date;
238
+ activeRequests: number;
239
+ completedRequests: number;
240
+ pendingRequestIds: ReadonlySet<string>;
241
+ categoryActiveRequests: ReadonlyMap<string, number>;
233
242
  }
234
243
  ```
235
244
 
236
- #### `WorkerCapabilities`
245
+ ### WorkerCapabilities
237
246
 
238
247
  ```typescript
239
248
  interface WorkerCapabilities {
240
249
  models: ModelInfo[];
241
250
  maxConcurrentRequests: number;
242
251
  metadata?: Record<string, unknown>;
243
- concurrencyLimits?: Record<string, number>; // per-category limits
252
+ concurrencyLimits?: Record<string, number>;
244
253
  }
245
254
  ```
246
255
 
247
- #### `ModelInfo`
256
+ ### ModelInfo
248
257
 
249
258
  ```typescript
250
259
  interface ModelInfo {
@@ -258,48 +267,30 @@ interface ModelInfo {
258
267
  }
259
268
  ```
260
269
 
261
- ### Secure Authentication
270
+ ## Advanced: Category-Based Concurrency
262
271
 
263
- Authentication tokens are compared using timing-safe comparison to prevent brute-force attacks:
272
+ Workers can define per-category concurrency limits:
264
273
 
265
274
  ```typescript
266
- import { safeCompare } from "@hardlydifficult/worker-server";
267
- // Internally used by ConnectionHandler; exposed for testing
268
- const valid = safeCompare("a", "b"); // false
269
- ```
270
-
271
- Workers must send the token in registration:
272
-
273
- ```json
274
- {
275
- "type": "worker_registration",
276
- "workerId": "worker-1",
277
- "workerName": "My Worker",
278
- "capabilities": { ... },
279
- "authToken": "secret-token"
275
+ capabilities: {
276
+ models: [{ modelId: "gpt-4", ... }],
277
+ maxConcurrentRequests: 10,
278
+ concurrencyLimits: {
279
+ local: 2,
280
+ remote: 4
281
+ }
280
282
  }
281
283
  ```
282
284
 
283
- ### Heartbeat Protocol
285
+ When tracking requests with a category:
284
286
 
285
- Workers must send periodic heartbeat messages:
286
-
287
- ```json
288
- {
289
- "type": "heartbeat",
290
- "workerId": "worker-1",
291
- "timestamp": "2024-01-01T00:00:00.000Z"
292
- }
287
+ ```typescript
288
+ server.trackRequest(workerId, requestId, "local");
289
+ server.releaseRequest(requestId);
293
290
  ```
294
291
 
295
- The server responds with:
292
+ Slot counting respects category limits:
296
293
 
297
- ```json
298
- {
299
- "type": "heartbeat_ack",
300
- "timestamp": "2024-01-01T00:00:00.000Z",
301
- "nextHeartbeatDeadline": "2024-01-01T00:01:15.000Z"
302
- }
303
- ```
304
-
305
- A worker is considered unhealthy if its heartbeat exceeds `heartbeatTimeoutMs`. It is marked dead and disconnected after `3x` the timeout.
294
+ ```typescript
295
+ server.getAvailableSlotCount("gpt-4", "local");
296
+ ```
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Timing-safe string comparison to prevent brute-force attacks.
3
- * Always takes constant time regardless of where strings differ.
2
+ * Constant-time string comparison to prevent timing attacks.
3
+ *
4
+ * Pads both values to the same length before comparing so the timing-safe
5
+ * compare path is always executed, even for different-length inputs.
4
6
  */
5
7
  export declare function safeCompare(a: string, b: string): boolean;
6
8
  //# sourceMappingURL=safeCompare.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safeCompare.d.ts","sourceRoot":"","sources":["../src/safeCompare.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAWzD"}
1
+ {"version":3,"file":"safeCompare.d.ts","sourceRoot":"","sources":["../src/safeCompare.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAczD"}
@@ -3,17 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.safeCompare = safeCompare;
4
4
  const crypto_1 = require("crypto");
5
5
  /**
6
- * Timing-safe string comparison to prevent brute-force attacks.
7
- * Always takes constant time regardless of where strings differ.
6
+ * Constant-time string comparison to prevent timing attacks.
7
+ *
8
+ * Pads both values to the same length before comparing so the timing-safe
9
+ * compare path is always executed, even for different-length inputs.
8
10
  */
9
11
  function safeCompare(a, b) {
10
12
  const bufA = Buffer.from(a);
11
13
  const bufB = Buffer.from(b);
12
- if (bufA.length !== bufB.length) {
13
- // Compare bufA against itself so the timing is consistent
14
- (0, crypto_1.timingSafeEqual)(bufA, bufA);
15
- return false;
16
- }
17
- return (0, crypto_1.timingSafeEqual)(bufA, bufB);
14
+ const maxLength = Math.max(bufA.length, bufB.length);
15
+ const paddedA = Buffer.alloc(maxLength);
16
+ const paddedB = Buffer.alloc(maxLength);
17
+ bufA.copy(paddedA);
18
+ bufB.copy(paddedB);
19
+ const valuesMatch = (0, crypto_1.timingSafeEqual)(paddedA, paddedB);
20
+ return bufA.length === bufB.length && valuesMatch;
18
21
  }
19
22
  //# sourceMappingURL=safeCompare.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"safeCompare.js","sourceRoot":"","sources":["../src/safeCompare.ts"],"names":[],"mappings":";;AAMA,kCAWC;AAjBD,mCAAyC;AAEzC;;;GAGG;AACH,SAAgB,WAAW,CAAC,CAAS,EAAE,CAAS;IAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE5B,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,0DAA0D;QAC1D,IAAA,wBAAe,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAA,wBAAe,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC"}
1
+ {"version":3,"file":"safeCompare.js","sourceRoot":"","sources":["../src/safeCompare.ts"],"names":[],"mappings":";;AAQA,kCAcC;AAtBD,mCAAyC;AAEzC;;;;;GAKG;AACH,SAAgB,WAAW,CAAC,CAAS,EAAE,CAAS;IAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE5B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAExC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAEnB,MAAM,WAAW,GAAG,IAAA,wBAAe,EAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAEtD,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC;AACpD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/worker-server",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [