station-adapter-sqlite 1.0.2 → 1.0.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.
@@ -1,4 +1,4 @@
1
- import type { BroadcastQueueAdapter, BroadcastRun, BroadcastRunPatch, BroadcastRunStatus, BroadcastNodeRun, BroadcastNodeRunPatch } from "station-broadcast";
1
+ import type { BroadcastQueueAdapter, BroadcastRun, BroadcastRunPatch, BroadcastRunStatus, BroadcastNodeRun, BroadcastNodeRunPatch, DynamicBroadcastSpec } from "station-broadcast";
2
2
  export interface BroadcastSqliteAdapterOptions {
3
3
  dbPath?: string;
4
4
  tableName?: string;
@@ -7,6 +7,7 @@ export declare class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
7
7
  private db;
8
8
  private runsTable;
9
9
  private nodesTable;
10
+ private definitionsTable;
10
11
  constructor(options?: BroadcastSqliteAdapterOptions);
11
12
  addBroadcastRun(run: BroadcastRun): Promise<void>;
12
13
  getBroadcastRun(id: string): Promise<BroadcastRun | null>;
@@ -22,6 +23,11 @@ export declare class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
22
23
  private static readonly NODE_RUN_PATCH_KEYS;
23
24
  updateNodeRun(id: string, patch: BroadcastNodeRunPatch): Promise<void>;
24
25
  getNodeRuns(broadcastRunId: string): Promise<BroadcastNodeRun[]>;
26
+ saveDefinition(spec: DynamicBroadcastSpec): Promise<DynamicBroadcastSpec>;
27
+ getDefinition(name: string, version?: number): Promise<DynamicBroadcastSpec | null>;
28
+ listDefinitions(): Promise<DynamicBroadcastSpec[]>;
29
+ listDefinitionVersions(name: string): Promise<DynamicBroadcastSpec[]>;
30
+ deleteDefinition(name: string): Promise<boolean>;
25
31
  generateId(): string;
26
32
  ping(): Promise<boolean>;
27
33
  close(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../src/broadcast.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,mBAAmB,CAAC;AAgC3B,MAAM,WAAW,6BAA6B;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,sBAAuB,YAAW,qBAAqB;IAClE,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,GAAE,6BAAkC;IA2DjD,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBjD,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAK/D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAG7C;IAEG,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBvE,mBAAmB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAW9C,uBAAuB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAKlD,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAKjE,yBAAyB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IASlG,kBAAkB,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAUpF,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBpD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAK9D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAExC;IAEG,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBtE,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAKtE,UAAU,IAAI,MAAM;IAId,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IASxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
1
+ {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../src/broadcast.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAiC3B,MAAM,WAAW,6BAA6B;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,sBAAuB,YAAW,qBAAqB;IAClE,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,gBAAgB,CAAS;gBAErB,OAAO,GAAE,6BAAkC;IAsFjD,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBjD,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAK/D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAG7C;IAEG,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBvE,mBAAmB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAW9C,uBAAuB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAKlD,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAKjE,yBAAyB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IASlG,kBAAkB,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAUpF,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBpD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAK9D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAExC;IAEG,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBtE,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAOhE,cAAc,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAwCzE,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAcnF,eAAe,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAclD,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAOrE,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBtD,UAAU,IAAI,MAAM;IAId,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IASxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
package/dist/broadcast.js CHANGED
@@ -8,6 +8,7 @@ const { toColumn: toBroadcastRunCol, toField: toBroadcastRunField } = createColu
8
8
  startedAt: "started_at",
9
9
  completedAt: "completed_at",
10
10
  createdAt: "created_at",
11
+ definitionSnapshot: "definition_snapshot",
11
12
  });
12
13
  const BROADCAST_RUN_DATE_FIELDS = new Set(["nextRunAt", "startedAt", "completedAt", "createdAt"]);
13
14
  const { toColumn: toNodeRunCol, toField: toNodeRunField } = createColumnMapper({
@@ -30,29 +31,39 @@ export class BroadcastSqliteAdapter {
30
31
  db;
31
32
  runsTable;
32
33
  nodesTable;
34
+ definitionsTable;
33
35
  constructor(options = {}) {
34
36
  const dbPath = options.dbPath ?? "station.db";
35
37
  this.runsTable = validateTableName(options.tableName ?? "broadcast_runs");
36
38
  this.nodesTable = validateTableName(`${this.runsTable}_nodes`);
39
+ this.definitionsTable = validateTableName(`${this.runsTable}_definitions`);
37
40
  this.db = new Database(dbPath);
38
41
  this.db.pragma("journal_mode = WAL");
39
42
  this.db.pragma("foreign_keys = ON");
40
43
  this.db.exec(`
41
44
  CREATE TABLE IF NOT EXISTS ${this.runsTable} (
42
- id TEXT PRIMARY KEY,
43
- broadcast_name TEXT NOT NULL,
44
- input TEXT NOT NULL,
45
- status TEXT NOT NULL DEFAULT 'pending',
46
- failure_policy TEXT NOT NULL DEFAULT 'fail-fast',
47
- timeout INTEGER,
48
- interval TEXT,
49
- next_run_at TEXT,
50
- started_at TEXT,
51
- completed_at TEXT,
52
- created_at TEXT NOT NULL,
53
- error TEXT
45
+ id TEXT PRIMARY KEY,
46
+ broadcast_name TEXT NOT NULL,
47
+ input TEXT NOT NULL,
48
+ status TEXT NOT NULL DEFAULT 'pending',
49
+ failure_policy TEXT NOT NULL DEFAULT 'fail-fast',
50
+ timeout INTEGER,
51
+ interval TEXT,
52
+ next_run_at TEXT,
53
+ started_at TEXT,
54
+ completed_at TEXT,
55
+ created_at TEXT NOT NULL,
56
+ error TEXT,
57
+ definition_snapshot TEXT
54
58
  )
55
59
  `);
60
+ // Idempotent migration: add column if it's missing (DB existed before this version).
61
+ const existingCols = this.db
62
+ .prepare(`PRAGMA table_info(${this.runsTable})`)
63
+ .all();
64
+ if (!existingCols.some((c) => c.name === "definition_snapshot")) {
65
+ this.db.exec(`ALTER TABLE ${this.runsTable} ADD COLUMN definition_snapshot TEXT`);
66
+ }
56
67
  this.db.exec(`
57
68
  CREATE INDEX IF NOT EXISTS idx_${this.runsTable}_status
58
69
  ON ${this.runsTable} (status, next_run_at)
@@ -80,16 +91,32 @@ export class BroadcastSqliteAdapter {
80
91
  this.db.exec(`
81
92
  CREATE INDEX IF NOT EXISTS idx_${this.nodesTable}_run_id
82
93
  ON ${this.nodesTable} (broadcast_run_id)
94
+ `);
95
+ // Dynamic broadcast definitions — name + version is the identity, full
96
+ // history is retained so deleted/older versions remain inspectable.
97
+ this.db.exec(`
98
+ CREATE TABLE IF NOT EXISTS ${this.definitionsTable} (
99
+ name TEXT NOT NULL,
100
+ version INTEGER NOT NULL,
101
+ spec TEXT NOT NULL,
102
+ failure_policy TEXT NOT NULL,
103
+ timeout INTEGER,
104
+ created_at TEXT NOT NULL,
105
+ updated_at TEXT NOT NULL,
106
+ created_by TEXT,
107
+ deleted_at TEXT,
108
+ PRIMARY KEY (name, version)
109
+ )
83
110
  `);
84
111
  }
85
112
  async addBroadcastRun(run) {
86
113
  this.db.prepare(`
87
114
  INSERT INTO ${this.runsTable}
88
115
  (id, broadcast_name, input, status, failure_policy, timeout, interval,
89
- next_run_at, started_at, completed_at, created_at, error)
116
+ next_run_at, started_at, completed_at, created_at, error, definition_snapshot)
90
117
  VALUES
91
118
  (@id, @broadcast_name, @input, @status, @failure_policy, @timeout, @interval,
92
- @next_run_at, @started_at, @completed_at, @created_at, @error)
119
+ @next_run_at, @started_at, @completed_at, @created_at, @error, @definition_snapshot)
93
120
  `).run({
94
121
  id: run.id,
95
122
  broadcast_name: run.broadcastName,
@@ -103,6 +130,7 @@ export class BroadcastSqliteAdapter {
103
130
  completed_at: dateToStr(run.completedAt),
104
131
  created_at: dateToStr(run.createdAt),
105
132
  error: run.error ?? null,
133
+ definition_snapshot: run.definitionSnapshot ?? null,
106
134
  });
107
135
  }
108
136
  async getBroadcastRun(id) {
@@ -111,7 +139,7 @@ export class BroadcastSqliteAdapter {
111
139
  }
112
140
  static BROADCAST_RUN_PATCH_KEYS = new Set([
113
141
  "input", "status", "failurePolicy", "timeout", "interval", "nextRunAt",
114
- "startedAt", "completedAt", "error",
142
+ "startedAt", "completedAt", "error", "definitionSnapshot",
115
143
  ]);
116
144
  async updateBroadcastRun(id, patch) {
117
145
  const setClauses = [];
@@ -220,6 +248,99 @@ export class BroadcastSqliteAdapter {
220
248
  const rows = this.db.prepare(`SELECT * FROM ${this.nodesTable} WHERE broadcast_run_id = ?`).all(broadcastRunId);
221
249
  return rows.map(rowToNodeRun);
222
250
  }
251
+ // ─── Dynamic broadcast definitions ───────────────────────────────
252
+ async saveDefinition(spec) {
253
+ // better-sqlite3 transactions serialize writers, so the MAX(version) read
254
+ // + INSERT can't be interleaved with another save — no PK collision is
255
+ // possible on the same DB. The transaction wrapper handles SAVEPOINTs
256
+ // for nested calls.
257
+ const txn = this.db.transaction((spec) => {
258
+ const row = this.db
259
+ .prepare(`SELECT MAX(version) AS v FROM ${this.definitionsTable} WHERE name = ?`)
260
+ .get(spec.name);
261
+ const nextVersion = (row?.v ?? 0) + 1;
262
+ const now = new Date();
263
+ const next = {
264
+ ...spec,
265
+ version: nextVersion,
266
+ createdAt: spec.createdAt ?? now,
267
+ updatedAt: now,
268
+ deletedAt: undefined,
269
+ };
270
+ this.db
271
+ .prepare(`
272
+ INSERT INTO ${this.definitionsTable}
273
+ (name, version, spec, failure_policy, timeout, created_at, updated_at, created_by, deleted_at)
274
+ VALUES
275
+ (@name, @version, @spec, @failure_policy, @timeout, @created_at, @updated_at, @created_by, NULL)
276
+ `)
277
+ .run({
278
+ name: next.name,
279
+ version: next.version,
280
+ spec: JSON.stringify(next),
281
+ failure_policy: next.failurePolicy,
282
+ timeout: next.timeout ?? null,
283
+ created_at: dateToStr(next.createdAt),
284
+ updated_at: dateToStr(next.updatedAt),
285
+ created_by: next.createdBy ?? null,
286
+ });
287
+ return next;
288
+ });
289
+ return txn(spec);
290
+ }
291
+ async getDefinition(name, version) {
292
+ let row;
293
+ if (version !== undefined) {
294
+ row = this.db
295
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? AND version = ?`)
296
+ .get(name, version);
297
+ }
298
+ else {
299
+ row = this.db
300
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? ORDER BY version DESC LIMIT 1`)
301
+ .get(name);
302
+ }
303
+ return row ? deserializeSpec(row.spec) : null;
304
+ }
305
+ async listDefinitions() {
306
+ const rows = this.db
307
+ .prepare(`
308
+ SELECT spec FROM ${this.definitionsTable} d1
309
+ WHERE version = (
310
+ SELECT MAX(version) FROM ${this.definitionsTable} d2 WHERE d2.name = d1.name
311
+ )
312
+ AND deleted_at IS NULL
313
+ ORDER BY name ASC
314
+ `)
315
+ .all();
316
+ return rows.map((r) => deserializeSpec(r.spec));
317
+ }
318
+ async listDefinitionVersions(name) {
319
+ const rows = this.db
320
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? ORDER BY version DESC`)
321
+ .all(name);
322
+ return rows.map((r) => deserializeSpec(r.spec));
323
+ }
324
+ async deleteDefinition(name) {
325
+ const row = this.db
326
+ .prepare(`SELECT MAX(version) AS v FROM ${this.definitionsTable} WHERE name = ? AND deleted_at IS NULL`)
327
+ .get(name);
328
+ if (!row?.v)
329
+ return false;
330
+ const now = new Date().toISOString();
331
+ // Update the spec JSON with the deletion timestamp so consumers see it.
332
+ const existing = this.db
333
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? AND version = ?`)
334
+ .get(name, row.v);
335
+ if (!existing)
336
+ return false;
337
+ const spec = deserializeSpec(existing.spec);
338
+ spec.deletedAt = new Date(now);
339
+ this.db
340
+ .prepare(`UPDATE ${this.definitionsTable} SET deleted_at = ?, spec = ? WHERE name = ? AND version = ?`)
341
+ .run(now, JSON.stringify(spec), name, row.v);
342
+ return true;
343
+ }
223
344
  generateId() {
224
345
  return randomUUID();
225
346
  }
@@ -236,4 +357,15 @@ export class BroadcastSqliteAdapter {
236
357
  this.db.close();
237
358
  }
238
359
  }
360
+ function deserializeSpec(json) {
361
+ const obj = JSON.parse(json);
362
+ // Revive Date fields
363
+ if (obj.createdAt)
364
+ obj.createdAt = new Date(obj.createdAt);
365
+ if (obj.updatedAt)
366
+ obj.updatedAt = new Date(obj.updatedAt);
367
+ if (obj.deletedAt)
368
+ obj.deletedAt = new Date(obj.deletedAt);
369
+ return obj;
370
+ }
239
371
  //# sourceMappingURL=broadcast.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../src/broadcast.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../src/broadcast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Schedule, SchedulePatch, ScheduleAdapter, ScheduleListFilter } from "station-schedules";
2
+ export interface ScheduleSqliteAdapterOptions {
3
+ dbPath?: string;
4
+ tableName?: string;
5
+ }
6
+ export declare class ScheduleSqliteAdapter implements ScheduleAdapter {
7
+ private db;
8
+ private table;
9
+ constructor(options?: ScheduleSqliteAdapterOptions);
10
+ add(schedule: Schedule): Promise<void>;
11
+ get(id: string): Promise<Schedule | null>;
12
+ list(filter?: ScheduleListFilter): Promise<Schedule[]>;
13
+ update(id: string, patch: SchedulePatch): Promise<void>;
14
+ delete(id: string): Promise<boolean>;
15
+ /**
16
+ * Atomic claim — only update if the schedule's nextRunAt is still what we
17
+ * expected. Prevents two runners from double-firing the same schedule.
18
+ */
19
+ claimDue(id: string, expectedNextRunAt: Date, newNextRunAt: Date): Promise<boolean>;
20
+ generateId(): string;
21
+ ping(): Promise<boolean>;
22
+ close(): Promise<void>;
23
+ }
24
+ //# sourceMappingURL=schedules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedules.d.ts","sourceRoot":"","sources":["../src/schedules.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,QAAQ,EACR,aAAa,EACb,eAAe,EACf,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAG3B,MAAM,WAAW,4BAA4B;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,KAAK,CAAS;gBAEV,OAAO,GAAE,4BAAiC;IA8BhD,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BtC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAOzC,IAAI,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAqBtD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA6CvD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK1C;;;OAGG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAczF,UAAU,IAAI,MAAM;IAId,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IASxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,185 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import Database from "better-sqlite3";
3
+ import { validateTableName, dateToStr, strToDate } from "./shared.js";
4
+ export class ScheduleSqliteAdapter {
5
+ db;
6
+ table;
7
+ constructor(options = {}) {
8
+ const dbPath = options.dbPath ?? "station.db";
9
+ this.table = validateTableName(options.tableName ?? "schedules");
10
+ this.db = new Database(dbPath);
11
+ this.db.pragma("journal_mode = WAL");
12
+ this.db.exec(`
13
+ CREATE TABLE IF NOT EXISTS ${this.table} (
14
+ id TEXT PRIMARY KEY,
15
+ kind TEXT NOT NULL,
16
+ target TEXT NOT NULL,
17
+ interval TEXT NOT NULL,
18
+ input TEXT,
19
+ enabled INTEGER NOT NULL DEFAULT 1,
20
+ next_run_at TEXT NOT NULL,
21
+ last_run_at TEXT,
22
+ last_run_status TEXT,
23
+ last_run_id TEXT,
24
+ created_at TEXT NOT NULL,
25
+ updated_at TEXT NOT NULL,
26
+ created_by TEXT
27
+ )
28
+ `);
29
+ this.db.exec(`
30
+ CREATE INDEX IF NOT EXISTS idx_${this.table}_due
31
+ ON ${this.table} (enabled, next_run_at)
32
+ `);
33
+ }
34
+ async add(schedule) {
35
+ this.db.prepare(`
36
+ INSERT INTO ${this.table}
37
+ (id, kind, target, interval, input, enabled, next_run_at,
38
+ last_run_at, last_run_status, last_run_id,
39
+ created_at, updated_at, created_by)
40
+ VALUES
41
+ (@id, @kind, @target, @interval, @input, @enabled, @next_run_at,
42
+ @last_run_at, @last_run_status, @last_run_id,
43
+ @created_at, @updated_at, @created_by)
44
+ `).run({
45
+ id: schedule.id,
46
+ kind: schedule.kind,
47
+ target: schedule.target,
48
+ interval: schedule.interval,
49
+ input: schedule.input !== undefined ? JSON.stringify(schedule.input) : null,
50
+ enabled: schedule.enabled ? 1 : 0,
51
+ next_run_at: dateToStr(schedule.nextRunAt),
52
+ last_run_at: dateToStr(schedule.lastRunAt),
53
+ last_run_status: schedule.lastRunStatus ?? null,
54
+ last_run_id: schedule.lastRunId ?? null,
55
+ created_at: dateToStr(schedule.createdAt),
56
+ updated_at: dateToStr(schedule.updatedAt),
57
+ created_by: schedule.createdBy ?? null,
58
+ });
59
+ }
60
+ async get(id) {
61
+ const row = this.db.prepare(`SELECT * FROM ${this.table} WHERE id = ?`).get(id);
62
+ return row ? rowToSchedule(row) : null;
63
+ }
64
+ async list(filter) {
65
+ const conditions = [];
66
+ const params = [];
67
+ if (filter?.kind) {
68
+ conditions.push("kind = ?");
69
+ params.push(filter.kind);
70
+ }
71
+ if (filter?.enabled !== undefined) {
72
+ conditions.push("enabled = ?");
73
+ params.push(filter.enabled ? 1 : 0);
74
+ }
75
+ if (filter?.due) {
76
+ conditions.push("enabled = 1");
77
+ conditions.push("next_run_at <= ?");
78
+ params.push(new Date().toISOString());
79
+ }
80
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
81
+ const rows = this.db.prepare(`SELECT * FROM ${this.table} ${where} ORDER BY next_run_at ASC`).all(...params);
82
+ return rows.map(rowToSchedule);
83
+ }
84
+ async update(id, patch) {
85
+ const setClauses = [];
86
+ const values = { id };
87
+ const map = {
88
+ interval: "interval",
89
+ input: "input",
90
+ enabled: "enabled",
91
+ nextRunAt: "next_run_at",
92
+ lastRunAt: "last_run_at",
93
+ lastRunStatus: "last_run_status",
94
+ lastRunId: "last_run_id",
95
+ updatedAt: "updated_at",
96
+ createdBy: "created_by",
97
+ };
98
+ let touched = false;
99
+ for (const [key, value] of Object.entries(patch)) {
100
+ const col = map[key];
101
+ if (!col)
102
+ continue;
103
+ touched = true;
104
+ const param = `p_${col}`;
105
+ setClauses.push(`${col} = @${param}`);
106
+ if (value === undefined) {
107
+ values[param] = null;
108
+ }
109
+ else if (key === "input") {
110
+ values[param] = JSON.stringify(value);
111
+ }
112
+ else if (key === "enabled") {
113
+ values[param] = value ? 1 : 0;
114
+ }
115
+ else if (key === "nextRunAt" || key === "lastRunAt" || key === "updatedAt") {
116
+ values[param] = dateToStr(value);
117
+ }
118
+ else {
119
+ values[param] = value;
120
+ }
121
+ }
122
+ // Always bump updated_at on update
123
+ if (touched && !("updatedAt" in patch)) {
124
+ setClauses.push("updated_at = @p_updated_at");
125
+ values.p_updated_at = new Date().toISOString();
126
+ }
127
+ if (setClauses.length === 0)
128
+ return;
129
+ this.db.prepare(`UPDATE ${this.table} SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
130
+ }
131
+ async delete(id) {
132
+ const result = this.db.prepare(`DELETE FROM ${this.table} WHERE id = ?`).run(id);
133
+ return result.changes > 0;
134
+ }
135
+ /**
136
+ * Atomic claim — only update if the schedule's nextRunAt is still what we
137
+ * expected. Prevents two runners from double-firing the same schedule.
138
+ */
139
+ async claimDue(id, expectedNextRunAt, newNextRunAt) {
140
+ const result = this.db
141
+ .prepare(`UPDATE ${this.table}
142
+ SET next_run_at = @new_next, updated_at = @now
143
+ WHERE id = @id AND next_run_at = @expected AND enabled = 1`)
144
+ .run({
145
+ id,
146
+ new_next: dateToStr(newNextRunAt),
147
+ expected: dateToStr(expectedNextRunAt),
148
+ now: new Date().toISOString(),
149
+ });
150
+ return result.changes > 0;
151
+ }
152
+ generateId() {
153
+ return randomUUID();
154
+ }
155
+ async ping() {
156
+ try {
157
+ this.db.prepare("SELECT 1").get();
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ async close() {
165
+ this.db.close();
166
+ }
167
+ }
168
+ function rowToSchedule(row) {
169
+ return {
170
+ id: row.id,
171
+ kind: row.kind,
172
+ target: row.target,
173
+ interval: row.interval,
174
+ input: row.input ? JSON.parse(row.input) : undefined,
175
+ enabled: Boolean(row.enabled),
176
+ nextRunAt: strToDate(row.next_run_at),
177
+ lastRunAt: strToDate(row.last_run_at),
178
+ lastRunStatus: row.last_run_status ?? undefined,
179
+ lastRunId: row.last_run_id ?? undefined,
180
+ createdAt: strToDate(row.created_at),
181
+ updatedAt: strToDate(row.updated_at),
182
+ createdBy: row.created_by ?? undefined,
183
+ };
184
+ }
185
+ //# sourceMappingURL=schedules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedules.js","sourceRoot":"","sources":["../src/schedules.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "station-adapter-sqlite",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "SQLite adapter for station-signal using better-sqlite3",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,6 +14,11 @@
14
14
  "types": "./dist/broadcast.d.ts",
15
15
  "import": "./dist/broadcast.js",
16
16
  "default": "./dist/broadcast.js"
17
+ },
18
+ "./schedules": {
19
+ "types": "./dist/schedules.d.ts",
20
+ "import": "./dist/schedules.js",
21
+ "default": "./dist/schedules.js"
17
22
  }
18
23
  },
