@qianxude/tem 0.3.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.3.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",
@@ -29,6 +30,7 @@
29
30
  "test:auto-detect": "bun test tests/integration/auto-detect.test.ts",
30
31
  "lint": "oxlint",
31
32
  "lint:file": "oxlint",
33
+ "example:llm-detect": "bun examples/llm-detect.ts",
32
34
  "dev": "bun --watch src/index.ts",
33
35
  "cli": "bun ./src/cli/index.ts",
34
36
  "publish:pkg": "bun publish --access public",
@@ -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
+ ```
package/src/core/tem.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Database, type DatabaseOptions } from '../database/index.js';
2
- import { BatchService, TaskService } from '../services/index.js';
2
+ import { BatchService, TaskService, BatchInterruptionService } from '../services/index.js';
3
3
  import { Worker, type WorkerConfig } from './worker.js';
4
4
  import {
5
5
  detectConstraints,
@@ -27,12 +27,16 @@ export interface TEMConfig {
27
27
 
28
28
  // Polling
29
29
  pollIntervalMs: number;
30
+
31
+ // Optional: Specific batch ID to process (if set, only processes this batch)
32
+ batchId?: string;
30
33
  }
31
34
 
32
35
  export class TEM {
33
36
  readonly batch: BatchService;
34
37
  readonly task: TaskService;
35
38
  readonly worker: Worker;
39
+ readonly interruption: BatchInterruptionService;
36
40
 
37
41
  private database: Database;
38
42
 
@@ -70,6 +74,7 @@ export class TEM {
70
74
  }
71
75
 
72
76
  constructor(config: TEMConfig) {
77
+
73
78
  // Initialize database
74
79
  const dbOptions: DatabaseOptions = {
75
80
  path: config.databasePath,
@@ -79,12 +84,15 @@ export class TEM {
79
84
  // Initialize services
80
85
  this.batch = new BatchService(this.database);
81
86
  this.task = new TaskService(this.database);
87
+ this.interruption = new BatchInterruptionService(this.database, this.batch);
82
88
 
83
89
  // Initialize worker with config
84
90
  const workerConfig: WorkerConfig = {
85
91
  concurrency: config.concurrency,
86
92
  pollIntervalMs: config.pollIntervalMs,
87
93
  rateLimit: config.rateLimit,
94
+ batchId: config.batchId,
95
+ interruptionService: this.interruption,
88
96
  };
89
97
  this.worker = new Worker(this.task, workerConfig);
90
98
  }
@@ -97,4 +105,24 @@ export class TEM {
97
105
  await this.worker.stop();
98
106
  this.database.close();
99
107
  }
108
+
109
+ /**
110
+ * Manually interrupt a batch with a specified reason.
111
+ * This will stop the worker if processing this batch and prevent further tasks from being claimed.
112
+ *
113
+ * @param batchId - The ID of the batch to interrupt
114
+ * @param reason - The reason for interruption (default: 'manual')
115
+ * @param message - Optional custom message explaining the interruption
116
+ */
117
+ async interruptBatch(
118
+ batchId: string,
119
+ reason?: import('../interfaces/index.js').BatchInterruptionReason,
120
+ message?: string
121
+ ): Promise<void> {
122
+ await this.interruption.interrupt(
123
+ batchId,
124
+ reason ?? 'manual',
125
+ message ?? 'Batch manually interrupted'
126
+ );
127
+ }
100
128
  }