@qianxude/tem 0.4.2 → 0.4.4
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/package.json +1 -1
- package/src/core/tem.ts +6 -2
- package/src/database/index.ts +11 -108
- package/src/database/schema.sql +17 -1
- package/src/interfaces/index.ts +3 -0
- package/src/services/batch-interruption.ts +8 -2
package/package.json
CHANGED
package/src/core/tem.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type DetectOptions,
|
|
7
7
|
type DetectedConfig,
|
|
8
8
|
} from '../utils/auto-detect.js';
|
|
9
|
+
import type { BatchInterruptionReason, BatchInterruptionCriteria } from '../interfaces/index.js';
|
|
9
10
|
|
|
10
11
|
export type { DetectOptions, DetectedConfig };
|
|
11
12
|
|
|
@@ -30,6 +31,9 @@ export interface TEMConfig {
|
|
|
30
31
|
|
|
31
32
|
// Optional: Specific batch ID to process (if set, only processes this batch)
|
|
32
33
|
batchId?: string;
|
|
34
|
+
|
|
35
|
+
// Default interruption criteria for all batches (batch-level overrides these)
|
|
36
|
+
defaultInterruptionCriteria?: BatchInterruptionCriteria;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export class TEM {
|
|
@@ -84,7 +88,7 @@ export class TEM {
|
|
|
84
88
|
// Initialize services
|
|
85
89
|
this.batch = new BatchService(this.database);
|
|
86
90
|
this.task = new TaskService(this.database);
|
|
87
|
-
this.interruption = new BatchInterruptionService(this.database, this.batch);
|
|
91
|
+
this.interruption = new BatchInterruptionService(this.database, this.batch, config.defaultInterruptionCriteria);
|
|
88
92
|
|
|
89
93
|
// Initialize worker with config
|
|
90
94
|
const workerConfig: WorkerConfig = {
|
|
@@ -116,7 +120,7 @@ export class TEM {
|
|
|
116
120
|
*/
|
|
117
121
|
async interruptBatch(
|
|
118
122
|
batchId: string,
|
|
119
|
-
reason?:
|
|
123
|
+
reason?: BatchInterruptionReason,
|
|
120
124
|
message?: string
|
|
121
125
|
): Promise<void> {
|
|
122
126
|
await this.interruption.interrupt(
|
package/src/database/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Database as SQLiteDatabase, type SQLQueryBindings } from 'bun:sqlite';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
2
4
|
import * as i from '../interfaces/index.js';
|
|
3
5
|
|
|
4
6
|
export interface DatabaseOptions {
|
|
@@ -13,121 +15,22 @@ export class Database implements i.DatabaseConnection {
|
|
|
13
15
|
this.db = new SQLiteDatabase(options.path);
|
|
14
16
|
|
|
15
17
|
// Enable WAL mode for better concurrency
|
|
16
|
-
this.db.
|
|
18
|
+
this.db.run('PRAGMA journal_mode = WAL;');
|
|
17
19
|
|
|
18
20
|
// Set busy timeout for concurrent access safety (default 5 seconds)
|
|
19
21
|
const timeout = options.busyTimeout ?? 5000;
|
|
20
|
-
this.db.
|
|
22
|
+
this.db.run(`PRAGMA busy_timeout = ${timeout};`);
|
|
21
23
|
|
|
22
|
-
//
|
|
23
|
-
this.
|
|
24
|
+
// Initialize schema
|
|
25
|
+
this.initSchema();
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
private
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
-
name TEXT NOT NULL UNIQUE,
|
|
32
|
-
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
33
|
-
)
|
|
34
|
-
`);
|
|
28
|
+
private initSchema(): void {
|
|
29
|
+
// Read schema from file
|
|
30
|
+
const schemaPath = join(import.meta.dirname, 'schema.sql');
|
|
31
|
+
const schema = readFileSync(schemaPath, 'utf-8');
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
const migrations = [
|
|
38
|
-
{ name: '001_initial_schema', apply: () => this.applyInitialSchema() },
|
|
39
|
-
{ name: '002_batch_interruption', apply: () => this.applyBatchInterruptionMigration() },
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
for (const migration of migrations) {
|
|
43
|
-
const migrationCount = this.db
|
|
44
|
-
.query('SELECT COUNT(*) as count FROM _migration WHERE name = $name')
|
|
45
|
-
.get({ $name: migration.name }) as { count: number };
|
|
46
|
-
|
|
47
|
-
if (migrationCount.count === 0) {
|
|
48
|
-
migration.apply();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private applyInitialSchema(): void {
|
|
54
|
-
const schema = `
|
|
55
|
-
-- Batch: Groups of related tasks
|
|
56
|
-
CREATE TABLE IF NOT EXISTS batch (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
code TEXT NOT NULL UNIQUE,
|
|
59
|
-
type TEXT NOT NULL,
|
|
60
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
-
completed_at DATETIME,
|
|
62
|
-
metadata TEXT
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
-- Task: Individual units of work
|
|
66
|
-
CREATE TABLE IF NOT EXISTS task (
|
|
67
|
-
id TEXT PRIMARY KEY,
|
|
68
|
-
batch_id TEXT REFERENCES batch(id) ON DELETE CASCADE,
|
|
69
|
-
type TEXT NOT NULL,
|
|
70
|
-
status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')),
|
|
71
|
-
payload TEXT NOT NULL,
|
|
72
|
-
result TEXT,
|
|
73
|
-
error TEXT,
|
|
74
|
-
attempt INTEGER NOT NULL DEFAULT 0,
|
|
75
|
-
max_attempt INTEGER NOT NULL DEFAULT 3,
|
|
76
|
-
claimed_at DATETIME,
|
|
77
|
-
completed_at DATETIME,
|
|
78
|
-
version INTEGER NOT NULL DEFAULT 0,
|
|
79
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
-- Indexes for performance
|
|
83
|
-
CREATE INDEX IF NOT EXISTS idx_batch_code ON batch(code);
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_batch_type ON batch(type);
|
|
85
|
-
CREATE INDEX IF NOT EXISTS idx_task_batch_id ON task(batch_id);
|
|
86
|
-
CREATE INDEX IF NOT EXISTS idx_task_status ON task(status);
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_task_type ON task(type);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_task_claim ON task(status, claimed_at);
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_task_pending ON task(status, created_at) WHERE status = 'pending';
|
|
90
|
-
`;
|
|
91
|
-
|
|
92
|
-
this.transaction(() => {
|
|
93
|
-
this.db.exec(schema);
|
|
94
|
-
this.db
|
|
95
|
-
.query('INSERT INTO _migration (name) VALUES ($name)')
|
|
96
|
-
.run({ $name: '001_initial_schema' });
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private applyBatchInterruptionMigration(): void {
|
|
101
|
-
const migration = `
|
|
102
|
-
-- Add status to batch table
|
|
103
|
-
ALTER TABLE batch ADD COLUMN status TEXT NOT NULL DEFAULT 'active'
|
|
104
|
-
CHECK(status IN ('active', 'interrupted', 'completed'));
|
|
105
|
-
|
|
106
|
-
-- Add interruption criteria storage (JSON)
|
|
107
|
-
ALTER TABLE batch ADD COLUMN interruption_criteria TEXT;
|
|
108
|
-
|
|
109
|
-
-- Index for quickly finding active batches
|
|
110
|
-
CREATE INDEX IF NOT EXISTS idx_batch_status ON batch(status);
|
|
111
|
-
|
|
112
|
-
-- New table: interruption log
|
|
113
|
-
CREATE TABLE IF NOT EXISTS batch_interrupt_log (
|
|
114
|
-
id TEXT PRIMARY KEY,
|
|
115
|
-
batch_id TEXT NOT NULL REFERENCES batch(id) ON DELETE CASCADE,
|
|
116
|
-
reason TEXT NOT NULL,
|
|
117
|
-
message TEXT NOT NULL,
|
|
118
|
-
stats_snapshot TEXT NOT NULL, -- JSON of BatchStats
|
|
119
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
CREATE INDEX IF NOT EXISTS idx_interrupt_log_batch_id ON batch_interrupt_log(batch_id);
|
|
123
|
-
`;
|
|
124
|
-
|
|
125
|
-
this.transaction(() => {
|
|
126
|
-
this.db.exec(migration);
|
|
127
|
-
this.db
|
|
128
|
-
.query('INSERT INTO _migration (name) VALUES ($name)')
|
|
129
|
-
.run({ $name: '002_batch_interruption' });
|
|
130
|
-
});
|
|
33
|
+
this.db.run(schema);
|
|
131
34
|
}
|
|
132
35
|
|
|
133
36
|
query<T = unknown>(sql: string, params?: SQLQueryBindings[]): T[] {
|
package/src/database/schema.sql
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
-- TEM Database Schema
|
|
2
2
|
-- SQLite with WAL mode
|
|
3
|
+
-- Complete schema - single source of truth for new databases
|
|
3
4
|
|
|
4
5
|
-- Migration tracking
|
|
5
6
|
CREATE TABLE IF NOT EXISTS _migration (
|
|
@@ -13,9 +14,12 @@ CREATE TABLE IF NOT EXISTS batch (
|
|
|
13
14
|
id TEXT PRIMARY KEY,
|
|
14
15
|
code TEXT NOT NULL UNIQUE,
|
|
15
16
|
type TEXT NOT NULL,
|
|
17
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
18
|
+
CHECK(status IN ('active', 'interrupted', 'completed')),
|
|
16
19
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
17
20
|
completed_at DATETIME,
|
|
18
|
-
metadata TEXT -- JSON object
|
|
21
|
+
metadata TEXT, -- JSON object
|
|
22
|
+
interruption_criteria TEXT -- JSON object
|
|
19
23
|
);
|
|
20
24
|
|
|
21
25
|
-- Task: Individual units of work
|
|
@@ -35,11 +39,23 @@ CREATE TABLE IF NOT EXISTS task (
|
|
|
35
39
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
36
40
|
);
|
|
37
41
|
|
|
42
|
+
-- Interruption log: Records batch interruption events
|
|
43
|
+
CREATE TABLE IF NOT EXISTS batch_interrupt_log (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
batch_id TEXT NOT NULL REFERENCES batch(id) ON DELETE CASCADE,
|
|
46
|
+
reason TEXT NOT NULL,
|
|
47
|
+
message TEXT NOT NULL,
|
|
48
|
+
stats_snapshot TEXT NOT NULL, -- JSON of BatchStats
|
|
49
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
50
|
+
);
|
|
51
|
+
|
|
38
52
|
-- Indexes for performance
|
|
39
53
|
CREATE INDEX IF NOT EXISTS idx_batch_code ON batch(code);
|
|
40
54
|
CREATE INDEX IF NOT EXISTS idx_batch_type ON batch(type);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_batch_status ON batch(status);
|
|
41
56
|
CREATE INDEX IF NOT EXISTS idx_task_batch_id ON task(batch_id);
|
|
42
57
|
CREATE INDEX IF NOT EXISTS idx_task_status ON task(status);
|
|
43
58
|
CREATE INDEX IF NOT EXISTS idx_task_type ON task(type);
|
|
44
59
|
CREATE INDEX IF NOT EXISTS idx_task_claim ON task(status, claimed_at);
|
|
45
60
|
CREATE INDEX IF NOT EXISTS idx_task_pending ON task(status, created_at) WHERE status = 'pending';
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_interrupt_log_batch_id ON batch_interrupt_log(batch_id);
|
package/src/interfaces/index.ts
CHANGED
|
@@ -105,6 +105,9 @@ export interface TEMConfig {
|
|
|
105
105
|
|
|
106
106
|
// Polling
|
|
107
107
|
pollIntervalMs: number;
|
|
108
|
+
|
|
109
|
+
// Default interruption criteria for all batches (batch-level overrides these)
|
|
110
|
+
defaultInterruptionCriteria?: BatchInterruptionCriteria;
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
// ============================================================================
|
|
@@ -13,7 +13,8 @@ export interface BatchInterruptionRow {
|
|
|
13
13
|
export class BatchInterruptionService implements i.BatchInterruptionService {
|
|
14
14
|
constructor(
|
|
15
15
|
private db: Database,
|
|
16
|
-
private batchService: BatchService
|
|
16
|
+
private batchService: BatchService,
|
|
17
|
+
private defaultCriteria?: i.BatchInterruptionCriteria
|
|
17
18
|
) {}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -30,13 +31,18 @@ export class BatchInterruptionService implements i.BatchInterruptionService {
|
|
|
30
31
|
}
|
|
31
32
|
): Promise<boolean> {
|
|
32
33
|
// Fetch batch with its interruption criteria
|
|
33
|
-
const { batch, criteria } = await this.batchService.getWithCriteria(batchId);
|
|
34
|
+
const { batch, criteria: batchCriteria } = await this.batchService.getWithCriteria(batchId);
|
|
34
35
|
|
|
35
36
|
// If already interrupted or completed, no need to check
|
|
36
37
|
if (batch.status !== 'active') {
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Merge criteria: TEM-level (default) overrides batch-level
|
|
42
|
+
const criteria: i.BatchInterruptionCriteria | undefined = batchCriteria || this.defaultCriteria
|
|
43
|
+
? { ...batchCriteria, ...this.defaultCriteria }
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
40
46
|
// If no criteria set, never interrupt
|
|
41
47
|
if (!criteria) {
|
|
42
48
|
return false;
|