19
24
  "files": [
@@ -32,11 +37,18 @@
32
37
  "@types/better-sqlite3": "^7.6.13"
33
38
  },
34
39
  "peerDependencies": {
35
- "station-signal": "1.0.2",
36
- "station-broadcast": "1.0.2"
40
+ "station-signal": "1.0.4",
41
+ "station-broadcast": "1.0.4",
42
+ "station-schedules": "1.0.4"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "station-schedules": {
46
+ "optional": true
47
+ }
37
48
  },
38
49
  "scripts": {
39
50
  "build": "tsc",
40
- "typecheck": "tsc --noEmit"
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "node --import tsx --test test/*.test.ts"
41
53
  }
42
54
  }
package/src/broadcast.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  BroadcastRunStatus,
8
8
  BroadcastNodeRun,
9
9
  BroadcastNodeRunPatch,
10
+ DynamicBroadcastSpec,
10
11
  } from "station-broadcast";
11
12
 
12
13
  import { validateTableName, dateToStr, createColumnMapper, rowToObject } from "./shared.js";
@@ -18,6 +19,7 @@ const { toColumn: toBroadcastRunCol, toField: toBroadcastRunField } = createColu
18
19
  startedAt: "started_at",
19
20
  completedAt: "completed_at",
20
21
  createdAt: "created_at",
