@qianxude/tem 0.2.0
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/LICENSE +21 -0
- package/README.md +303 -0
- package/package.json +42 -0
- package/src/core/index.ts +4 -0
- package/src/core/tem.ts +100 -0
- package/src/core/worker.ts +168 -0
- package/src/database/index.ts +114 -0
- package/src/database/schema.sql +45 -0
- package/src/index.ts +19 -0
- package/src/interfaces/index.ts +186 -0
- package/src/mock-server/README.md +352 -0
- package/src/mock-server/index.ts +3 -0
- package/src/mock-server/router.ts +235 -0
- package/src/mock-server/server.ts +148 -0
- package/src/mock-server/service.ts +122 -0
- package/src/mock-server/types.ts +62 -0
- package/src/services/batch.ts +121 -0
- package/src/services/index.ts +2 -0
- package/src/services/task.ts +176 -0
- package/src/utils/auto-detect.ts +487 -0
- package/src/utils/batch-monitor.ts +52 -0
- package/src/utils/concurrency.ts +44 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/rate-limiter.ts +54 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Mock Server
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The mock server provides a controlled environment to test how TEM handles:
|
|
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
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
17
|
+
│ HTTP Router │────▶│ MockService │────▶│ RateLimiter │
|
|
18
|
+
│ (router.ts) │ │ (service.ts) │ │ (token bucket) │
|
|
19
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
20
|
+
│ │
|
|
21
|
+
▼ ▼
|
|
22
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
23
|
+
│ Service Mgmt │ │ Concurrency │
|
|
24
|
+
│ Endpoints │ │ Controller │
|
|
25
|
+
└─────────────────┘ └─────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Components
|
|
29
|
+
|
|
30
|
+
| Component | File | Purpose |
|
|
31
|
+
|-----------|------|---------|
|
|
32
|
+
| `startMockServer` | `server.ts` | Server lifecycle management |
|
|
33
|
+
| `createRouter` | `router.ts` | HTTP routing and request handling |
|
|
34
|
+
| `MockService` | `service.ts` | Per-service concurrency and rate limiting |
|
|
35
|
+
| `RejectingRateLimiter` | `service.ts` | Token bucket rate limiter with immediate reject |
|
|
36
|
+
|
|
37
|
+
## Modes
|
|
38
|
+
|
|
39
|
+
### Single Mode
|
|
40
|
+
|
|
41
|
+
A simple mode with one pre-configured service accessed at the root path (`/`). No dynamic service management.
|
|
42
|
+
|
|
43
|
+
**Use case:** Simple tests where you just need a constrained endpoint.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
startMockServer({
|
|
47
|
+
port: 8080,
|
|
48
|
+
mode: 'single',
|
|
49
|
+
defaultService: {
|
|
50
|
+
maxConcurrency: 3,
|
|
51
|
+
rateLimit: { limit: 10, windowMs: 1000 },
|
|
52
|
+
delayMs: [10, 50]
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Multi Mode
|
|
58
|
+
|
|
59
|
+
Dynamic service creation and management. Each service has its own concurrency/rate limits and is accessed via `/mock/:name`.
|
|
60
|
+
|
|
61
|
+
**Use case:** Complex tests with multiple services having different constraints.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
startMockServer({
|
|
65
|
+
port: 8080,
|
|
66
|
+
mode: 'multi'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create services dynamically via HTTP API
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API Reference
|
|
73
|
+
|
|
74
|
+
### Single Mode Endpoints
|
|
75
|
+
|
|
76
|
+
| Method | Path | Description |
|
|
77
|
+
|--------|------|-------------|
|
|
78
|
+
| `GET` | `/` | Access the default service |
|
|
79
|
+
| `POST` | `/shutdown` | Shutdown the server |
|
|
80
|
+
|
|
81
|
+
### Multi Mode Endpoints
|
|
82
|
+
|
|
83
|
+
| Method | Path | Description |
|
|
84
|
+
|--------|------|-------------|
|
|
85
|
+
| `POST` | `/service/:name` | Create or replace a service |
|
|
86
|
+
| `DELETE` | `/service/:name` | Delete a service |
|
|
87
|
+
| `GET` | `/mock/:name` | Access a service |
|
|
88
|
+
| `POST` | `/shutdown` | Shutdown the server |
|
|
89
|
+
|
|
90
|
+
### Create Service (Multi Mode Only)
|
|
91
|
+
|
|
92
|
+
```http
|
|
93
|
+
POST /service/:name
|
|
94
|
+
Content-Type: application/json
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
"maxConcurrency": 2, // Max concurrent requests (required)
|
|
98
|
+
"rateLimit": {
|
|
99
|
+
"limit": 10, // Requests per window (required)
|
|
100
|
+
"windowMs": 1000 // Window size in ms (required)
|
|
101
|
+
},
|
|
102
|
+
"delayMs": [10, 200] // [min, max] processing delay (optional, default: [10, 200])
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Response:**
|
|
107
|
+
```http
|
|
108
|
+
HTTP/1.1 201 Created
|
|
109
|
+
Content-Type: application/json
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
"service": "test1",
|
|
113
|
+
"status": "created"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Access Service
|
|
118
|
+
|
|
119
|
+
```http
|
|
120
|
+
GET /mock/:name # Multi mode
|
|
121
|
+
GET / # Single mode
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Success Response (200):**
|
|
125
|
+
```http
|
|
126
|
+
HTTP/1.1 200 OK
|
|
127
|
+
Content-Type: application/json
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
"requestId": "lxyz123-abc456",
|
|
131
|
+
"meta": {
|
|
132
|
+
"ts": 1699999999999, // Timestamp (ms)
|
|
133
|
+
"rt": 45 // Response time (ms)
|
|
134
|
+
},
|
|
135
|
+
"data": "ok"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Shutdown Server
|
|
140
|
+
|
|
141
|
+
```http
|
|
142
|
+
POST /shutdown
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Response:**
|
|
146
|
+
```http
|
|
147
|
+
HTTP/1.1 200 OK
|
|
148
|
+
Content-Type: application/json
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
"status": "shutting_down"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Error Responses
|
|
156
|
+
|
|
157
|
+
| Status | Error Code | Description |
|
|
158
|
+
|--------|------------|-------------|
|
|
159
|
+
| `400` | `invalid_params` | Missing or invalid request parameters |
|
|
160
|
+
| `400` | `single_mode_no_create` | Attempted to create service in single mode |
|
|
161
|
+
| `404` | `not_found` | Unknown route |
|
|
162
|
+
| `404` | `service_not_found` | Service does not exist |
|
|
163
|
+
| `429` | `rate_limit_exceeded` | Rate limit reached |
|
|
164
|
+
| `503` | `concurrency_limit_exceeded` | Concurrency limit reached |
|
|
165
|
+
|
|
166
|
+
## Configuration
|
|
167
|
+
|
|
168
|
+
### ServerConfig
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
interface ServerConfig {
|
|
172
|
+
port: number; // Server port (required)
|
|
173
|
+
mode?: 'single' | 'multi'; // Server mode (default: 'multi')
|
|
174
|
+
defaultService?: ServiceConfig; // Required for single mode
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### ServiceConfig
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
interface ServiceConfig {
|
|
182
|
+
maxConcurrency: number; // Max concurrent requests allowed
|
|
183
|
+
rateLimit: {
|
|
184
|
+
limit: number; // Max requests per window
|
|
185
|
+
windowMs: number; // Window duration in milliseconds
|
|
186
|
+
};
|
|
187
|
+
delayMs: [number, number]; // [min, max] simulated processing delay
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Usage Examples
|
|
192
|
+
|
|
193
|
+
### Basic Single Mode
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { startMockServer, stopMockServer } from './src/mock-server';
|
|
197
|
+
|
|
198
|
+
// Start server with a single constrained service
|
|
199
|
+
startMockServer({
|
|
200
|
+
port: 8080,
|
|
201
|
+
mode: 'single',
|
|
202
|
+
defaultService: {
|
|
203
|
+
maxConcurrency: 2,
|
|
204
|
+
rateLimit: { limit: 5, windowMs: 1000 },
|
|
205
|
+
delayMs: [50, 100]
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Make requests
|
|
210
|
+
const response = await fetch('http://localhost:8080/');
|
|
211
|
+
const data = await response.json();
|
|
212
|
+
// { requestId: '...', meta: { ts: ..., rt: ... }, data: 'ok' }
|
|
213
|
+
|
|
214
|
+
// Cleanup
|
|
215
|
+
stopMockServer();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Multi Mode with Dynamic Services
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { startMockServer, stopMockServer } from './src/mock-server';
|
|
222
|
+
|
|
223
|
+
startMockServer({ port: 8080, mode: 'multi' });
|
|
224
|
+
|
|
225
|
+
// Create a service with strict limits
|
|
226
|
+
await fetch('http://localhost:8080/service/strict', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
maxConcurrency: 1,
|
|
231
|
+
rateLimit: { limit: 2, windowMs: 1000 },
|
|
232
|
+
delayMs: [100, 200]
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Create another service with relaxed limits
|
|
237
|
+
await fetch('http://localhost:8080/service/relaxed', {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: { 'Content-Type': 'application/json' },
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
maxConcurrency: 10,
|
|
242
|
+
rateLimit: { limit: 100, windowMs: 1000 },
|
|
243
|
+
delayMs: [10, 50]
|
|
244
|
+
})
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Access services
|
|
248
|
+
const strict = await fetch('http://localhost:8080/mock/strict');
|
|
249
|
+
const relaxed = await fetch('http://localhost:8080/mock/relaxed');
|
|
250
|
+
|
|
251
|
+
// Delete a service
|
|
252
|
+
await fetch('http://localhost:8080/service/strict', { method: 'DELETE' });
|
|
253
|
+
|
|
254
|
+
// Shutdown
|
|
255
|
+
await fetch('http://localhost:8080/shutdown', { method: 'POST' });
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Testing Concurrency Limits
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
startMockServer({
|
|
262
|
+
port: 8080,
|
|
263
|
+
mode: 'multi'
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Create service with concurrency=1
|
|
267
|
+
await fetch('http://localhost:8080/service/singleton', {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
maxConcurrency: 1,
|
|
272
|
+
rateLimit: { limit: 100, windowMs: 1000 },
|
|
273
|
+
delayMs: [200, 200] // Fixed 200ms delay
|
|
274
|
+
})
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// First request starts (holds slot for 200ms)
|
|
278
|
+
const req1 = fetch('http://localhost:8080/mock/singleton');
|
|
279
|
+
|
|
280
|
+
// Second request immediately (should fail with 503)
|
|
281
|
+
const req2 = await fetch('http://localhost:8080/mock/singleton');
|
|
282
|
+
console.log(req2.status); // 503
|
|
283
|
+
console.log(await req2.json()); // { error: 'concurrency_limit_exceeded' }
|
|
284
|
+
|
|
285
|
+
await req1; // First request succeeds
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Testing Rate Limits
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
startMockServer({
|
|
292
|
+
port: 8080,
|
|
293
|
+
mode: 'multi'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Create service with rate limit of 2 per second
|
|
297
|
+
await fetch('http://localhost:8080/service/ratelimited', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: { 'Content-Type': 'application/json' },
|
|
300
|
+
body: JSON.stringify({
|
|
301
|
+
maxConcurrency: 10,
|
|
302
|
+
rateLimit: { limit: 2, windowMs: 1000 },
|
|
303
|
+
delayMs: [10, 10]
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// First two succeed
|
|
308
|
+
const r1 = await fetch('http://localhost:8080/mock/ratelimited');
|
|
309
|
+
const r2 = await fetch('http://localhost:8080/mock/ratelimited');
|
|
310
|
+
console.log(r1.status, r2.status); // 200 200
|
|
311
|
+
|
|
312
|
+
// Third is rate limited
|
|
313
|
+
const r3 = await fetch('http://localhost:8080/mock/ratelimited');
|
|
314
|
+
console.log(r3.status); // 429
|
|
315
|
+
console.log(await r3.json()); // { error: 'rate_limit_exceeded' }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Rate Limiting Algorithm
|
|
319
|
+
|
|
320
|
+
The mock server uses a **token bucket** algorithm for rate limiting:
|
|
321
|
+
|
|
322
|
+
- Tokens are refilled continuously based on `limit / windowMs` rate
|
|
323
|
+
- Each request consumes 1 token
|
|
324
|
+
- If no tokens available, request is rejected immediately (no queueing)
|
|
325
|
+
- This provides smooth rate limiting without burst issues
|
|
326
|
+
|
|
327
|
+
## Concurrency Control
|
|
328
|
+
|
|
329
|
+
Concurrency is tracked per-service:
|
|
330
|
+
|
|
331
|
+
- `currentConcurrency` increments on successful `tryAcquire()`
|
|
332
|
+
- Decrements when request completes (in `finally` block)
|
|
333
|
+
- If `currentConcurrency >= maxConcurrency`, new requests get 503
|
|
334
|
+
|
|
335
|
+
## Programmatic API
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { startMockServer, stopMockServer, getServerState } from './src/mock-server';
|
|
339
|
+
|
|
340
|
+
// Start server
|
|
341
|
+
startMockServer(config: ServerConfig): void
|
|
342
|
+
|
|
343
|
+
// Stop server programmatically
|
|
344
|
+
stopMockServer(): void
|
|
345
|
+
|
|
346
|
+
// Get current state (for testing)
|
|
347
|
+
getServerState(): {
|
|
348
|
+
services: Map<string, MockService>;
|
|
349
|
+
mode: 'single' | 'multi';
|
|
350
|
+
hasDefaultService: boolean;
|
|
351
|
+
}
|
|
352
|
+
```
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type * as i from './types';
|
|
2
|
+
import { MockService } from './service';
|
|
3
|
+
|
|
4
|
+
// Generate a simple random ID without external deps
|
|
5
|
+
function generateRequestId(): string {
|
|
6
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// JSON response helper
|
|
10
|
+
function jsonResponse(data: unknown, status: number): Response {
|
|
11
|
+
return new Response(JSON.stringify(data), {
|
|
12
|
+
status,
|
|
13
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Error responses
|
|
18
|
+
const errors = {
|
|
19
|
+
serviceNotFound: () => jsonResponse({ error: 'service_not_found' }, 404),
|
|
20
|
+
concurrencyExceeded: () => jsonResponse({ error: 'concurrency_limit_exceeded' }, 503),
|
|
21
|
+
rateLimitExceeded: () => jsonResponse({ error: 'rate_limit_exceeded' }, 429),
|
|
22
|
+
invalidParams: () => jsonResponse({ error: 'invalid_params' }, 400),
|
|
23
|
+
singleModeNoCreate: () => jsonResponse({ error: 'single_mode_no_create' }, 400),
|
|
24
|
+
serviceError: (message: string) => jsonResponse({ error: message }, 500),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Parse request body safely
|
|
28
|
+
async function parseBody(req: Request): Promise<unknown> {
|
|
29
|
+
try {
|
|
30
|
+
return await req.json();
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate service config
|
|
37
|
+
function validateServiceConfig(body: unknown): i.ServiceConfig | null {
|
|
38
|
+
if (!body || typeof body !== 'object') return null;
|
|
39
|
+
|
|
40
|
+
const b = body as Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
// Required: maxConcurrency (number > 0)
|
|
43
|
+
if (typeof b.maxConcurrency !== 'number' || b.maxConcurrency < 1) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Required: rateLimit object with limit and windowMs
|
|
48
|
+
if (!b.rateLimit || typeof b.rateLimit !== 'object') return null;
|
|
49
|
+
const rl = b.rateLimit as Record<string, unknown>;
|
|
50
|
+
if (typeof rl.limit !== 'number' || rl.limit < 1) return null;
|
|
51
|
+
if (typeof rl.windowMs !== 'number' || rl.windowMs < 1) return null;
|
|
52
|
+
|
|
53
|
+
// Optional: delayMs (defaults to [10, 200])
|
|
54
|
+
let delayMs: [number, number];
|
|
55
|
+
if (b.delayMs && Array.isArray(b.delayMs) && b.delayMs.length === 2) {
|
|
56
|
+
delayMs = [b.delayMs[0], b.delayMs[1]];
|
|
57
|
+
} else {
|
|
58
|
+
delayMs = [10, 200];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (delayMs[0] < 0 || delayMs[1] < delayMs[0]) return null;
|
|
62
|
+
|
|
63
|
+
// Optional: errorSimulation config
|
|
64
|
+
let errorSimulation: i.ErrorSimulationConfig | undefined;
|
|
65
|
+
if (b.errorSimulation && typeof b.errorSimulation === 'object') {
|
|
66
|
+
const es = b.errorSimulation as Record<string, unknown>;
|
|
67
|
+
|
|
68
|
+
// Validate rate (required, 0-1)
|
|
69
|
+
if (typeof es.rate !== 'number' || es.rate < 0 || es.rate > 1) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
errorSimulation = { rate: es.rate };
|
|
74
|
+
|
|
75
|
+
// Optional statusCode
|
|
76
|
+
if (typeof es.statusCode === 'number') {
|
|
77
|
+
errorSimulation.statusCode = es.statusCode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Optional errorMessage
|
|
81
|
+
if (typeof es.errorMessage === 'string') {
|
|
82
|
+
errorSimulation.errorMessage = es.errorMessage;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
maxConcurrency: b.maxConcurrency,
|
|
88
|
+
rateLimit: { limit: rl.limit, windowMs: rl.windowMs },
|
|
89
|
+
delayMs,
|
|
90
|
+
errorSimulation,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Router state interface - passed as object with getters
|
|
95
|
+
interface RouterState {
|
|
96
|
+
services: Map<string, MockService>;
|
|
97
|
+
getMode: () => i.ServerMode;
|
|
98
|
+
getDefaultService: () => MockService | null;
|
|
99
|
+
shutdownFn: () => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create router with state
|
|
103
|
+
export function createRouter(state: RouterState) {
|
|
104
|
+
return async function handleRequest(req: Request): Promise<Response> {
|
|
105
|
+
const url = new URL(req.url);
|
|
106
|
+
const method = req.method;
|
|
107
|
+
const pathname = url.pathname;
|
|
108
|
+
const mode = state.getMode();
|
|
109
|
+
|
|
110
|
+
// POST /shutdown - Shutdown server
|
|
111
|
+
if (method === 'POST' && pathname === '/shutdown') {
|
|
112
|
+
// Schedule shutdown after response
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
state.shutdownFn();
|
|
115
|
+
}, 100);
|
|
116
|
+
|
|
117
|
+
return jsonResponse({ status: 'shutting_down' }, 200);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// POST /service/:name - Create service
|
|
121
|
+
if (method === 'POST' && pathname.startsWith('/service/')) {
|
|
122
|
+
if (mode === 'single') {
|
|
123
|
+
return errors.singleModeNoCreate();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const name = pathname.slice('/service/'.length);
|
|
127
|
+
if (!name) return errors.invalidParams();
|
|
128
|
+
|
|
129
|
+
const body = await parseBody(req);
|
|
130
|
+
const config = validateServiceConfig(body);
|
|
131
|
+
if (!config) return errors.invalidParams();
|
|
132
|
+
|
|
133
|
+
if (state.services.has(name)) {
|
|
134
|
+
// Delete old service if exists
|
|
135
|
+
state.services.delete(name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const service = new MockService(name, config);
|
|
139
|
+
state.services.set(name, service);
|
|
140
|
+
|
|
141
|
+
return jsonResponse({ service: name, status: 'created' }, 201);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// DELETE /service/:name - Delete service
|
|
145
|
+
if (method === 'DELETE' && pathname.startsWith('/service/')) {
|
|
146
|
+
if (mode === 'single') {
|
|
147
|
+
return errors.singleModeNoCreate();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const name = pathname.slice('/service/'.length);
|
|
151
|
+
if (!name) return errors.invalidParams();
|
|
152
|
+
|
|
153
|
+
const existed = state.services.delete(name);
|
|
154
|
+
if (!existed) return errors.serviceNotFound();
|
|
155
|
+
|
|
156
|
+
return jsonResponse({ service: name, status: 'deleted' }, 200);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// GET /mock/:name - Access mock service (multi mode)
|
|
160
|
+
// POST /mock/:name - Also supported (body ignored for mock)
|
|
161
|
+
if ((method === 'GET' || method === 'POST') && pathname.startsWith('/mock/')) {
|
|
162
|
+
if (mode === 'single') {
|
|
163
|
+
return errors.invalidParams();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const name = pathname.slice('/mock/'.length);
|
|
167
|
+
if (!name) return errors.invalidParams();
|
|
168
|
+
|
|
169
|
+
const service = state.services.get(name);
|
|
170
|
+
if (!service) return errors.serviceNotFound();
|
|
171
|
+
|
|
172
|
+
return handleMockRequest(service);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// GET / - Access default service (single mode)
|
|
176
|
+
// POST / - Also supported (body ignored for mock)
|
|
177
|
+
if ((method === 'GET' || method === 'POST') && pathname === '/') {
|
|
178
|
+
const defaultService = state.getDefaultService();
|
|
179
|
+
if (mode !== 'single' || !defaultService) {
|
|
180
|
+
return errors.invalidParams();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return handleMockRequest(defaultService);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 404 for unmatched routes
|
|
187
|
+
return jsonResponse({ error: 'not_found' }, 404);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle mock service request
|
|
192
|
+
async function handleMockRequest(service: MockService): Promise<Response> {
|
|
193
|
+
const startTime = Date.now();
|
|
194
|
+
|
|
195
|
+
// Check for random service error (before acquiring resources)
|
|
196
|
+
const randomError = service.checkRandomError();
|
|
197
|
+
if (randomError) {
|
|
198
|
+
return errors.serviceError(randomError.errorMessage);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Try to acquire (non-blocking)
|
|
202
|
+
const acquireResult = service.tryAcquire();
|
|
203
|
+
|
|
204
|
+
if (!acquireResult.allowed) {
|
|
205
|
+
if (acquireResult.error === 'concurrency') {
|
|
206
|
+
return errors.concurrencyExceeded();
|
|
207
|
+
}
|
|
208
|
+
if (acquireResult.error === 'rateLimit') {
|
|
209
|
+
return errors.rateLimitExceeded();
|
|
210
|
+
}
|
|
211
|
+
return errors.invalidParams();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Simulate processing delay
|
|
216
|
+
const delay = service.getDelay();
|
|
217
|
+
await Bun.sleep(delay);
|
|
218
|
+
|
|
219
|
+
const rt = Date.now() - startTime;
|
|
220
|
+
|
|
221
|
+
const response: i.MockResponse = {
|
|
222
|
+
requestId: generateRequestId(),
|
|
223
|
+
meta: {
|
|
224
|
+
ts: startTime,
|
|
225
|
+
rt,
|
|
226
|
+
},
|
|
227
|
+
data: 'ok',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return jsonResponse(response, 200);
|
|
231
|
+
} finally {
|
|
232
|
+
// Always release concurrency slot
|
|
233
|
+
service.release();
|
|
234
|
+
}
|
|
235
|
+
}
|