@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,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
|
+
}
|