@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.
- package/README.md +160 -286
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/worker-server
|
|
2
2
|
|
|
3
|
-
|
|
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({
|
|
16
|
+
const server = new WorkerServer({
|
|
17
|
+
port: 19100,
|
|
18
|
+
authToken: "secret-token", // optional
|
|
19
|
+
});
|
|
17
20
|
|
|
18
21
|
server.onWorkerConnected((worker) => {
|
|
19
|
-
console.log(
|
|
22
|
+
console.log(`Worker ${worker.name} connected with status ${worker.status}`);
|
|
20
23
|
});
|
|
21
24
|
|
|
22
|
-
server.onWorkerMessage("
|
|
23
|
-
console.log(
|
|
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
|
|
30
|
+
console.log("Worker server listening on port", server.port);
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
## Core Concepts
|
|
32
34
|
|
|
33
|
-
###
|
|
35
|
+
### WorkerServer
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
Main entry point for managing worker connections via WebSocket.
|
|
36
38
|
|
|
37
39
|
```typescript
|
|
38
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
// Process work request from worker
|
|
64
|
+
server.onWorkerConnected((worker) => {
|
|
65
|
+
console.log(`Connected: ${worker.id} (${worker.name})`);
|
|
123
66
|
});
|
|
124
67
|
|
|
125
|
-
server.
|
|
126
|
-
|
|
68
|
+
server.onWorkerDisconnected((worker, pendingRequestIds) => {
|
|
69
|
+
console.log(`Disconnected: ${worker.id} with ${pendingRequestIds.size} pending requests`);
|
|
127
70
|
});
|
|
128
71
|
```
|
|
129
72
|
|
|
130
|
-
####
|
|
73
|
+
#### Message Routing
|
|
74
|
+
|
|
75
|
+
Register handlers for message types sent by workers:
|
|
131
76
|
|
|
132
77
|
```typescript
|
|
133
|
-
|
|
134
|
-
const
|
|
78
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
79
|
+
const { requestId, result } = message;
|
|
80
|
+
console.log(`Worker ${worker.id} completed ${requestId}`);
|
|
81
|
+
});
|
|
135
82
|
|
|
136
|
-
//
|
|
137
|
-
server.
|
|
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
|
|
144
|
-
const worker = server.getAvailableWorker("sonnet
|
|
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
|
|
95
|
+
const any = server.getAnyAvailableWorker();
|
|
96
|
+
|
|
97
|
+
// Slot counts
|
|
98
|
+
console.log("Available slots:", server.getAvailableSlotCount("sonnet", "local"));
|
|
148
99
|
|
|
149
|
-
//
|
|
150
|
-
const
|
|
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
|
-
//
|
|
157
|
-
server.trackRequest(
|
|
111
|
+
// When assigning a request to a worker
|
|
112
|
+
server.trackRequest(workerId, requestId, "local");
|
|
158
113
|
|
|
159
|
-
//
|
|
160
|
-
server.releaseRequest(
|
|
114
|
+
// When the request completes
|
|
115
|
+
server.releaseRequest(requestId, { incrementCompleted: true });
|
|
161
116
|
```
|
|
162
117
|
|
|
163
118
|
#### Extensibility
|
|
164
119
|
|
|
165
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
133
|
+
// Custom WebSocket endpoint
|
|
134
|
+
server.addWebSocketEndpoint("/ws/dashboard", (ws) => {
|
|
135
|
+
ws.send(JSON.stringify({ type: "hello" }));
|
|
208
136
|
});
|
|
209
137
|
```
|
|
210
138
|
|
|
211
|
-
|
|
139
|
+
### WorkerPool
|
|
140
|
+
|
|
141
|
+
Low-level pool manager for worker state and selection.
|
|
212
142
|
|
|
213
143
|
```typescript
|
|
214
|
-
|
|
215
|
-
console.log("Worker connected:", worker.id);
|
|
216
|
-
});
|
|
144
|
+
import { WorkerPool, WorkerStatus } from "@hardlydifficult/worker-server";
|
|
217
145
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
153
|
+
#### Selection Logic
|
|
224
154
|
|
|
225
|
-
|
|
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
|
-
|
|
159
|
+
#### Request Management
|
|
228
160
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
166
|
+
#### Health Monitoring
|
|
241
167
|
|
|
242
|
-
|
|
168
|
+
| Method | Description |
|
|
169
|
+
|--------|-------------|
|
|
170
|
+
| `checkHealth(timeoutMs)` | Returns IDs of workers exceeding `3x` timeout; marks unhealthy ones |
|
|
243
171
|
|
|
244
|
-
|
|
245
|
-
server.addWebSocketEndpoint("/ws/admin", (ws) => {
|
|
246
|
-
ws.on("message", (data) => {
|
|
247
|
-
// Handle admin WebSocket messages
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
```
|
|
172
|
+
### ConnectionHandler
|
|
251
173
|
|
|
252
|
-
|
|
174
|
+
Handles WebSocket lifecycle, registration, heartbeats, and message routing. Most consumers use `WorkerServer`, which encapsulates this.
|
|
253
175
|
|
|
254
|
-
|
|
176
|
+
### Types & Interfaces
|
|
255
177
|
|
|
256
|
-
|
|
257
|
-
const server = new WorkerServer({
|
|
258
|
-
port: 8080,
|
|
259
|
-
authToken: "your-secret-token"
|
|
260
|
-
});
|
|
178
|
+
#### `WorkerStatus`
|
|
261
179
|
|
|
262
|
-
|
|
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
|
-
|
|
187
|
+
#### `WorkerInfo`
|
|
266
188
|
|
|
267
|
-
|
|
189
|
+
Public worker metadata (excludes raw WebSocket):
|
|
268
190
|
|
|
269
191
|
```typescript
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
218
|
+
#### `ModelInfo`
|
|
347
219
|
|
|
348
220
|
```typescript
|
|
349
|
-
interface
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
232
|
+
### Secure Authentication
|
|
358
233
|
|
|
359
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
243
|
+
Workers must send:
|
|
374
244
|
|
|
375
|
-
```
|
|
245
|
+
```json
|
|
376
246
|
{
|
|
377
|
-
type: "
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
255
|
+
### Heartbeat Protocol
|
|
386
256
|
|
|
387
|
-
|
|
257
|
+
Workers must send periodic heartbeat messages:
|
|
258
|
+
|
|
259
|
+
```json
|
|
388
260
|
{
|
|
389
|
-
type: "heartbeat",
|
|
390
|
-
workerId:
|
|
391
|
-
timestamp:
|
|
261
|
+
"type": "heartbeat",
|
|
262
|
+
"workerId": "worker-1",
|
|
263
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
392
264
|
}
|
|
393
265
|
```
|
|
394
266
|
|
|
395
|
-
|
|
267
|
+
The server responds with:
|
|
396
268
|
|
|
397
|
-
```
|
|
269
|
+
```json
|
|
398
270
|
{
|
|
399
|
-
type: "heartbeat_ack",
|
|
400
|
-
timestamp:
|
|
401
|
-
nextHeartbeatDeadline:
|
|
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.
|