22
+ definitionSnapshot: "definition_snapshot",
21
23
  });
22
24
  const BROADCAST_RUN_DATE_FIELDS = new Set(["nextRunAt", "startedAt", "completedAt", "createdAt"]);
23
25
 
@@ -48,11 +50,13 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
48
50
  private db: Database.Database;
49
51
  private runsTable: string;
50
52
  private nodesTable: string;
53
+ private definitionsTable: string;
51
54
 
52
55
  constructor(options: BroadcastSqliteAdapterOptions = {}) {
53
56
  const dbPath = options.dbPath ?? "station.db";
54
57
  this.runsTable = validateTableName(options.tableName ?? "broadcast_runs");
55
58
  this.nodesTable = validateTableName(`${this.runsTable}_nodes`);
59
+ this.definitionsTable = validateTableName(`${this.runsTable}_definitions`);
56
60
  this.db = new Database(dbPath);
57
61
 
58
62
  this.db.pragma("journal_mode = WAL");
@@ -60,21 +64,30 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
60
64
 
61
65
  this.db.exec(`
62
66
  CREATE TABLE IF NOT EXISTS ${this.runsTable} (
63
- id TEXT PRIMARY KEY,
64
- broadcast_name TEXT NOT NULL,
65
- input TEXT NOT NULL,
66
- status TEXT NOT NULL DEFAULT 'pending',
67
- failure_policy TEXT NOT NULL DEFAULT 'fail-fast',
68
- timeout INTEGER,
69
- interval TEXT,
70
- next_run_at TEXT,
71
- started_at TEXT,
72
- completed_at TEXT,
73
- created_at TEXT NOT NULL,
74
- error TEXT
67
+ id TEXT PRIMARY KEY,
68
+ broadcast_name TEXT NOT NULL,
69
+ input TEXT NOT NULL,
70
+ status TEXT NOT NULL DEFAULT 'pending',
71
+ failure_policy TEXT NOT NULL DEFAULT 'fail-fast',
72
+ timeout INTEGER,
73
+ interval TEXT,
74
+ next_run_at TEXT,
75
+ started_at TEXT,
76
+ completed_at TEXT,
77
+ created_at TEXT NOT NULL,
78
+ error TEXT,
79
+ definition_snapshot TEXT
75
80
  )
76
81
  `);
77
82
 
83
+ // Idempotent migration: add column if it's missing (DB existed before this version).
84
+ const existingCols = this.db
85
+ .prepare(`PRAGMA table_info(${this.runsTable})`)
86
+ .all() as Array<{ name: string }>;
87
+ if (!existingCols.some((c) => c.name === "definition_snapshot")) {
88
+ this.db.exec(`ALTER TABLE ${this.runsTable} ADD COLUMN definition_snapshot TEXT`);
89
+ }
90
+
78
91
  this.db.exec(`
79
92
  CREATE INDEX IF NOT EXISTS idx_${this.runsTable}_status
80
93
  ON ${this.runsTable} (status, next_run_at)
@@ -106,16 +119,33 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
106
119
  CREATE INDEX IF NOT EXISTS idx_${this.nodesTable}_run_id
107
120
  ON ${this.nodesTable} (broadcast_run_id)
108
121
  `);
122
+
123
+ // Dynamic broadcast definitions — name + version is the identity, full
124
+ // history is retained so deleted/older versions remain inspectable.
125
+ this.db.exec(`
126
+ CREATE TABLE IF NOT EXISTS ${this.definitionsTable} (
127
+ name TEXT NOT NULL,
128
+ version INTEGER NOT NULL,
129
+ spec TEXT NOT NULL,
130
+ failure_policy TEXT NOT NULL,
131
+ timeout INTEGER,
132
+ created_at TEXT NOT NULL,
133
+ updated_at TEXT NOT NULL,
134
+ created_by TEXT,
135
+ deleted_at TEXT,
136
+ PRIMARY KEY (name, version)
137
+ )
138
+ `);
109
139
  }
