@qianxude/tem 0.4.0 → 0.4.2

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
@@ -6,6 +6,14 @@ Built for **single-process, IO-bound scenarios** where you need reliable task ex
6
6
 
7
7
  ---
8
8
 
9
+ ## Installation
10
+
11
+ ```sh
12
+ bun add @qianxude/tem
13
+ ```
14
+
15
+ ---
16
+
9
17
  ## Features
10
18
 
11
19
  - **SQLite Persistence** — Tasks survive process restarts using `bun:sqlite` with WAL mode
@@ -43,38 +51,37 @@ Don't use tem when you need:
43
51
  ```typescript
44
52
  import { TEM } from "@qianxude/tem";
45
53
 
46
- // Initialize
47
54
  const tem = new TEM({
48
- dbPath: "./tem.db",
49
- concurrency: 5, // Max 5 concurrent tasks
50
- pollInterval: 1000, // Check for new tasks every 1s
51
- rateLimit: {
52
- perMinute: 60, // Respect LLM provider limits
53
- perSecond: 5
54
- }
55
+ databasePath: "./tem.db",
56
+ concurrency: 5,
57
+ pollIntervalMs: 1000,
58
+ rateLimit: { requests: 60, windowMs: 60000 } // 60 req/min
55
59
  });
56
60
 
57
61
  // Create a batch
58
62
  const batch = await tem.batch.create({
59
- code: "2026-02-15-llm-fix", // Your custom tag
63
+ code: "2026-02-15-llm-fix",
60
64
  type: "rewrite-docs"
61
65
  });
62
66
 
63
- // Enqueue tasks
64
- await tem.task.enqueueMany([
67
+ // Create tasks
68
+ await tem.task.createMany([
65
69
  { batchId: batch.id, type: "rewrite", payload: { docId: 1 } },
66
70
  { batchId: batch.id, type: "rewrite", payload: { docId: 2 } },
67
71
  { batchId: batch.id, type: "rewrite", payload: { docId: 3 } }
68
72
  ]);
69
73
 
70
- // Register handler
71
- tem.worker.register("rewrite", async (task) => {
72
- const result = await callLLM(task.payload);
74
+ // Register handler — payload is your task data, context has metadata
75
+ tem.worker.register("rewrite", async (payload, context) => {
76
+ const result = await callLLM(payload);
73
77
  return result; // Stored in task.result
74
78
  });
75
79
 
76
80
  // Start processing
77
81
  tem.worker.start();
82
+
83
+ // Stop when done
84
+ await tem.stop();
78
85
  ```
79
86
 
80
87
  ---
@@ -99,6 +106,111 @@ failed
99
106
 
100
107
  ---
101
108
 
