@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.
@@ -0,0 +1,114 @@
1
+ import { Database as SQLiteDatabase, type SQLQueryBindings } from 'bun:sqlite';
2
+ import * as i from '../interfaces/index.js';
3
+
4
+ export interface DatabaseOptions {
5
+ path: string;
6
+ busyTimeout?: number;
7
+ }
8
+
9
+ export class Database implements i.DatabaseConnection {
10
+ private db: SQLiteDatabase;
11
+
12
+ constructor(options: DatabaseOptions) {
13
+ this.db = new SQLiteDatabase(options.path);
14
+
15
+ // Enable WAL mode for better concurrency
16
+ this.db.exec('PRAGMA journal_mode = WAL;');
17
+
18
+ // Set busy timeout for concurrent access safety (default 5 seconds)
19
+ const timeout = options.busyTimeout ?? 5000;
20
+ this.db.exec(`PRAGMA busy_timeout = ${timeout};`);
21
+
22
+ // Run migrations
23
+ this.migrate();
24
+ }
25
+
26
+ private migrate(): void {
27
+ // Create migration tracking table first
28
+ this.db.exec(`
29
+ CREATE TABLE IF NOT EXISTS _migration (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ name TEXT NOT NULL UNIQUE,
32
+ applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
33
+ )
34
+ `);
35
+
36
+ // Check if initial schema needs to be applied
37
+ const migrationCount = this.db
38
+ .query('SELECT COUNT(*) as count FROM _migration WHERE name = $name')
39
+ .get({ $name: '001_initial_schema' }) as { count: number };
40
+
41
+ if (migrationCount.count === 0) {
42
+ this.applyInitialSchema();
43
+ }
44
+ }
45
+
46
+ private applyInitialSchema(): void {
47
+ const schema = `
48
+ -- Batch: Groups of related tasks
49
+ CREATE TABLE IF NOT EXISTS batch (
50
+ id TEXT PRIMARY KEY,
51
+ code TEXT NOT NULL UNIQUE,
52
+ type TEXT NOT NULL,
53
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
54
+ completed_at DATETIME,
55
+ metadata TEXT
56
+ );
57
+
58
+ -- Task: Individual units of work
59
+ CREATE TABLE IF NOT EXISTS task (
60
+ id TEXT PRIMARY KEY,
61
+ batch_id TEXT REFERENCES batch(id) ON DELETE CASCADE,
62
+ type TEXT NOT NULL,
63
+ status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')),
64
+ payload TEXT NOT NULL,
65
+ result TEXT,
66
+ error TEXT,
67
+ attempt INTEGER NOT NULL DEFAULT 0,
68
+ max_attempt INTEGER NOT NULL DEFAULT 3,
69
+ claimed_at DATETIME,
70
+ completed_at DATETIME,
71
+ version INTEGER NOT NULL DEFAULT 0,
72
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
73
+ );
74
+
75
+ -- Indexes for performance
76
+ CREATE INDEX IF NOT EXISTS idx_batch_code ON batch(code);
77
+ CREATE INDEX IF NOT EXISTS idx_batch_type ON batch(type);
78
+ CREATE INDEX IF NOT EXISTS idx_task_batch_id ON task(batch_id);
79
+ CREATE INDEX IF NOT EXISTS idx_task_status ON task(status);
80
+ CREATE INDEX IF NOT EXISTS idx_task_type ON task(type);
81
+ CREATE INDEX IF NOT EXISTS idx_task_claim ON task(status, claimed_at);
82
+ CREATE INDEX IF NOT EXISTS idx_task_pending ON task(status, created_at) WHERE status = 'pending';
83
+ `;
84
+
85
+ this.transaction(() => {
86
+ this.db.exec(schema);
87
+ this.db
88
+ .query('INSERT INTO _migration (name) VALUES ($name)')
89
+ .run({ $name: '001_initial_schema' });
90
+ });
91
+ }
92
+
93
+ query<T = unknown>(sql: string, params?: SQLQueryBindings[]): T[] {
94
+ const stmt = this.db.prepare(sql);
95
+ const results = stmt.all(...(params ?? []));
96
+ stmt.finalize();
97
+ return results as T[];
98
+ }
99
+
100
+ run(sql: string, params?: SQLQueryBindings[]): { changes: number } {
101
+ const stmt = this.db.prepare(sql);
102
+ const result = stmt.run(...(params ?? []));
103
+ stmt.finalize();
104
+ return { changes: result.changes };
105
+ }
106
+
107
+ transaction<T>(fn: () => T): T {
108
+ return this.db.transaction(fn)();
109
+ }
110
+
111
+ close(): void {
112
+ this.db.close();
113
+ }
114
+ }
@@ -0,0 +1,45 @@
1
+ -- TEM Database Schema
2
+ -- SQLite with WAL mode
3
+
4
+ -- Migration tracking
5
+ CREATE TABLE IF NOT EXISTS _migration (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ name TEXT NOT NULL UNIQUE,
8
+ applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
9
+ );
10
+
11
+ -- Batch: Groups of related tasks
12
+ CREATE TABLE IF NOT EXISTS batch (
13
+ id TEXT PRIMARY KEY,
14
+ code TEXT NOT NULL UNIQUE,
15
+ type TEXT NOT NULL,
16
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
17
+ completed_at DATETIME,
18
+ metadata TEXT -- JSON object
19
+ );
20
+
21
+ -- Task: Individual units of work
22
+ CREATE TABLE IF NOT EXISTS task (
23
+ id TEXT PRIMARY KEY,
24
+ batch_id TEXT REFERENCES batch(id) ON DELETE CASCADE,
25
+ type TEXT NOT NULL,
26
+ status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')),
27
+ payload TEXT NOT NULL, -- JSON object (opaque)
28
+ result TEXT, -- JSON object (opaque)
29
+ error TEXT,
30
+ attempt INTEGER NOT NULL DEFAULT 0,
31
+ max_attempt INTEGER NOT NULL DEFAULT 3,
32
+ claimed_at DATETIME,
33
+ completed_at DATETIME,
34
+ version INTEGER NOT NULL DEFAULT 0,
35
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
36
+ );
37
+
38
+ -- Indexes for performance
39
+ CREATE INDEX IF NOT EXISTS idx_batch_code ON batch(code);
40
+ CREATE INDEX IF NOT EXISTS idx_batch_type ON batch(type);
41
+ CREATE INDEX IF NOT EXISTS idx_task_batch_id ON task(batch_id);
42
+ CREATE INDEX IF NOT EXISTS idx_task_status ON task(status);
43
+ CREATE INDEX IF NOT EXISTS idx_task_type ON task(type);
44
+ CREATE INDEX IF NOT EXISTS idx_task_claim ON task(status, claimed_at);
45
+ CREATE INDEX IF NOT EXISTS idx_task_pending ON task(status, created_at) WHERE status = 'pending';
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Main exports for TEM framework
2
+ export * as interfaces from './interfaces/index.js';
3
+ export { Database, type DatabaseOptions } from './database/index.js';
4
+ export { BatchService, TaskService } from './services/index.js';
5
+ export {
6
+ ConcurrencyController,
7
+ RateLimiter,
8
+ printDetectedConfig,
9
+ type RateLimitConfig,
10
+ } from './utils/index.js';
11
+ export {
12
+ TEM,
13
+ Worker,
14
+ NonRetryableError,
15
+ type TEMConfig,
16
+ type WorkerConfig,
17
+ type DetectOptions,
18
+ type DetectedConfig,
19
+ } from './core/index.js';
@@ -0,0 +1,186 @@
1
+ // Public API types for TEM
2
+ // Import as: import * as i from './interfaces'
3
+
4
+ // ============================================================================
5
+ // Enums
6
+ // ============================================================================
7
+
8
+ export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
9
+
10
+ // ============================================================================
11
+ // Entity Types
12
+ // ============================================================================
13
+
14
+ export interface Batch {
15
+ id: string;
16
+ code: string;
17
+ type: string;
18
+ createdAt: Date;
19
+ completedAt: Date | null;
20
+ metadata: Record<string, unknown> | null;
21
+ }
22
+
23
+ export interface BatchStats {
24
+ batchId: string;
25
+ total: number;
26
+ pending: number;
27
+ running: number;
28
+ completed: number;
29
+ failed: number;
30
+ }
31
+
32
+ export interface Task {
33
+ id: string;
34
+ batchId: string | null;
35
+ type: string;
36
+ status: TaskStatus;
37
+ payload: string; // JSON string - opaque to framework
38
+ result: string | null; // JSON string - opaque to framework
39
+ error: string | null;
40
+ attempt: number;
41
+ maxAttempt: number;
42
+ claimedAt: Date | null;
43
+ completedAt: Date | null;
44
+ version: number;
45
+ createdAt: Date;
46
+ }
47
+
48
+ // ============================================================================
49
+ // Configuration
50
+ // ============================================================================
51
+
52
+ export interface TEMConfig {
53
+ // Database
54
+ databasePath: string;
55
+
56
+ // Concurrency
57
+ concurrency: number;
58
+
59
+ // Rate limiting
60
+ rateLimit?: {
61
+ requests: number;
62
+ windowMs: number;
63
+ };
64
+
65
+ // Retry
66
+ defaultMaxAttempts: number;
67
+
68
+ // Polling
69
+ pollIntervalMs: number;
70
+ }
71
+
72
+ // ============================================================================
73
+ // Auto-Detect Configuration
74
+ // ============================================================================
75
+
76
+ export interface DetectOptions {
77
+ /** Target URL to test */
78
+ url: string;
79
+ /** HTTP method to use (default: GET) */
80
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
81
+ /** Request headers to include */
82
+ headers?: Record<string, string>;
83
+ /** Request body (will be JSON stringified for POST/PUT/PATCH) */
84
+ body?: unknown;
85
+ /** Request timeout in milliseconds (default: 30000) */
86
+ timeoutMs?: number;
87
+ /** Maximum concurrency level to test (default: 100) */
88
+ maxConcurrencyToTest?: number;
89
+ /** Duration to run rate limit tests (default: 10000) */
90
+ rateLimitTestDurationMs?: number;
91
+ }
92
+
93
+ export interface DetectedConfig {
94
+ /** Recommended concurrency (80% of detected max) */
95
+ concurrency: number;
96
+ /** Recommended rate limit (90% of detected limit) */
97
+ rateLimit: {
98
+ requests: number;
99
+ windowMs: number;
100
+ };
101
+ /** Confidence level in the detection results */
102
+ confidence: 'high' | 'medium' | 'low';
103
+ /** Notes about the detection process and findings */
104
+ notes: string[];
105
+ }
106
+
107
+ // ============================================================================
108
+ // Task Handler
109
+ // ============================================================================
110
+
111
+ export type TaskHandler<TInput = unknown, TOutput = unknown> = (
112
+ payload: TInput,
113
+ context: TaskContext
114
+ ) => Promise<TOutput>;
115
+
116
+ export interface TaskContext {
117
+ taskId: string;
118
+ batchId: string | null;
119
+ attempt: number;
120
+ signal: AbortSignal;
121
+ }
122
+
123
+ // ============================================================================
124
+ // Error Types
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Error class to mark errors as non-retryable.
129
+ * When thrown from a task handler, the task will fail immediately
130
+ * without retry attempts.
131
+ */
132
+ export class NonRetryableError extends Error {
133
+ constructor(message: string) {
134
+ super(message);
135
+ this.name = 'NonRetryableError';
136
+ }
137
+ }
138
+
139
+ // ============================================================================
140
+ // Service Interfaces
141
+ // ============================================================================
142
+
143
+ export interface CreateBatchInput {
144
+ code: string;
145
+ type: string;
146
+ metadata?: Record<string, unknown>;
147
+ }
148
+
149
+ export interface CreateTaskInput {
150
+ batchId?: string;
151
+ type: string;
152
+ payload: unknown;
153
+ maxAttempt?: number;
154
+ }
155
+
156
+ export interface BatchService {
157
+ create(input: CreateBatchInput): Promise<Batch>;
158
+ getById(id: string): Promise<Batch | null>;
159
+ getByCode(code: string): Promise<Batch | null>;
160
+ list(filter?: { type?: string }): Promise<Batch[]>;
161
+ getStats(id: string): Promise<BatchStats>;
162
+ complete(id: string): Promise<void>;
163
+ resume(id: string): Promise<number>;
164
+ retryFailed(id: string): Promise<number>;
165
+ }
166
+
167
+ export interface TaskService {
168
+ create(input: CreateTaskInput): Promise<Task>;
169
+ createMany(inputs: CreateTaskInput[]): Promise<Task[]>;
170
+ getById(id: string): Promise<Task | null>;
171
+ claim(batchId?: string): Promise<Task | null>;
172
+ complete(id: string, result: unknown): Promise<void>;
173
+ fail(id: string, error: string): Promise<void>;
174
+ retry(id: string): Promise<void>;
175
+ }
176
+
177
+ // ============================================================================
178
+ // Database
179
+ // ============================================================================
180
+
181
+ export interface DatabaseConnection {
182
+ query<T = unknown>(sql: string, params?: unknown[]): T[];
183
+ run(sql: string, params?: unknown[]): { changes: number };
184
+ transaction<T>(fn: () => T): T;
185
+ close(): void;
186
+ }