110
140
 
111
141
  async addBroadcastRun(run: BroadcastRun): Promise<void> {
112
142
  this.db.prepare(`
113
143
  INSERT INTO ${this.runsTable}
114
144
  (id, broadcast_name, input, status, failure_policy, timeout, interval,
115
- next_run_at, started_at, completed_at, created_at, error)
145
+ next_run_at, started_at, completed_at, created_at, error, definition_snapshot)
116
146
  VALUES
117
147
  (@id, @broadcast_name, @input, @status, @failure_policy, @timeout, @interval,
118
- @next_run_at, @started_at, @completed_at, @created_at, @error)
148
+ @next_run_at, @started_at, @completed_at, @created_at, @error, @definition_snapshot)
119
149
  `).run({
120
150
  id: run.id,
121
151
  broadcast_name: run.broadcastName,
@@ -129,6 +159,7 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
129
159
  completed_at: dateToStr(run.completedAt),
130
160
  created_at: dateToStr(run.createdAt),
131
161
  error: run.error ?? null,
162
+ definition_snapshot: run.definitionSnapshot ?? null,
132
163
  });
133
164
  }
134
165
 
@@ -139,7 +170,7 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
139
170
 
140
171
  private static readonly BROADCAST_RUN_PATCH_KEYS = new Set([
141
172
  "input", "status", "failurePolicy", "timeout", "interval", "nextRunAt",
142
- "startedAt", "completedAt", "error",
173
+ "startedAt", "completedAt", "error", "definitionSnapshot",
143
174
  ]);