109
+ ## Core Concepts
110
+
111
+ - **Batch** — A named group of tasks. All recovery operations (resume, retry) work at batch level.
112
+ - **Task** — A unit of work with a `type`, opaque `payload`, and tracked `status`.
113
+ - **Worker** — Polls for pending tasks and dispatches them to registered handlers by type.
114
+ - **Payload** — Opaque JSON; the framework never parses it. Your handler receives it as-is.
115
+ - **Claim model** — Tasks are acquired atomically (`UPDATE ... WHERE status='pending'`), preventing duplicate execution.
116
+
117
+ ### Task Ordering
118
+
119
+ Tasks within a batch are claimed and executed in **FIFO order** (First-In-First-Out) based on creation time.
120
+ When multiple tasks are pending, the task created first will be claimed first:
121
+
122
+ ```typescript
123
+ // These tasks will be claimed in order: task1, then task2, then task3
124
+ await tem.task.create({ batchId: batch.id, type: "process", payload: { id: 1 } }); // task1
125
+ await tem.task.create({ batchId: batch.id, type: "process", payload: { id: 2 } }); // task2
126
+ await tem.task.create({ batchId: batch.id, type: "process", payload: { id: 3 } }); // task3
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Error Handling
132
+
133
+ By default, any thrown error causes the task to retry up to `defaultMaxAttempts`:
134
+
135
+ ```typescript
136
+ tem.worker.register("process", async (payload, context) => {
137
+ console.log(`Attempt ${context.attempt}`);
138
+ const result = await callAPI(payload); // throws → auto-retry
139
+ return result;
140
+ });
141
+ ```
142
+
143
+ For permanent failures that should not be retried, throw `NonRetryableError`:
144
+
145
+ ```typescript
146
+ import { TEM, NonRetryableError } from "@qianxude/tem";
147
+
148
+ tem.worker.register("validate", async (payload) => {
149
+ if (!payload.id) {
150
+ throw new NonRetryableError("Missing required field: id");
151
+ // Task goes directly to 'failed', no retries
152
+ }
153
+ return process(payload);
154
+ });
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Batch Interruption
160
+
161
+ Automatically stop a batch when error thresholds are exceeded:
162
+
163
+ ```typescript
164
+ const batch = await tem.batch.create({
165
+ code: "llm-run-01",
166
+ type: "summarize",
167
+ interruptionCriteria: {
168
+ maxErrorRate: 0.3, // Stop if >30% tasks fail
169
+ maxFailedTasks: 10, // Stop if >10 tasks fail
170
+ maxConsecutiveFailures: 5, // Stop if 5 failures in a row
171
+ }
172
+ });
173
+ ```
174
+
175
+ Check interruption details after the batch stops:
176
+
177
+ ```typescript
178
+ const logs = await tem.interruption.getInterruptionLog(batchId);
179
+ // [{ reason, message, statsAtInterruption }]
180
+ ```
181
+
182
+ Manually interrupt a running batch:
183
+
184
+ ```typescript
185
+ await tem.interruptBatch(batchId, "manual", "Stopping due to bad data");
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Auto-Detect Constraints
191
+
192
+ Probe an API endpoint to discover its concurrency and rate limits before running tasks:
193
+
194
+ ```typescript
195
+ const config = await TEM.detectConstraints({
196
+ url: "https://api.example.com/v1/endpoint",
197
+ method: "POST",
198
+ headers: { Authorization: "Bearer " + process.env.API_KEY },
199
+ body: { /* minimal valid request */ },
200
+ timeoutMs: 30000,
201
+ maxConcurrencyToTest: 50,
202
+ rateLimitTestDurationMs: 10000,
203
+ });
204
+
205
+ const tem = new TEM({
206
+ databasePath: "./tasks.db",
207
+ concurrency: config.concurrency,
208
+ rateLimit: config.rateLimit,
209
+ });
210
+ ```
211
+
212
+ ---
213
+
102
214
  ## Recovery Patterns
103
215
 
104
216
  ### Resume After Crash
@@ -133,7 +245,7 @@ TEM
133
245
  ├── Worker # Execution loop with concurrency/rate limiting
134
246
  ├── ConcurrencyController # Semaphore for local concurrency
135
247
  ├── RateLimiter # Token bucket for API rate limits
136
- └── RetryStrategy # Configurable retry logic
248
+ └── BatchInterruptionService # Auto-stop on error thresholds
137
249
  ```
138
250
 
139
251
  ### Why Claim-Based?
@@ -199,12 +311,13 @@ This ensures:
199
311
 
200
312
  ```typescript
201
313
  interface TEMConfig {
202
- dbPath: string; // SQLite file path
203
- concurrency?: number; // Default: 5
204
- pollInterval?: number; // Default: 1000ms
314
+ databasePath: string; // SQLite file path
315
+ concurrency?: number; // Default: 5
316
+ pollIntervalMs?: number; // Default: 1000ms
317
+ defaultMaxAttempts?: number; // Default: 3
205
318
  rateLimit?: {
206
- perMinute?: number;
207
- perSecond?: number;
319
+ requests: number; // Number of requests
320
+ windowMs: number; // Time window in ms (e.g. 60000 for per-minute)
208
321
  };
209
322
  }
210
323
  ```
