local-lite 0.0.19 → 0.0.20

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.
Files changed (63) hide show
  1. package/README.md +5 -1
  2. package/dist/actions/store.js +1 -1
  3. package/dist/core/client.d.ts +15 -0
  4. package/dist/core/client.js +46 -2
  5. package/dist/index.d.ts +5 -0
  6. package/dist/index.js +5 -0
  7. package/dist/keys/generator.d.ts +38 -0
  8. package/dist/keys/generator.js +136 -0
  9. package/dist/keys/index.d.ts +5 -0
  10. package/dist/keys/index.js +21 -0
  11. package/dist/keys/manager.d.ts +59 -0
  12. package/dist/keys/manager.js +127 -0
  13. package/dist/keys/signed-token.d.ts +18 -0
  14. package/dist/keys/signed-token.js +171 -0
  15. package/dist/keys/store.d.ts +23 -0
  16. package/dist/keys/store.js +109 -0
  17. package/dist/keys/types.d.ts +95 -0
  18. package/dist/keys/types.js +2 -0
  19. package/dist/kv/index.d.ts +3 -0
  20. package/dist/kv/index.js +19 -0
  21. package/dist/kv/manager.d.ts +52 -0
  22. package/dist/kv/manager.js +164 -0
  23. package/dist/kv/store.d.ts +13 -0
  24. package/dist/kv/store.js +85 -0
  25. package/dist/kv/types.d.ts +27 -0
  26. package/dist/kv/types.js +2 -0
  27. package/dist/react/index.d.ts +2 -0
  28. package/dist/react/index.js +2 -0
  29. package/dist/react/kv.d.ts +9 -0
  30. package/dist/react/kv.js +53 -0
  31. package/dist/react/search.d.ts +19 -0
  32. package/dist/react/search.js +49 -0
  33. package/dist/reactive/event-emitter.d.ts +1 -0
  34. package/dist/reactive/event-emitter.js +9 -6
  35. package/dist/scheduler/cron.d.ts +6 -0
  36. package/dist/scheduler/cron.js +62 -0
  37. package/dist/scheduler/index.d.ts +4 -0
  38. package/dist/scheduler/index.js +20 -0
  39. package/dist/scheduler/manager.d.ts +59 -0
  40. package/dist/scheduler/manager.js +186 -0
  41. package/dist/scheduler/store.d.ts +20 -0
  42. package/dist/scheduler/store.js +134 -0
  43. package/dist/scheduler/types.d.ts +44 -0
  44. package/dist/scheduler/types.js +2 -0
  45. package/dist/search/index.d.ts +3 -0
  46. package/dist/search/index.js +19 -0
  47. package/dist/search/manager.d.ts +42 -0
  48. package/dist/search/manager.js +123 -0
  49. package/dist/search/store.d.ts +33 -0
  50. package/dist/search/store.js +137 -0
  51. package/dist/search/types.d.ts +49 -0
  52. package/dist/search/types.js +2 -0
  53. package/dist/storage/driver.d.ts +25 -0
  54. package/dist/storage/driver.js +92 -0
  55. package/dist/storage/index.d.ts +4 -0
  56. package/dist/storage/index.js +20 -0
  57. package/dist/storage/manager.d.ts +48 -0
  58. package/dist/storage/manager.js +149 -0
  59. package/dist/storage/store.d.ts +14 -0
  60. package/dist/storage/store.js +116 -0
  61. package/dist/storage/types.d.ts +44 -0
  62. package/dist/storage/types.js +2 -0
  63. package/package.json +26 -1
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KVStore = exports.DEFAULT_KV_TABLE = void 0;
4
+ exports.mapKVRow = mapKVRow;
5
+ exports.DEFAULT_KV_TABLE = '_local_kv';
6
+ function mapKVRow(r) {
7
+ let val = r.value;
8
+ const type = r.type;
9
+ if (type === 'number') {
10
+ val = Number(val);
11
+ }
12
+ else if (type === 'boolean') {
13
+ val = val === 'true' || val === '1' || val === 1;
14
+ }
15
+ else if (type === 'json') {
16
+ try {
17
+ val = JSON.parse(val);
18
+ }
19
+ catch {
20
+ // fallback
21
+ }
22
+ }
23
+ return {
24
+ key: r.key,
25
+ value: val,
26
+ type: r.type,
27
+ createdAt: Number(r.created_at || 0),
28
+ updatedAt: Number(r.updated_at || 0),
29
+ expiresAt: r.expires_at != null ? Number(r.expires_at) : null,
30
+ };
31
+ }
32
+ class KVStore {
33
+ static async ensureSchema(db, tableName = exports.DEFAULT_KV_TABLE) {
34
+ await db.execute(`CREATE TABLE IF NOT EXISTS ${tableName} (
35
+ key TEXT PRIMARY KEY,
36
+ value TEXT NOT NULL,
37
+ type TEXT NOT NULL,
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL,
40
+ expires_at INTEGER
41
+ );`);
42
+ await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_expires ON ${tableName} (expires_at);`);
43
+ }
44
+ static async set(db, tableName = exports.DEFAULT_KV_TABLE, key, valueStr, type, now, expiresAt) {
45
+ await db.execute(`INSERT INTO ${tableName} (key, value, type, created_at, updated_at, expires_at)
46
+ VALUES (?, ?, ?, ?, ?, ?)
47
+ ON CONFLICT(key) DO UPDATE SET
48
+ value = excluded.value,
49
+ type = excluded.type,
50
+ updated_at = excluded.updated_at,
51
+ expires_at = excluded.expires_at;`, [key, valueStr, type, now, now, expiresAt]);
52
+ }
53
+ static async get(db, tableName = exports.DEFAULT_KV_TABLE, key) {
54
+ const rows = await db.execute(`SELECT * FROM ${tableName} WHERE key = ? LIMIT 1;`, [key]);
55
+ if (!rows || rows.length === 0)
56
+ return null;
57
+ return mapKVRow(rows[0]);
58
+ }
59
+ static async delete(db, tableName = exports.DEFAULT_KV_TABLE, key) {
60
+ await db.execute(`DELETE FROM ${tableName} WHERE key = ?;`, [key]);
61
+ return true;
62
+ }
63
+ static async listKeys(db, tableName = exports.DEFAULT_KV_TABLE, prefix) {
64
+ let rows;
65
+ if (prefix) {
66
+ rows = await db.execute(`SELECT key, expires_at FROM ${tableName} WHERE key LIKE ? ORDER BY key ASC;`, [`${prefix}%`]);
67
+ }
68
+ else {
69
+ rows = await db.execute(`SELECT key, expires_at FROM ${tableName} ORDER BY key ASC;`);
70
+ }
71
+ const now = Date.now();
72
+ return (rows || [])
73
+ .filter((r) => r.expires_at == null || Number(r.expires_at) > now)
74
+ .map((r) => r.key);
75
+ }
76
+ static async clear(db, tableName = exports.DEFAULT_KV_TABLE) {
77
+ await db.execute(`DELETE FROM ${tableName};`);
78
+ }
79
+ static async pruneExpired(db, tableName = exports.DEFAULT_KV_TABLE) {
80
+ const now = Date.now();
81
+ await db.execute(`DELETE FROM ${tableName} WHERE expires_at IS NOT NULL AND expires_at <= ?;`, [now]);
82
+ return 0;
83
+ }
84
+ }
85
+ exports.KVStore = KVStore;
@@ -0,0 +1,27 @@
1
+ export interface KVSetOptions {
2
+ /**
3
+ * Time-to-live in milliseconds from current time.
4
+ */
5
+ ttlMs?: number;
6
+ /**
7
+ * Explicit Unix timestamp in milliseconds when this key expires.
8
+ */
9
+ expiresAt?: number;
10
+ }
11
+ export interface KVSetSecretOptions extends KVSetOptions {
12
+ /**
13
+ * Passphrase used to encrypt the value using AES-256-GCM.
14
+ */
15
+ passphrase: string;
16
+ }
17
+ export interface KVRecord<T = any> {
18
+ key: string;
19
+ value: T;
20
+ type: 'string' | 'number' | 'boolean' | 'json' | 'encrypted';
21
+ createdAt: number;
22
+ updatedAt: number;
23
+ expiresAt?: number | null;
24
+ }
25
+ export interface KVStoreOptions {
26
+ tableName?: string;
27
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -131,3 +131,5 @@ export { SyncInspector } from './SyncInspector.js';
131
131
  export type { SyncInspectorProps } from './SyncInspector.js';
132
132
  export * from './auth.js';
133
133
  export * from './backups.js';
134
+ export * from './kv.js';
135
+ export * from './search.js';
@@ -646,3 +646,5 @@ var SyncInspector_js_1 = require("./SyncInspector.js");
646
646
  Object.defineProperty(exports, "SyncInspector", { enumerable: true, get: function () { return SyncInspector_js_1.SyncInspector; } });
647
647
  __exportStar(require("./auth.js"), exports);
648
648
  __exportStar(require("./backups.js"), exports);
649
+ __exportStar(require("./kv.js"), exports);
650
+ __exportStar(require("./search.js"), exports);
@@ -0,0 +1,9 @@
1
+ import { LocalFirstClient } from '../core/client.js';
2
+ /**
3
+ * Reactive React hook for client.kv key-value preferences.
4
+ * Automatically re-renders whenever the key is updated or deleted.
5
+ *
6
+ * Example:
7
+ * const [theme, setTheme, loading] = useKV(client, 'theme', 'light');
8
+ */
9
+ export declare function useKV<T = any>(client: LocalFirstClient | null | undefined, key: string, defaultValue?: T): [T | undefined, (value: T) => Promise<void>, boolean];
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useKV = useKV;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Reactive React hook for client.kv key-value preferences.
7
+ * Automatically re-renders whenever the key is updated or deleted.
8
+ *
9
+ * Example:
10
+ * const [theme, setTheme, loading] = useKV(client, 'theme', 'light');
11
+ */
12
+ function useKV(client, key, defaultValue) {
13
+ const [value, setValueState] = (0, react_1.useState)(defaultValue);
14
+ const [loading, setLoading] = (0, react_1.useState)(true);
15
+ (0, react_1.useEffect)(() => {
16
+ if (!client) {
17
+ setValueState(defaultValue);
18
+ setLoading(false);
19
+ return;
20
+ }
21
+ let isMounted = true;
22
+ const load = async () => {
23
+ try {
24
+ const val = await client.kv.get(key, defaultValue);
25
+ if (isMounted) {
26
+ setValueState(val);
27
+ setLoading(false);
28
+ }
29
+ }
30
+ catch {
31
+ if (isMounted)
32
+ setLoading(false);
33
+ }
34
+ };
35
+ load();
36
+ const unsubscribe = client.events.on('_local_kv', (e) => {
37
+ if (!e || e.key === key || !e.key) {
38
+ load();
39
+ }
40
+ });
41
+ return () => {
42
+ isMounted = false;
43
+ unsubscribe();
44
+ };
45
+ }, [client, key, defaultValue]);
46
+ const setKVValue = (0, react_1.useCallback)(async (newValue) => {
47
+ if (!client)
48
+ return;
49
+ await client.kv.set(key, newValue);
50
+ setValueState(newValue);
51
+ }, [client, key]);
52
+ return [value, setKVValue, loading];
53
+ }
@@ -0,0 +1,19 @@
1
+ import { LocalFirstClient } from '../core/client.js';
2
+ import { SearchQueryOptions, SearchResult } from '../search/types.js';
3
+ export interface UseSearchOptions extends SearchQueryOptions {
4
+ /**
5
+ * Debounce delay in milliseconds (default: 150ms).
6
+ */
7
+ debounceMs?: number;
8
+ }
9
+ /**
10
+ * Reactive React hook for instant full-text search with automatic debouncing.
11
+ *
12
+ * Example:
13
+ * const { results, loading, error } = useSearch(client, 'properties', searchQuery);
14
+ */
15
+ export declare function useSearch<T = any>(client: LocalFirstClient | null | undefined, table: string, queryText: string, options?: UseSearchOptions): {
16
+ results: SearchResult<T>[];
17
+ loading: boolean;
18
+ error: Error | null;
19
+ };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSearch = useSearch;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Reactive React hook for instant full-text search with automatic debouncing.
7
+ *
8
+ * Example:
9
+ * const { results, loading, error } = useSearch(client, 'properties', searchQuery);
10
+ */
11
+ function useSearch(client, table, queryText, options) {
12
+ const [results, setResults] = (0, react_1.useState)([]);
13
+ const [loading, setLoading] = (0, react_1.useState)(false);
14
+ const [error, setError] = (0, react_1.useState)(null);
15
+ const debounceMs = options?.debounceMs ?? 150;
16
+ (0, react_1.useEffect)(() => {
17
+ if (!client || !table || !queryText.trim()) {
18
+ setResults([]);
19
+ setLoading(false);
20
+ return;
21
+ }
22
+ let isMounted = true;
23
+ setLoading(true);
24
+ const timer = setTimeout(async () => {
25
+ try {
26
+ const res = await client.search.query(table, queryText, options);
27
+ if (isMounted) {
28
+ setResults(res);
29
+ setError(null);
30
+ }
31
+ }
32
+ catch (err) {
33
+ if (isMounted) {
34
+ setError(err instanceof Error ? err : new Error(String(err)));
35
+ }
36
+ }
37
+ finally {
38
+ if (isMounted) {
39
+ setLoading(false);
40
+ }
41
+ }
42
+ }, debounceMs);
43
+ return () => {
44
+ isMounted = false;
45
+ clearTimeout(timer);
46
+ };
47
+ }, [client, table, queryText, debounceMs]);
48
+ return { results, loading, error };
49
+ }
@@ -2,6 +2,7 @@ type EventCallback = (data: any) => void;
2
2
  export declare class EventEmitter {
3
3
  private listeners;
4
4
  on(event: string, callback: EventCallback): () => void;
5
+ off(event: string, callback: EventCallback): void;
5
6
  emit(event: string, data: any): void;
6
7
  }
7
8
  export {};
@@ -9,14 +9,17 @@ class EventEmitter {
9
9
  }
10
10
  this.listeners.get(event).add(callback);
11
11
  return () => {
12
- const eventSet = this.listeners.get(event);
13
- if (eventSet) {
14
- eventSet.delete(callback);
15
- if (eventSet.size === 0)
16
- this.listeners.delete(event);
17
- }
12
+ this.off(event, callback);
18
13
  };
19
14
  }
15
+ off(event, callback) {
16
+ const eventSet = this.listeners.get(event);
17
+ if (eventSet) {
18
+ eventSet.delete(callback);
19
+ if (eventSet.size === 0)
20
+ this.listeners.delete(event);
21
+ }
22
+ }
20
23
  emit(event, data) {
21
24
  const eventSet = this.listeners.get(event);
22
25
  if (eventSet) {
@@ -0,0 +1,6 @@
1
+ import { ScheduleType } from './types.js';
2
+ export declare function parseScheduleToIntervalMs(schedule: ScheduleType): number;
3
+ /**
4
+ * Calculates next run timestamp given schedule and last run (or current) timestamp.
5
+ */
6
+ export declare function calculateNextRun(schedule: ScheduleType, fromTimestamp?: number): number;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseScheduleToIntervalMs = parseScheduleToIntervalMs;
4
+ exports.calculateNextRun = calculateNextRun;
5
+ function parseScheduleToIntervalMs(schedule) {
6
+ if (typeof schedule === 'object' && schedule !== null && 'intervalMs' in schedule) {
7
+ return Math.max(1000, Number(schedule.intervalMs) || 60000);
8
+ }
9
+ if (typeof schedule === 'number') {
10
+ return Math.max(1000, schedule);
11
+ }
12
+ const str = String(schedule).trim().toLowerCase();
13
+ if (str === 'hourly')
14
+ return 60 * 60 * 1000;
15
+ if (str === 'daily')
16
+ return 24 * 60 * 60 * 1000;
17
+ if (str === 'weekly')
18
+ return 7 * 24 * 60 * 60 * 1000;
19
+ // 'every X seconds/minutes/hours/days'
20
+ const match = str.match(/^every\s+(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hours?|d|days?)$/i);
21
+ if (match) {
22
+ const val = parseInt(match[1], 10);
23
+ const unit = match[2].toLowerCase();
24
+ if (unit.startsWith('s'))
25
+ return val * 1000;
26
+ if (unit.startsWith('m'))
27
+ return val * 60 * 1000;
28
+ if (unit.startsWith('h'))
29
+ return val * 60 * 60 * 1000;
30
+ if (unit.startsWith('d'))
31
+ return val * 24 * 60 * 60 * 1000;
32
+ }
33
+ // 5-field cron expression: min hour dom month dow
34
+ const parts = str.split(/\s+/);
35
+ if (parts.length === 5) {
36
+ // If it's */n in minutes
37
+ if (parts[0].startsWith('*/')) {
38
+ const step = parseInt(parts[0].slice(2), 10);
39
+ if (!isNaN(step) && step > 0) {
40
+ return step * 60 * 1000;
41
+ }
42
+ }
43
+ // If it's */n in hours (e.g. 0 */2 * * *)
44
+ if (parts[1].startsWith('*/')) {
45
+ const step = parseInt(parts[1].slice(2), 10);
46
+ if (!isNaN(step) && step > 0) {
47
+ return step * 60 * 60 * 1000;
48
+ }
49
+ }
50
+ // Default fallback for daily cron like 0 9 * * * or 0 0 * * *
51
+ return 24 * 60 * 60 * 1000;
52
+ }
53
+ // Default fallback: 1 hour
54
+ return 60 * 60 * 1000;
55
+ }
56
+ /**
57
+ * Calculates next run timestamp given schedule and last run (or current) timestamp.
58
+ */
59
+ function calculateNextRun(schedule, fromTimestamp = Date.now()) {
60
+ const interval = parseScheduleToIntervalMs(schedule);
61
+ return fromTimestamp + interval;
62
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './cron.js';
3
+ export * from './store.js';
4
+ export * from './manager.js';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types.js"), exports);
18
+ __exportStar(require("./cron.js"), exports);
19
+ __exportStar(require("./store.js"), exports);
20
+ __exportStar(require("./manager.js"), exports);
@@ -0,0 +1,59 @@
1
+ import { DBExecutable } from '../core/schema-manager.js';
2
+ import { JobDefinition, JobRecord, JobRunRecord, JobSchedulerOptions } from './types.js';
3
+ export declare class JobScheduler {
4
+ private db;
5
+ private handlers;
6
+ private timer;
7
+ private isTicking;
8
+ private checkIntervalMs;
9
+ constructor(db: DBExecutable, options?: JobSchedulerOptions);
10
+ initialize(): Promise<void>;
11
+ /**
12
+ * Registers a background recurring job and stores its schedule in SQLite.
13
+ */
14
+ register(job: JobDefinition): Promise<JobRecord>;
15
+ /**
16
+ * Attaches or updates a handler function for an already registered job id.
17
+ */
18
+ setHandler(jobId: string, handler: (job: JobRecord) => Promise<void> | void): void;
19
+ /**
20
+ * Removes a job from schedule and handlers.
21
+ */
22
+ unregister(jobId: string): Promise<void>;
23
+ /**
24
+ * Enables a paused job.
25
+ */
26
+ enable(jobId: string): Promise<void>;
27
+ /**
28
+ * Disables/pauses a job.
29
+ */
30
+ disable(jobId: string): Promise<void>;
31
+ /**
32
+ * Manually triggers a job immediately, logging execution in run history.
33
+ */
34
+ trigger(jobId: string): Promise<JobRunRecord>;
35
+ /**
36
+ * Checks for due jobs and executes them. Safe against reentrant concurrent ticks.
37
+ */
38
+ tick(): Promise<number>;
39
+ /**
40
+ * Starts periodic background ticker.
41
+ */
42
+ start(intervalMs?: number): void;
43
+ /**
44
+ * Stops the background periodic ticker.
45
+ */
46
+ stop(): void;
47
+ /**
48
+ * Returns all scheduled jobs.
49
+ */
50
+ getJobs(): Promise<JobRecord[]>;
51
+ /**
52
+ * Returns run history for a specific job or all jobs.
53
+ */
54
+ getHistory(jobId?: string, limit?: number): Promise<JobRunRecord[]>;
55
+ /**
56
+ * Prunes historical run logs older than the given age in milliseconds (default: 30 days).
57
+ */
58
+ pruneHistory(maxAgeMs?: number): Promise<void>;
59
+ }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JobScheduler = void 0;
4
+ const uuid_js_1 = require("../utils/uuid.js");
5
+ const cron_js_1 = require("./cron.js");
6
+ const store_js_1 = require("./store.js");
7
+ class JobScheduler {
8
+ db;
9
+ handlers = new Map();
10
+ timer = null;
11
+ isTicking = false;
12
+ checkIntervalMs;
13
+ constructor(db, options = {}) {
14
+ this.db = db;
15
+ this.checkIntervalMs = options.checkIntervalMs ?? 30000;
16
+ }
17
+ async initialize() {
18
+ await store_js_1.JobStore.ensureSchema(this.db);
19
+ }
20
+ /**
21
+ * Registers a background recurring job and stores its schedule in SQLite.
22
+ */
23
+ async register(job) {
24
+ const intervalMs = (0, cron_js_1.parseScheduleToIntervalMs)(job.schedule);
25
+ const now = Date.now();
26
+ const nextRunAt = (0, cron_js_1.calculateNextRun)(job.schedule, now);
27
+ const record = {
28
+ id: job.id,
29
+ name: job.name || job.id,
30
+ schedule: typeof job.schedule === 'string' ? job.schedule : `${intervalMs}ms`,
31
+ intervalMs,
32
+ lastRunAt: null,
33
+ nextRunAt,
34
+ enabled: job.enabled !== false,
35
+ payload: job.payload,
36
+ createdAt: now,
37
+ };
38
+ if (job.handler) {
39
+ this.handlers.set(job.id, job.handler);
40
+ }
41
+ await store_js_1.JobStore.upsertJob(this.db, undefined, record);
42
+ return record;
43
+ }
44
+ /**
45
+ * Attaches or updates a handler function for an already registered job id.
46
+ */
47
+ setHandler(jobId, handler) {
48
+ this.handlers.set(jobId, handler);
49
+ }
50
+ /**
51
+ * Removes a job from schedule and handlers.
52
+ */
53
+ async unregister(jobId) {
54
+ this.handlers.delete(jobId);
55
+ await store_js_1.JobStore.deleteJob(this.db, undefined, jobId);
56
+ }
57
+ /**
58
+ * Enables a paused job.
59
+ */
60
+ async enable(jobId) {
61
+ await store_js_1.JobStore.setJobEnabled(this.db, undefined, jobId, true);
62
+ }
63
+ /**
64
+ * Disables/pauses a job.
65
+ */
66
+ async disable(jobId) {
67
+ await store_js_1.JobStore.setJobEnabled(this.db, undefined, jobId, false);
68
+ }
69
+ /**
70
+ * Manually triggers a job immediately, logging execution in run history.
71
+ */
72
+ async trigger(jobId) {
73
+ const job = await store_js_1.JobStore.getJob(this.db, undefined, jobId);
74
+ if (!job) {
75
+ throw new Error(`Job "${jobId}" not found.`);
76
+ }
77
+ const runId = (0, uuid_js_1.generateUUID)();
78
+ const startedAt = Date.now();
79
+ const runRecord = {
80
+ id: runId,
81
+ jobId,
82
+ status: 'RUNNING',
83
+ startedAt,
84
+ finishedAt: null,
85
+ durationMs: null,
86
+ };
87
+ await store_js_1.JobStore.recordRunStart(this.db, undefined, runRecord);
88
+ const handler = this.handlers.get(jobId);
89
+ let runStatus = 'SUCCESS';
90
+ let runError = null;
91
+ try {
92
+ if (handler) {
93
+ await handler(job);
94
+ }
95
+ }
96
+ catch (err) {
97
+ runStatus = 'FAILED';
98
+ runError = err?.message || String(err);
99
+ }
100
+ const finishedAt = Date.now();
101
+ const durationMs = finishedAt - startedAt;
102
+ await store_js_1.JobStore.recordRunFinish(this.db, undefined, runId, runStatus, finishedAt, durationMs, runError);
103
+ const nextRunAt = (0, cron_js_1.calculateNextRun)(job.schedule, finishedAt);
104
+ await store_js_1.JobStore.updateJobSchedule(this.db, undefined, jobId, finishedAt, nextRunAt);
105
+ return {
106
+ id: runId,
107
+ jobId,
108
+ status: runStatus,
109
+ startedAt,
110
+ finishedAt,
111
+ durationMs,
112
+ error: runError,
113
+ };
114
+ }
115
+ /**
116
+ * Checks for due jobs and executes them. Safe against reentrant concurrent ticks.
117
+ */
118
+ async tick() {
119
+ if (this.isTicking)
120
+ return 0;
121
+ this.isTicking = true;
122
+ try {
123
+ const now = Date.now();
124
+ const dueJobs = await store_js_1.JobStore.getDueJobs(this.db, undefined, now);
125
+ if (dueJobs.length === 0)
126
+ return 0;
127
+ let executed = 0;
128
+ for (const job of dueJobs) {
129
+ try {
130
+ await this.trigger(job.id);
131
+ executed++;
132
+ }
133
+ catch {
134
+ // Failure logged in trigger; continue next job
135
+ }
136
+ }
137
+ return executed;
138
+ }
139
+ finally {
140
+ this.isTicking = false;
141
+ }
142
+ }
143
+ /**
144
+ * Starts periodic background ticker.
145
+ */
146
+ start(intervalMs) {
147
+ if (this.timer)
148
+ return;
149
+ const interval = intervalMs ?? this.checkIntervalMs;
150
+ this.timer = setInterval(() => {
151
+ this.tick().catch(() => { });
152
+ }, interval);
153
+ if (typeof this.timer?.unref === 'function') {
154
+ this.timer.unref();
155
+ }
156
+ }
157
+ /**
158
+ * Stops the background periodic ticker.
159
+ */
160
+ stop() {
161
+ if (this.timer) {
162
+ clearInterval(this.timer);
163
+ this.timer = null;
164
+ }
165
+ }
166
+ /**
167
+ * Returns all scheduled jobs.
168
+ */
169
+ async getJobs() {
170
+ return await store_js_1.JobStore.getAllJobs(this.db);
171
+ }
172
+ /**
173
+ * Returns run history for a specific job or all jobs.
174
+ */
175
+ async getHistory(jobId, limit = 50) {
176
+ return await store_js_1.JobStore.getRuns(this.db, undefined, jobId, limit);
177
+ }
178
+ /**
179
+ * Prunes historical run logs older than the given age in milliseconds (default: 30 days).
180
+ */
181
+ async pruneHistory(maxAgeMs = 30 * 24 * 60 * 60 * 1000) {
182
+ const cutoff = Date.now() - maxAgeMs;
183
+ await store_js_1.JobStore.pruneRuns(this.db, undefined, cutoff);
184
+ }
185
+ }
186
+ exports.JobScheduler = JobScheduler;
@@ -0,0 +1,20 @@
1
+ import { DBExecutable } from '../core/schema-manager.js';
2
+ import { JobRecord, JobRunRecord } from './types.js';
3
+ export declare const DEFAULT_JOBS_TABLE = "_local_scheduled_jobs";
4
+ export declare const DEFAULT_JOB_RUNS_TABLE = "_local_job_runs";
5
+ export declare function mapJobRow(r: any): JobRecord;
6
+ export declare function mapJobRunRow(r: any): JobRunRecord;
7
+ export declare class JobStore {
8
+ static ensureSchema(db: DBExecutable, jobsTable?: string, runsTable?: string): Promise<void>;
9
+ static upsertJob(db: DBExecutable, jobsTable: string | undefined, job: JobRecord): Promise<void>;
10
+ static getJob(db: DBExecutable, jobsTable: string | undefined, id: string): Promise<JobRecord | null>;
11
+ static getAllJobs(db: DBExecutable, jobsTable?: string): Promise<JobRecord[]>;
12
+ static getDueJobs(db: DBExecutable, jobsTable?: string, now?: number): Promise<JobRecord[]>;
13
+ static updateJobSchedule(db: DBExecutable, jobsTable: string | undefined, id: string, lastRunAt: number, nextRunAt: number): Promise<void>;
14
+ static setJobEnabled(db: DBExecutable, jobsTable: string | undefined, id: string, enabled: boolean): Promise<void>;
15
+ static deleteJob(db: DBExecutable, jobsTable: string | undefined, id: string): Promise<void>;
16
+ static recordRunStart(db: DBExecutable, runsTable: string | undefined, run: JobRunRecord): Promise<void>;
17
+ static recordRunFinish(db: DBExecutable, runsTable: string | undefined, id: string, status: 'SUCCESS' | 'FAILED', finishedAt: number, durationMs: number, error?: string | null): Promise<void>;
18
+ static getRuns(db: DBExecutable, runsTable?: string, jobId?: string, limit?: number): Promise<JobRunRecord[]>;
19
+ static pruneRuns(db: DBExecutable, runsTable: string | undefined, cutoffTimestamp: number): Promise<void>;
20
+ }