144
175
 
145
176
  async updateBroadcastRun(id: string, patch: BroadcastRunPatch): Promise<void> {
@@ -260,6 +291,102 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
260
291
  return rows.map(rowToNodeRun);
261
292
  }
262
293
 
294
+ // ─── Dynamic broadcast definitions ───────────────────────────────
295
+
296
+ async saveDefinition(spec: DynamicBroadcastSpec): Promise<DynamicBroadcastSpec> {
297
+ // better-sqlite3 transactions serialize writers, so the MAX(version) read
298
+ // + INSERT can't be interleaved with another save — no PK collision is
299
+ // possible on the same DB. The transaction wrapper handles SAVEPOINTs
300
+ // for nested calls.
301
+ const txn = this.db.transaction((spec: DynamicBroadcastSpec): DynamicBroadcastSpec => {
302
+ const row = this.db
303
+ .prepare(`SELECT MAX(version) AS v FROM ${this.definitionsTable} WHERE name = ?`)
304
+ .get(spec.name) as { v: number | null } | undefined;
305
+ const nextVersion = (row?.v ?? 0) + 1;
306
+ const now = new Date();
307
+ const next: DynamicBroadcastSpec = {
308
+ ...spec,
309
+ version: nextVersion,
310
+ createdAt: spec.createdAt ?? now,
311
+ updatedAt: now,
312
+ deletedAt: undefined,
313
+ };
314
+ this.db
315
+ .prepare(`
316
+ INSERT INTO ${this.definitionsTable}
317
+ (name, version, spec, failure_policy, timeout, created_at, updated_at, created_by, deleted_at)
318
+ VALUES
319
+ (@name, @version, @spec, @failure_policy, @timeout, @created_at, @updated_at, @created_by, NULL)
320
+ `)
321
+ .run({
322
+ name: next.name,
323
+ version: next.version,
324
+ spec: JSON.stringify(next),
325
+ failure_policy: next.failurePolicy,
326
+ timeout: next.timeout ?? null,
327
+ created_at: dateToStr(next.createdAt),
328
+ updated_at: dateToStr(next.updatedAt),
329
+ created_by: next.createdBy ?? null,
330
+ });
331
+ return next;
332
+ });
333
+ return txn(spec);
334
+ }
335
+
336
+ async getDefinition(name: string, version?: number): Promise<DynamicBroadcastSpec | null> {
337
+ let row: { spec: string } | undefined;
338
+ if (version !== undefined) {
339
+ row = this.db
340
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? AND version = ?`)
341
+ .get(name, version) as { spec: string } | undefined;
342
+ } else {
343
+ row = this.db
344
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? ORDER BY version DESC LIMIT 1`)
345
+ .get(name) as { spec: string } | undefined;
346
+ }
347
+ return row ? deserializeSpec(row.spec) : null;
348
+ }
349
+
350
+ async listDefinitions(): Promise<DynamicBroadcastSpec[]> {
351
+ const rows = this.db
352
+ .prepare(`
353
+ SELECT spec FROM ${this.definitionsTable} d1
354
+ WHERE version = (
355
+ SELECT MAX(version) FROM ${this.definitionsTable} d2 WHERE d2.name = d1.name
356
+ )
357
+ AND deleted_at IS NULL
358
+ ORDER BY name ASC
359
+ `)
360
+ .all() as Array<{ spec: string }>;
361
+ return rows.map((r) => deserializeSpec(r.spec));
362
+ }
363
+
364
+ async listDefinitionVersions(name: string): Promise<DynamicBroadcastSpec[]> {
365
+ const rows = this.db
366
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? ORDER BY version DESC`)
367
+ .all(name) as Array<{ spec: string }>;
368
+ return rows.map((r) => deserializeSpec(r.spec));
369
+ }
370
+
371
+ async deleteDefinition(name: string): Promise<boolean> {
372
+ const row = this.db
373
+ .prepare(`SELECT MAX(version) AS v FROM ${this.definitionsTable} WHERE name = ? AND deleted_at IS NULL`)
374
+ .get(name) as { v: number | null } | undefined;
375
+ if (!row?.v) return false;
376
+ const now = new Date().toISOString();
377
+ // Update the spec JSON with the deletion timestamp so consumers see it.
378
+ const existing = this.db
379
+ .prepare(`SELECT spec FROM ${this.definitionsTable} WHERE name = ? AND version = ?`)
380
+ .get(name, row.v) as { spec: string } | undefined;
381
+ if (!existing) return false;
382
+ const spec = deserializeSpec(existing.spec);
383
+ spec.deletedAt = new Date(now);
384
+ this.db
385
+ .prepare(`UPDATE ${this.definitionsTable} SET deleted_at = ?, spec = ? WHERE name = ? AND version = ?`)
386
+ .run(now, JSON.stringify(spec), name, row.v);
387
+ return true;
388
+ }
389
+
263
390
  generateId(): string {
264
391
  return randomUUID();
265
392
  }
@@ -277,3 +404,12 @@ export class BroadcastSqliteAdapter implements BroadcastQueueAdapter {
277
404
  this.db.close();
278
405
  }
279
406
  }
407
+
408
+ function deserializeSpec(json: string): DynamicBroadcastSpec {
409
+ const obj = JSON.parse(json) as DynamicBroadcastSpec;
410
+ // Revive Date fields
411
+ if (obj.createdAt) obj.createdAt = new Date(obj.createdAt);
412
+ if (obj.updatedAt) obj.updatedAt = new Date(obj.updatedAt);
413
+ if (obj.deletedAt) obj.deletedAt = new Date(obj.deletedAt);
414
+ return obj;
415
+ }
@@ -0,0 +1,207 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import Database from "better-sqlite3";
3
+ import type {
4
+ Schedule,
5
+ SchedulePatch,
6
+ ScheduleAdapter,
7
+ ScheduleListFilter,
8
+ } from "station-schedules";
9
+ import { validateTableName, dateToStr, strToDate } from "./shared.js";
10
+
11
+ export interface ScheduleSqliteAdapterOptions {
12
+ dbPath?: string;
13
+ tableName?: string;
14
+ }
15
+
16
+ export class ScheduleSqliteAdapter implements ScheduleAdapter {
17
+ private db: Database.Database;
18
+ private table: string;
19
+
20
+ constructor(options: ScheduleSqliteAdapterOptions = {}) {
21
+ const dbPath = options.dbPath ?? "station.db";
22
+ this.table = validateTableName(options.tableName ?? "schedules");
23
+ this.db = new Database(dbPath);
24
+ this.db.pragma("journal_mode = WAL");
25
+
26
+ this.db.exec(`
27
+ CREATE TABLE IF NOT EXISTS ${this.table} (
28
+ id TEXT PRIMARY KEY,
29
+ kind TEXT NOT NULL,
30
+ target TEXT NOT NULL,
31
+ interval TEXT NOT NULL,
32
+ input TEXT,
33
+ enabled INTEGER NOT NULL DEFAULT 1,
34
+ next_run_at TEXT NOT NULL,
35
+ last_run_at TEXT,
36
+ last_run_status TEXT,
37
+ last_run_id TEXT,
38
+ created_at TEXT NOT NULL,
39
+ updated_at TEXT NOT NULL,
40
+ created_by TEXT
41
+ )
42
+ `);
43
+
44
+ this.db.exec(`
45
+ CREATE INDEX IF NOT EXISTS idx_${this.table}_due
46
+ ON ${this.table} (enabled, next_run_at)
47
+ `);
48
+ }
49
+
50
+ async add(schedule: Schedule): Promise<void> {
51
+ this.db.prepare(`
52
+ INSERT INTO ${this.table}
53
+ (id, kind, target, interval, input, enabled, next_run_at,
54
+ last_run_at, last_run_status, last_run_id,
55
+ created_at, updated_at, created_by)
56
+ VALUES
57
+ (@id, @kind, @target, @interval, @input, @enabled, @next_run_at,
58
+ @last_run_at, @last_run_status, @last_run_id,
59
+ @created_at, @updated_at, @created_by)
60
+ `).run({
61
+ id: schedule.id,
62
+ kind: schedule.kind,
63
+ target: schedule.target,
64
+ interval: schedule.interval,
65
+ input: schedule.input !== undefined ? JSON.stringify(schedule.input) : null,
66
+ enabled: schedule.enabled ? 1 : 0,
67
+ next_run_at: dateToStr(schedule.nextRunAt),
68
+ last_run_at: dateToStr(schedule.lastRunAt),
69
+ last_run_status: schedule.lastRunStatus ?? null,
70
+ last_run_id: schedule.lastRunId ?? null,
71
+ created_at: dateToStr(schedule.createdAt),
72
+ updated_at: dateToStr(schedule.updatedAt),
73
+ created_by: schedule.createdBy ?? null,
74
+ });
75
+ }
76
+
77
+ async get(id: string): Promise<Schedule | null> {
78
+ const row = this.db.prepare(`SELECT * FROM ${this.table} WHERE id = ?`).get(id) as
79
+ | Record<string, unknown>
80
+ | undefined;
81
+ return row ? rowToSchedule(row) : null;
82
+ }
83
+
84
+ async list(filter?: ScheduleListFilter): Promise<Schedule[]> {
85
+ const conditions: string[] = [];
86
+ const params: unknown[] = [];
87
+ if (filter?.kind) {
88
+ conditions.push("kind = ?");
89
+ params.push(filter.kind);
90
+ }
91
+ if (filter?.enabled !== undefined) {
92
+ conditions.push("enabled = ?");
93
+ params.push(filter.enabled ? 1 : 0);
94
+ }
95
+ if (filter?.due) {
96
+ conditions.push("enabled = 1");
97
+ conditions.push("next_run_at <= ?");
98
+ params.push(new Date().toISOString());
99
+ }
100
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
101
+ const rows = this.db.prepare(`SELECT * FROM ${this.table} ${where} ORDER BY next_run_at ASC`).all(...params) as Record<string, unknown>[];
102
+ return rows.map(rowToSchedule);
103
+ }
104
+
105
+ async update(id: string, patch: SchedulePatch): Promise<void> {
106
+ const setClauses: string[] = [];
107
+ const values: Record<string, unknown> = { id };
108
+ const map: Record<string, string> = {
109
+ interval: "interval",
110
+ input: "input",
111
+ enabled: "enabled",
112
+ nextRunAt: "next_run_at",
113
+ lastRunAt: "last_run_at",
114
+ lastRunStatus: "last_run_status",
115
+ lastRunId: "last_run_id",
116
+ updatedAt: "updated_at",
117
+ createdBy: "created_by",
118
+ };
119
+
120
+ let touched = false;
121
+ for (const [key, value] of Object.entries(patch)) {
122
+ const col = map[key];
123
+ if (!col) continue;
124
+ touched = true;
125
+ const param = `p_${col}`;
126
+ setClauses.push(`${col} = @${param}`);
127
+ if (value === undefined) {
128
+ values[param] = null;
129
+ } else if (key === "input") {
130
+ values[param] = JSON.stringify(value);
131
+ } else if (key === "enabled") {
132
+ values[param] = value ? 1 : 0;
133
+ } else if (key === "nextRunAt" || key === "lastRunAt" || key === "updatedAt") {
134
+ values[param] = dateToStr(value);
135
+ } else {
136
+ values[param] = value;
137
+ }
138
+ }
139
+
140
+ // Always bump updated_at on update
141
+ if (touched && !("updatedAt" in patch)) {
142
+ setClauses.push("updated_at = @p_updated_at");
143
+ values.p_updated_at = new Date().toISOString();
144
+ }
145
+
146
+ if (setClauses.length === 0) return;
147
+ this.db.prepare(`UPDATE ${this.table} SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
148
+ }
149
+
150
+ async delete(id: string): Promise<boolean> {
151
+ const result = this.db.prepare(`DELETE FROM ${this.table} WHERE id = ?`).run(id);
152
+ return result.changes > 0;
153
+ }
154
+
155
+ /**
156
+ * Atomic claim — only update if the schedule's nextRunAt is still what we
157
+ * expected. Prevents two runners from double-firing the same schedule.
158
+ */
159
+ async claimDue(id: string, expectedNextRunAt: Date, newNextRunAt: Date): Promise<boolean> {
160
+ const result = this.db
161
+ .prepare(`UPDATE ${this.table}
162
+ SET next_run_at = @new_next, updated_at = @now
163
+ WHERE id = @id AND next_run_at = @expected AND enabled = 1`)
164
+ .run({
165
+ id,
166
+ new_next: dateToStr(newNextRunAt),
167
+ expected: dateToStr(expectedNextRunAt),
168
+ now: new Date().toISOString(),
169
+ });
170
+ return result.changes > 0;
171
+ }
172
+
173
+ generateId(): string {
174
+ return randomUUID();
175
+ }
176
+
177
+ async ping(): Promise<boolean> {
178
+ try {
179
+ this.db.prepare("SELECT 1").get();
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ async close(): Promise<void> {
187
+ this.db.close();
188
+ }
189
+ }
190
+
191
+ function rowToSchedule(row: Record<string, unknown>): Schedule {
192
+ return {
193
+ id: row.id as string,
194
+ kind: row.kind as Schedule["kind"],
195
+ target: row.target as string,
196
+ interval: row.interval as string,
197
+ input: row.input ? JSON.parse(row.input as string) : undefined,
198
+ enabled: Boolean(row.enabled),
199
+ nextRunAt: strToDate(row.next_run_at)!,
200
+ lastRunAt: strToDate(row.last_run_at),
201
+ lastRunStatus: (row.last_run_status as string | null) ?? undefined,
202
+ lastRunId: (row.last_run_id as string | null) ?? undefined,
203
+ createdAt: strToDate(row.created_at)!,
204
+ updatedAt: strToDate(row.updated_at)!,
205
+ createdBy: (row.created_by as string | null) ?? undefined,
206
+ };
207
+ }