@@ -216,57 +329,71 @@ interface TEMConfig {
216
329
  const batch = await tem.batch.create({
217
330
  code: "unique-batch-code",
218
331
  type: "batch-type",
219
- metadata?: { ... }
332
+ metadata?: { ... },
333
+ interruptionCriteria?: {
334
+ maxErrorRate?: number;
335
+ maxFailedTasks?: number;
336
+ maxConsecutiveFailures?: number;
337
+ }
220
338
  });
221
339
 
222
- // Get batch info
223
- const batch = await tem.batch.get(batchId);
224
-
225
- // List batches
226
- const batches = await tem.batch.list({ type?: "..." });
340
+ // Get batch by ID
341
+ const batch = await tem.batch.getById(batchId);
227
342
 
228
343
  // Get statistics
229
344
  const stats = await tem.batch.getStats(batchId);
230
- // { pending: 5, running: 2, completed: 10, failed: 3 }
345
+ // { pending, running, completed, failed, total }
231
346
 
232
347
  // Resume after crash (running → pending)
233
348
  await tem.batch.resume(batchId);
234
349
 
235
- // Retry all failed (failed → pending, attempt=0)
350
+ // Retry all failed (failed → pending, attempt reset)
236
351
  await tem.batch.retryFailed(batchId);
237
352
  ```
238
353
 
239
354
  ### Task Operations
240
355
 
241
356
  ```typescript
242
- // Enqueue single task
243
- await tem.task.enqueue({
357
+ // Create single task
358
+ await tem.task.create({
244
359
  batchId: string,
245
360
  type: string,
246
361
  payload: object,
247
- maxAttempt?: number // Default: 3
362
+ maxAttempts?: number
248
363
  });
249
364
 
250
- // Bulk enqueue (transaction)
251
- await tem.task.enqueueMany([
365
+ // Bulk create (single transaction)
366
+ await tem.task.createMany([
252
367
  { batchId, type, payload },
253
368
  ...
254
369
  ]);
370
+
371
+ // Get task by ID
372
+ const task = await tem.task.getById(taskId);
255
373
  ```
256
374
 
257
375
  ### Worker
258
376
 
259
377
  ```typescript
260
378
  // Register handler
261
- tem.worker.register("task-type", async (task) => {
262
- // task.id, task.batchId, task.payload, task.attempt
263
- const result = await doWork(task.payload);
264
- return result; // Will be JSON-serialized to task.result
379
+ // payload: your task data; context: { taskId, batchId, attempt }
380
+ tem.worker.register("task-type", async (payload, context) => {
381
+ const result = await doWork(payload);
382
+ return result; // JSON-serialized to task.result
265
383
  });
266
384
 
267
385
  // Control execution
268
386
  tem.worker.start();
269
- await tem.worker.stop();
387
+ await tem.stop(); // Stops worker and closes DB
388
+ ```
389
+
390
+ ### NonRetryableError
391
+
392
+ ```typescript
393
+ import { NonRetryableError } from "@qianxude/tem";
394
+
395
+ throw new NonRetryableError("reason");
396
+ // Task goes to 'failed' immediately, skipping remaining attempts
270
397
  ```
271
398
 
272
399
  ---
@@ -298,6 +425,37 @@ await tem.worker.stop();
298
425
 
299
426
  ---
300
427
 
428
+ ## Mock Server
429
+
430
+ Tem includes a built-in mock HTTP server for testing task execution under various constraints. Use it to simulate APIs with:
431
+
432
+ - **Concurrency limits** — Test how your tasks handle 503 errors
433
+ - **Rate limiting** — Verify retry behavior against 429 responses
434
+ - **Error simulation** — Test resilience with configurable failure rates
435
+
436
+ See [src/mock-server/README.md](src/mock-server/README.md) for detailed documentation.
437
+
438
+ ---
439
+
440
+ ## CLI
441
+
442
+ Tem includes a CLI for batch diagnostics and monitoring:
443
+
444
+ ```sh
445
+ # Generate diagnostic report
446
+ tem report ./tem.db my-batch
447
+
448
+ # List failed tasks
449
+ tem list ./tem.db --batch my-batch --status failed
450
+
451
+ # Watch batch progress in real-time
452
+ tem watch ./tem.db --latest
453
+ ```
454
+
455
+ See [src/cli/README.md](src/cli/README.md) for full documentation.
456
+
457
+ ---
458
+
301
459
  ## License
302
460
 
303
461
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qianxude/tem",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A lightweight task execution engine for IO-bound workloads with SQLite persistence, retry, and rate limiting",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  "scripts": {
23
23
  "typecheck": "tsc --noEmit",
24
24
  "test": "bun test",
25
+ "coverage": "bun test --coverage",
25
26
  "test:integration": "bun test tests/integration/*.test.ts",
26
27
  "test:mock-server": "bun test tests/integration/mock-server.test.ts",
27
28
  "test:simple-tasks": "bun test tests/integration/tem-with-mock-server.test.ts",
@@ -0,0 +1,218 @@
1
+ # tem CLI
2
+
3
+ Command-line interface for batch diagnostics and monitoring.
4
+
5
+ ## Installation
6
+
7
+ The CLI is included with the tem package:
8
+
9
+ ```sh
10
+ bun add @qianxude/tem
11
+ ```
12
+
13
+ You can run it directly with bun:
14
+
15
+ ```sh
16
+ bun run src/cli/index.ts <command> [options]
17
+ ```
18
+
19
+ Or install globally:
20
+
21
+ ```sh
22
+ bun link
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```
28
+ tem <command> [options]
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `report`
34
+
35
+ Generate a diagnostic report for batches.
36
+
37
+ ```sh
38
+ tem report <db-path> [batch-code]
39
+ ```
40
+
41
+ **Arguments:**
42
+
43
+ - `db-path` - Path to the SQLite database file (required)
44
+ - `batch-code` - Specific batch code to report on (optional)
45
+
46
+ **Options:**
47
+
48
+ - `--latest` - Report on the most recently created batch
49
+ - `--limit-errors N` - Show top N error patterns (default: 10)
50
+
51
+ **Examples:**
52
+
53
+ ```sh
54
+ # Summary report for all batches
55
+ tem report ./tem.db
56
+
57
+ # Detailed report for specific batch
58
+ tem report ./tem.db my-batch-code
59
+
60
+ # Report on latest batch
61
+ tem report ./tem.db --latest
62
+
63
+ # Show top 20 error patterns
64
+ tem report ./tem.db my-batch-code --limit-errors 20
65
+ ```
66
+
67
+ **Report includes:**
68
+
69
+ - Batch overview (code, type, status, timestamps, duration)
70
+ - Status breakdown with counts and percentages
71
+ - Timing analysis (avg/min/max task times, throughput)
72
+ - Error patterns for failed tasks
73
+ - Retry analysis statistics
74
+ - Detection of stuck tasks (running > 5 minutes)
75
+
76
+ ---
77
+
78
+ ### `list`
79
+
80
+ List tasks with filtering options.
81
+
82
+ ```sh
83
+ tem list <db-path>
84
+ ```
85
+
86
+ **Arguments:**
87
+
88
+ - `db-path` - Path to the SQLite database file (required)
89
+
90
+ **Options:**
91
+
92
+ - `--batch <code>` - Filter by batch code
93
+ - `--status <status>` - Filter by status: `pending`, `running`, `completed`, or `failed`
94
+ - `--type <type>` - Filter by task type
95
+ - `--limit <n>` - Limit results (default: 100)
96
+
97
+ **Examples:**
98
+
99
+ ```sh
100
+ # List all tasks (up to 100)
101
+ tem list ./tem.db
102
+
103
+ # List failed tasks from a specific batch
104
+ tem list ./tem.db --batch my-batch --status failed
105
+
106
+ # List pending tasks of a specific type
107
+ tem list ./tem.db --status pending --type rewrite --limit 20
108
+ ```
109
+
110
+ **Output columns:**
111
+
112
+ - ID - Task UUID
113
+ - Batch - Batch code
114
+ - Type - Task type
115
+ - Status - Current status
116
+ - Attempts - Current attempt / max attempts
117
+ - Created - Timestamp
118
+ - Completed - Completion timestamp
119
+ - Error - Truncated error message (if failed)
120
+
121
+ ---
122
+
123
+ ### `watch`
124
+
125
+ Monitor a running batch in real-time.
126
+
127
+ ```sh
128
+ tem watch <db-path> [batch-code]
129
+ ```
130
+
131
+ **Arguments:**
132
+
133
+ - `db-path` - Path to the SQLite database file (required)
134
+ - `batch-code` - Specific batch code to watch (optional if using `--latest`)
135
+
136
+ **Options:**
137
+
138
+ - `--latest` - Watch the most recently created batch
139
+ - `--interval N` - Refresh interval in seconds (default: 5)
140
+ - `--timeout N` - Maximum watch time in seconds (default: 3600)
141
+ - `--no-clear` - Don't clear screen between updates
142
+
143
+ **Examples:**
144
+
145
+ ```sh
146
+ # Watch the latest batch
147
+ tem watch ./tem.db --latest
148
+
149
+ # Watch specific batch with 10-second refresh
150
+ tem watch ./tem.db my-batch-code --interval 10
151
+
152
+ # Watch for up to 5 minutes
153
+ tem watch ./tem.db --latest --timeout 300
154
+
155
+ # Watch without clearing screen (for logging)
156
+ tem watch ./tem.db --latest --no-clear
157
+ ```
158
+
159
+ **Watch display includes:**
160
+
161
+ - Visual progress bar
162
+ - Batch status with color coding:
163
+ - 🟢 Green - Completed
164
+ - 🔴 Red - Failed
165
+ - 🟡 Yellow - Running
166
+ - 🔵 Cyan - Pending
167
+ - Real-time statistics (pending, running, completed, failed, total)
168
+ - Throughput and ETA
169
+ - Recent errors (last 3)
170
+ - Stuck task warnings (> 5 minutes running)
171
+
172
+ Press `Ctrl+C` to stop watching. A final report is displayed when the batch completes.
173
+
174
+ ---
175
+
176
+ ## Exit Codes
177
+
178
+ | Code | Meaning |
179
+ |------|---------|
180
+ | 0 | Success |
181
+ | 1 | Runtime error (database issues, batch not found, timeout) |
182
+ | 2 | Usage error (missing arguments, invalid commands/options) |
183
+ | 130 | Interrupted by user (SIGINT) |
184
+
185
+ ---
186
+
187
+ ## Global Options
188
+
189
+ - `--help, -h` - Show help message for any command
190
+
191
+ ## Common Workflows
192
+
193
+ ### Debug a failing batch
194
+
195
+ ```sh
196
+ # Watch the batch in one terminal
197
+ tem watch ./tem.db my-batch --latest
198
+
199
+ # In another terminal, list failed tasks
200
+ tem list ./tem.db --batch my-batch --status failed
201
+
202
+ # Generate detailed report
203
+ tem report ./tem.db my-batch --limit-errors 20
204
+ ```
205
+
206
+ ### Monitor a long-running job
207
+
208
+ ```sh
209
+ # Watch with longer interval to reduce database queries
210
+ tem watch ./tem.db my-batch --interval 30 --timeout 7200
211
+ ```
212
+
213
+ ### Quick status check
214
+
215
+ ```sh
216
+ # Summary of all batches
217
+ tem report ./tem.db
218
+ ```
@@ -1,14 +1,15 @@
1
1
  # Mock Server
2
2
 
3
- A lightweight HTTP server for simulating external API services with configurable concurrency and rate limiting constraints. Used for testing TEM's task execution capabilities under various load conditions.
3
+ A lightweight HTTP server for simulating external API services with configurable concurrency, rate limiting, and error simulation. Used for testing TEM's task execution capabilities under various load conditions.
4
4
 
5
5
  ## Overview
6
6
 
7
7
  The mock server provides a controlled environment to test how TEM handles:
8
8
 
9
- - **Concurrency limits** - Simulate services that reject requests when too many are in flight
10
- - **Rate limiting** - Test backoff and retry behavior against rate-limited endpoints
11
- - **Processing delays** - Verify timeout handling and async processing
9
+ - **Concurrency limits** Simulate services that reject requests when too many are in flight (503 errors)
10
+ - **Rate limiting** Test backoff and retry behavior against rate-limited endpoints (429 errors)
11
+ - **Error simulation** Verify resilience with configurable random failure rates
12
+ - **Processing delays** — Verify timeout handling and async processing
12
13
 
13
14
  ## Architecture
14
15
 
@@ -30,6 +31,8 @@ The mock server provides a controlled environment to test how TEM handles:
30
31
  | Component | File | Purpose |
31
32
  |-----------|------|---------|
32
33
  | `startMockServer` | `server.ts` | Server lifecycle management |
34
+ | `createMockService` | `server.ts` | Client helper to create services |
35
+ | `createErrorSimulation` | `server.ts` | Helper to create error simulation config |
33
36
  | `createRouter` | `router.ts` | HTTP routing and request handling |
34
37
  | `MockService` | `service.ts` | Per-service concurrency and rate limiting |
35
38
  | `RejectingRateLimiter` | `service.ts` | Token bucket rate limiter with immediate reject |
@@ -49,7 +52,8 @@ startMockServer({
49
52
  defaultService: {
50
53
  maxConcurrency: 3,
51
54
  rateLimit: { limit: 10, windowMs: 1000 },
52
- delayMs: [10, 50]
55
+ delayMs: [10, 50],
56
+ errorSimulation: { rate: 0.1, statusCode: 500 }
53
57
  }
54
58
  });
55
59
  ```
@@ -61,10 +65,7 @@ Dynamic service creation and management. Each service has its own concurrency/ra
61
65
  **Use case:** Complex tests with multiple services having different constraints.
62
66
 
63
67
  ```typescript
64
- startMockServer({
65
- port: 8080,
66
- mode: 'multi'
67
- });
68
+ startMockServer({ port: 8080, mode: 'multi' });
68
69
 
69
70
  // Create services dynamically via HTTP API
70
71
  ```
@@ -75,7 +76,7 @@ startMockServer({
75
76
 
76
77
  | Method | Path | Description |
77
78
  |--------|------|-------------|
78
- | `GET` | `/` | Access the default service |
79
+ | `GET` / `POST` | `/` | Access the default service |
79
80
  | `POST` | `/shutdown` | Shutdown the server |
80
81
 
81
82
  ### Multi Mode Endpoints
@@ -84,7 +85,7 @@ startMockServer({
84
85
  |--------|------|-------------|
85
86
  | `POST` | `/service/:name` | Create or replace a service |
86
87
  | `DELETE` | `/service/:name` | Delete a service |
87
- | `GET` | `/mock/:name` | Access a service |
88
+ | `GET` / `POST` | `/mock/:name` | Access a service |
88
89
  | `POST` | `/shutdown` | Shutdown the server |
89
90
 
90
91
  ### Create Service (Multi Mode Only)
@@ -99,7 +100,12 @@ Content-Type: application/json
99
100
  "limit": 10, // Requests per window (required)
100
101
  "windowMs": 1000 // Window size in ms (required)
101
102
  },
102
- "delayMs": [10, 200] // [min, max] processing delay (optional, default: [10, 200])
103
+ "delayMs": [10, 200], // [min, max] processing delay (optional, default: [10, 200])
104
+ "errorSimulation": { // Optional error simulation config
105
+ "rate": 0.1, // Error rate 0-1 (10% = 0.1)
106
+ "statusCode": 503, // HTTP status to return (default: 500)
107
+ "errorMessage": "simulated_error" // Error message (default: "internal_server_error")
108
+ }
103
109
  }
104
110
  ```
105
111
 
@@ -114,11 +120,30 @@ Content-Type: application/json
114
120
  }
115
121
  ```
116
122
 
123
+ ### Delete Service (Multi Mode Only)
124
+
125
+ ```http
126
+ DELETE /service/:name
127
+ ```
128
+
129
+ **Response:**
130
+ ```http
131
+ HTTP/1.1 200 OK
132
+ Content-Type: application/json
133
+
134
+ {
135
+ "service": "test1",
136
+ "status": "deleted"
137
+ }
138
+ ```
139
+
117
140
  ### Access Service
118
141
 
119
142
  ```http
120
143
  GET /mock/:name # Multi mode
144
+ POST /mock/:name # Multi mode (body ignored)
121
145
  GET / # Single mode
146
+ POST / # Single mode (body ignored)
122
147
  ```
123
148
 
124
149
  **Success Response (200):**
@@ -152,6 +177,8 @@ Content-Type: application/json
152
177
  }
153
178
  ```
154
179
 
180
+ The server will stop accepting new connections and exit the process after a brief delay.
181
+
155
182
  ## Error Responses
156
183
 
157
184
  | Status | Error Code | Description |
@@ -163,6 +190,21 @@ Content-Type: application/json
163
190
  | `429` | `rate_limit_exceeded` | Rate limit reached |
164
191
  | `503` | `concurrency_limit_exceeded` | Concurrency limit reached |
165
192
 
193
+ ### Error Simulation
194
+
195
+ When `errorSimulation` is configured, requests may randomly fail with:
196
+
197
+ ```http
198
+ HTTP/1.1 500 Internal Server Error // Or configured statusCode
199
+ Content-Type: application/json
200
+
201
+ {
202
+ "error": "internal_server_error" // Or configured errorMessage
203
+ }
204
+ ```
205
+
206
+ The error is checked **before** acquiring concurrency/rate limit resources, so simulated errors don't count against limits.
207
+
166
208
  ## Configuration
167
209
 
168
210
  ### ServerConfig
@@ -185,6 +227,21 @@ interface ServiceConfig {
185
227
  windowMs: number; // Window duration in milliseconds
186
228
  };
187
229
  delayMs: [number, number]; // [min, max] simulated processing delay
230
+ errorSimulation?: { // Optional error simulation
231
+ rate: number; // Error rate 0-1
232
+ statusCode?: number; // HTTP status code (default: 500)
233
+ errorMessage?: string; // Error message (default: "internal_server_error")
234
+ };
235
+ }
236
+ ```
237
+
238
+ ### ErrorSimulationConfig
239
+
240
+ ```typescript
241
+ interface ErrorSimulationConfig {
242
+ rate: number; // Error rate 0-1 (e.g., 0.1 = 10% error rate)
243
+ statusCode?: number; // HTTP status code to return (default: 500)
244
+ errorMessage?: string; // Error message (default: "internal_server_error")
188
245
  }
189
246
  ```
190
247
 
@@ -315,6 +372,29 @@ console.log(r3.status); // 429
315
372
  console.log(await r3.json()); // { error: 'rate_limit_exceeded' }
316
373
  ```
317
374
 
375
+ ### Testing Error Simulation
376
+
377
+ ```typescript
378
+ import { startMockServer, stopMockServer, createErrorSimulation } from './src/mock-server';
379
+
380
+ startMockServer({
381
+ port: 8080,
382
+ mode: 'single',
383
+ defaultService: {
384
+ maxConcurrency: 10,
385
+ rateLimit: { limit: 100, windowMs: 1000 },
386
+ delayMs: [10, 50],
387
+ errorSimulation: createErrorSimulation(0.3, 503, "service_unavailable")
388
+ }
389
+ });
390
+
391
+ // Approximately 30% of requests will fail with 503
392
+ for (let i = 0; i < 10; i++) {
393
+ const res = await fetch('http://localhost:8080/');
394
+ console.log(res.status); // Mix of 200 and 503
395
+ }
396
+ ```
397
+
318
398
  ## Rate Limiting Algorithm
319
399
 
320
400
  The mock server uses a **token bucket** algorithm for rate limiting:
@@ -332,15 +412,31 @@ Concurrency is tracked per-service:
332
412
  - Decrements when request completes (in `finally` block)
333
413
  - If `currentConcurrency >= maxConcurrency`, new requests get 503
334
414
 
415
+ ## Error Simulation
416
+
417
+ Error simulation is checked before acquiring resources:
418
+
419
+ ```typescript
420
+ // Pseudocode
421
+ if (Math.random() < errorSimulation.rate) {
422
+ return errorResponse; // Doesn't consume concurrency/rate limit tokens
423
+ }
424
+ // Continue with normal request handling...
425
+ ```
426
+
427
+ This ensures that simulated errors don't deplete your concurrency slots or rate limit budget.
428
+
335
429
  ## Programmatic API
336
430
 
431
+ ### Server Lifecycle
432
+
337
433
  ```typescript
338
434
  import { startMockServer, stopMockServer, getServerState } from './src/mock-server';
339
435
 
340
436
  // Start server
341
437
  startMockServer(config: ServerConfig): void
342
438
 
343
- // Stop server programmatically
439
+ // Stop server programmatically (for testing)
344
440
  stopMockServer(): void
345
441
 
346
442
  // Get current state (for testing)
@@ -350,3 +446,74 @@ getServerState(): {
350
446
  hasDefaultService: boolean;
351
447
  }
352
448
  ```
449
+
450
+ ### Client Helpers
451
+
452
+ ```typescript
453
+ import { createMockService, createErrorSimulation } from './src/mock-server';
454
+
455
+ // Create a service programmatically (wraps HTTP call)
456
+ createMockService(
457
+ name: string,
458
+ config: CreateServiceRequest,
459
+ mockUrl?: string // defaults to http://localhost:19999
460
+ ): Promise<Response>
461
+
462
+ // Create error simulation config with validation
463
+ createErrorSimulation(
464
+ rate: number, // 0-1 error rate
465
+ statusCode?: number, // HTTP status (default: 500)
466
+ errorMessage?: string // Error message
467
+ ): ErrorSimulationConfig
468
+ ```
469
+
470
+ Example using client helpers:
471
+
472
+ ```typescript
473
+ import { createMockService, createErrorSimulation } from './src/mock-server';
474
+
475
+ // Create service with error simulation
476
+ await createMockService('flaky-api', {
477
+ maxConcurrency: 5,
478
+ rateLimit: { limit: 10, windowMs: 1000 },
479
+ delayMs: [50, 100],
480
+ errorSimulation: createErrorSimulation(0.2, 503)
481
+ });
482
+
483
+ // Use the service
484
+ const res = await fetch('http://localhost:19999/mock/flaky-api');
485
+ ```
486
+
487
+ ## Testing with TEM
488
+
489
+ The mock server is designed to integrate seamlessly with TEM for testing retry and error handling:
490
+
491
+ ```typescript
492
+ import { TEM } from '@qianxude/tem';
493
+ import { startMockServer, createMockService, createErrorSimulation } from './src/mock-server';
494
+
495
+ // Start mock server with flaky service
496
+ startMockServer({ port: 19999, mode: 'multi' });
497
+
498
+ await createMockService('api', {
499
+ maxConcurrency: 3,
500
+ rateLimit: { limit: 10, windowMs: 1000 },
501
+ errorSimulation: createErrorSimulation(0.2) // 20% failure rate
502
+ });
503
+
504
+ // Configure TEM to match mock server limits
505
+ const tem = new TEM({
506
+ databasePath: ':memory:',
507
+ concurrency: 3,
508
+ rateLimit: { requests: 10, windowMs: 1000 }
509
+ });
510
+
511
+ // Register handler that calls mock server
512
+ tem.worker.register('test', async (payload) => {
513
+ const res = await fetch('http://localhost:19999/mock/api');
514
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
515
+ return res.json();
516
+ });
517
+
518
+ // TEM's retry mechanism will handle the 20% failure rate
519
+ ```