@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qianxude/tem",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",
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?: import('../interfaces/index.js').BatchInterruptionReason,
123
+ reason?: BatchInterruptionReason,
120
124
  message?: string
121
125
  ): Promise<void> {
122
126
  await this.interruption.interrupt(
@@ -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.exec('PRAGMA journal_mode = WAL;');
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.exec(`PRAGMA busy_timeout = ${timeout};`);
22
+ this.db.run(`PRAGMA busy_timeout = ${timeout};`);
21
23
 
22
- // Run migrations
23
- this.migrate();
24
+ // Initialize schema
25
+ this.initSchema();
24
26
  }
25
27
 
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
- `);
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
- // Check and apply migrations in order
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[] {
@@ -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);
@@ -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;