@hardlydifficult/worker-server 1.0.15 → 1.0.16
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 +157 -166
- package/package.json +1 -1
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
|
-
##
|
|
43
|
+
## Worker Lifecycle
|
|
40
44
|
|
|
41
|
-
###
|
|
45
|
+
### Registration
|
|
42
46
|
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
port: 19100,
|
|
50
|
-
heartbeatTimeoutMs: 60_000,
|
|
51
|
-
healthCheckIntervalMs: 10_000,
|
|
52
|
-
heartbeatIntervalMs: 15_000,
|
|
53
|
-
});
|
|
77
|
+
### Heartbeat Monitoring
|
|
54
78
|
|
|
55
|
-
|
|
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
|
-
|
|
81
|
+
## Message Operations
|
|
60
82
|
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
93
|
+
// Broadcast to all connected workers
|
|
94
|
+
server.broadcast({ type: "shutdown" });
|
|
74
95
|
```
|
|
75
96
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
## Worker Pool Queries
|
|
92
106
|
|
|
93
|
-
|
|
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("
|
|
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
|
-
//
|
|
110
|
-
|
|
117
|
+
// Get any available worker (model-agnostic)
|
|
118
|
+
const anyWorker = server.getAnyAvailableWorker();
|
|
119
|
+
```
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
132
|
+
## Request Tracking
|
|
119
133
|
|
|
120
|
-
Track
|
|
134
|
+
Track requests to maintain accurate worker load statistics.
|
|
121
135
|
|
|
122
136
|
```typescript
|
|
123
|
-
//
|
|
137
|
+
// Mark a request as in-progress
|
|
124
138
|
server.trackRequest(workerId, requestId, "local");
|
|
125
139
|
|
|
126
|
-
//
|
|
140
|
+
// Release the request when complete
|
|
127
141
|
server.releaseRequest(requestId, { incrementCompleted: true });
|
|
128
142
|
```
|
|
129
143
|
|
|
130
|
-
|
|
144
|
+
## Authentication
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
Configure an authentication token to require workers to provide credentials during registration.
|
|
133
147
|
|
|
134
148
|
```typescript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
164
|
+
## Event Handlers
|
|
152
165
|
|
|
153
|
-
|
|
166
|
+
### Connection Events
|
|
154
167
|
|
|
155
168
|
```typescript
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
169
|
+
server.onWorkerConnected((worker) => {
|
|
170
|
+
console.log(`Worker connected: ${worker.id} (${worker.name})`);
|
|
171
|
+
});
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
196
|
+
### Custom WebSocket Endpoints
|
|
193
197
|
|
|
194
|
-
|
|
198
|
+
```typescript
|
|
199
|
+
server.addWebSocketEndpoint("/ws/metrics", (ws) => {
|
|
200
|
+
ws.on("message", (msg) => {
|
|
201
|
+
// Handle metrics-specific messages
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
```
|
|
195
205
|
|
|
196
|
-
|
|
206
|
+
## Server Lifecycle
|
|
197
207
|
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
```typescript
|
|
209
|
+
// Start the server
|
|
210
|
+
await server.start();
|
|
200
211
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
// Stop gracefully
|
|
213
|
+
await server.stop();
|
|
214
|
+
```
|
|
204
215
|
|
|
205
|
-
|
|
216
|
+
## Types and Interfaces
|
|
206
217
|
|
|
207
|
-
|
|
218
|
+
### WorkerStatus
|
|
208
219
|
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
### WorkerInfo
|
|
219
228
|
|
|
220
229
|
```typescript
|
|
221
230
|
interface WorkerInfo {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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>;
|
|
252
|
+
concurrencyLimits?: Record<string, number>;
|
|
244
253
|
}
|
|
245
254
|
```
|
|
246
255
|
|
|
247
|
-
|
|
256
|
+
### ModelInfo
|
|
248
257
|
|
|
249
258
|
```typescript
|
|
250
259
|
interface ModelInfo {
|
|
@@ -258,48 +267,30 @@ interface ModelInfo {
|
|
|
258
267
|
}
|
|
259
268
|
```
|
|
260
269
|
|
|
261
|
-
|
|
270
|
+
## Advanced: Category-Based Concurrency
|
|
262
271
|
|
|
263
|
-
|
|
272
|
+
Workers can define per-category concurrency limits:
|
|
264
273
|
|
|
265
274
|
```typescript
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
285
|
+
When tracking requests with a category:
|
|
284
286
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
292
|
+
Slot counting respects category limits:
|
|
296
293
|
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
```
|