@hybrd/scheduler 2.0.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/README.md +339 -0
- package/dist/index.cjs +1135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +1096 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +672 -0
- package/src/scheduler.eval.ts +164 -0
- package/src/sql.js.d.ts +21 -0
- package/src/store.ts +316 -0
- package/src/tools.ts +394 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { SqliteSchedulerStore } from './src/store.js'
|
|
6
|
+
import { SchedulerService, createSchedulerService } from './src/index.js'
|
|
7
|
+
|
|
8
|
+
describe('Scheduler Integration Eval', () => {
|
|
9
|
+
let tempDir: string
|
|
10
|
+
let store: SqliteSchedulerStore
|
|
11
|
+
let scheduler: SchedulerService
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = mkdtempSync(join(tmpdir(), 'eval-scheduler-'))
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (existsSync(tempDir)) {
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
20
|
+
}
|
|
21
|
+
scheduler?.stop()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('initializes scheduler with SQLite', async () => {
|
|
25
|
+
store = new SqliteSchedulerStore({ dbPath: join(tempDir, 'scheduler.db') })
|
|
26
|
+
await store.init()
|
|
27
|
+
|
|
28
|
+
scheduler = await createSchedulerService({
|
|
29
|
+
store,
|
|
30
|
+
dispatcher: {
|
|
31
|
+
dispatch: vi.fn().mockResolvedValue({ delivered: true })
|
|
32
|
+
},
|
|
33
|
+
executor: {
|
|
34
|
+
runAgentTurn: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
35
|
+
runSystemEvent: vi.fn().mockResolvedValue({ status: 'ok' })
|
|
36
|
+
},
|
|
37
|
+
enabled: false
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(scheduler).toBeDefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('creates and lists scheduled tasks', async () => {
|
|
44
|
+
store = new SqliteSchedulerStore({ dbPath: join(tempDir, 'scheduler.db') })
|
|
45
|
+
await store.init()
|
|
46
|
+
|
|
47
|
+
scheduler = await createSchedulerService({
|
|
48
|
+
store,
|
|
49
|
+
dispatcher: {
|
|
50
|
+
dispatch: vi.fn().mockResolvedValue({ delivered: true })
|
|
51
|
+
},
|
|
52
|
+
executor: {
|
|
53
|
+
runAgentTurn: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
54
|
+
runSystemEvent: vi.fn().mockResolvedValue({ status: 'ok' })
|
|
55
|
+
},
|
|
56
|
+
enabled: false
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const task = await scheduler.add({
|
|
60
|
+
agentId: 'eval-agent',
|
|
61
|
+
name: 'eval-test-task',
|
|
62
|
+
schedule: { kind: 'every', everyMs: 60000 },
|
|
63
|
+
payload: { kind: 'systemEvent', text: 'Test event' },
|
|
64
|
+
enabled: true
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
expect(task.id).toBeDefined()
|
|
68
|
+
|
|
69
|
+
const tasks = await scheduler.list()
|
|
70
|
+
expect(tasks.length).toBe(1)
|
|
71
|
+
expect(tasks[0].name).toBe('eval-test-task')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('cancels scheduled tasks', async () => {
|
|
75
|
+
store = new SqliteSchedulerStore({ dbPath: join(tempDir, 'scheduler.db') })
|
|
76
|
+
await store.init()
|
|
77
|
+
|
|
78
|
+
scheduler = await createSchedulerService({
|
|
79
|
+
store,
|
|
80
|
+
dispatcher: {
|
|
81
|
+
dispatch: vi.fn().mockResolvedValue({ delivered: true })
|
|
82
|
+
},
|
|
83
|
+
executor: {
|
|
84
|
+
runAgentTurn: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
85
|
+
runSystemEvent: vi.fn().mockResolvedValue({ status: 'ok' })
|
|
86
|
+
},
|
|
87
|
+
enabled: false
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const task = await scheduler.add({
|
|
91
|
+
agentId: 'eval-agent',
|
|
92
|
+
name: 'cancellable-task',
|
|
93
|
+
schedule: { kind: 'every', everyMs: 60000 },
|
|
94
|
+
payload: { kind: 'systemEvent', text: 'Test event' },
|
|
95
|
+
enabled: true
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await scheduler.remove(task.id)
|
|
99
|
+
|
|
100
|
+
const tasks = await scheduler.list()
|
|
101
|
+
expect(tasks.length).toBe(0)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('handles cron expressions', async () => {
|
|
105
|
+
store = new SqliteSchedulerStore({ dbPath: join(tempDir, 'scheduler.db') })
|
|
106
|
+
await store.init()
|
|
107
|
+
|
|
108
|
+
scheduler = await createSchedulerService({
|
|
109
|
+
store,
|
|
110
|
+
dispatcher: {
|
|
111
|
+
dispatch: vi.fn().mockResolvedValue({ delivered: true })
|
|
112
|
+
},
|
|
113
|
+
executor: {
|
|
114
|
+
runAgentTurn: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
115
|
+
runSystemEvent: vi.fn().mockResolvedValue({ status: 'ok' })
|
|
116
|
+
},
|
|
117
|
+
enabled: false
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const task = await scheduler.add({
|
|
121
|
+
agentId: 'eval-agent',
|
|
122
|
+
name: 'cron-task',
|
|
123
|
+
schedule: { kind: 'cron', expr: '* * * * *' },
|
|
124
|
+
payload: { kind: 'systemEvent', text: 'Cron test' },
|
|
125
|
+
enabled: true
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(task.id).toBeDefined()
|
|
129
|
+
|
|
130
|
+
const retrieved = await scheduler.get(task.id)
|
|
131
|
+
expect(retrieved?.schedule.kind).toBe('cron')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('updates scheduled tasks', async () => {
|
|
135
|
+
store = new SqliteSchedulerStore({ dbPath: join(tempDir, 'scheduler.db') })
|
|
136
|
+
await store.init()
|
|
137
|
+
|
|
138
|
+
scheduler = await createSchedulerService({
|
|
139
|
+
store,
|
|
140
|
+
dispatcher: {
|
|
141
|
+
dispatch: vi.fn().mockResolvedValue({ delivered: true })
|
|
142
|
+
},
|
|
143
|
+
executor: {
|
|
144
|
+
runAgentTurn: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
145
|
+
runSystemEvent: vi.fn().mockResolvedValue({ status: 'ok' })
|
|
146
|
+
},
|
|
147
|
+
enabled: false
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const task = await scheduler.add({
|
|
151
|
+
agentId: 'eval-agent',
|
|
152
|
+
name: 'original-name',
|
|
153
|
+
schedule: { kind: 'every', everyMs: 60000 },
|
|
154
|
+
payload: { kind: 'systemEvent', text: 'Test event' },
|
|
155
|
+
enabled: true
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
await scheduler.update(task.id, { name: 'updated-name', enabled: false })
|
|
159
|
+
|
|
160
|
+
const retrieved = await scheduler.get(task.id)
|
|
161
|
+
expect(retrieved?.name).toBe('updated-name')
|
|
162
|
+
expect(retrieved?.enabled).toBe(false)
|
|
163
|
+
})
|
|
164
|
+
})
|
package/src/sql.js.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module "sql.js" {
|
|
2
|
+
export interface Database {
|
|
3
|
+
run(sql: string, params?: unknown[]): void
|
|
4
|
+
exec(sql: string, params?: unknown[]): QueryExecResult[]
|
|
5
|
+
export(): Uint8Array
|
|
6
|
+
close(): void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface QueryExecResult {
|
|
10
|
+
columns: string[]
|
|
11
|
+
values: unknown[][]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SqlJsStatic {
|
|
15
|
+
Database: new (data?: ArrayLike<number>) => Database
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function initSqlJs(config?: {
|
|
19
|
+
locateFile?: (file: string) => string
|
|
20
|
+
}): Promise<SqlJsStatic>
|
|
21
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { createRequire } from "node:module"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import initSqlJs from "sql.js"
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CronJob,
|
|
8
|
+
CronJobState,
|
|
9
|
+
CronPayload,
|
|
10
|
+
CronSchedule,
|
|
11
|
+
SessionTarget,
|
|
12
|
+
WakeMode
|
|
13
|
+
} from "@hybrd/types"
|
|
14
|
+
|
|
15
|
+
const SCHEMA = `
|
|
16
|
+
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
agent_id TEXT,
|
|
19
|
+
session_key TEXT,
|
|
20
|
+
name TEXT NOT NULL,
|
|
21
|
+
description TEXT,
|
|
22
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
23
|
+
delete_after_run INTEGER,
|
|
24
|
+
created_at_ms INTEGER NOT NULL,
|
|
25
|
+
updated_at_ms INTEGER NOT NULL,
|
|
26
|
+
schedule TEXT NOT NULL,
|
|
27
|
+
session_target TEXT NOT NULL DEFAULT 'isolated',
|
|
28
|
+
wake_mode TEXT NOT NULL DEFAULT 'now',
|
|
29
|
+
payload TEXT NOT NULL,
|
|
30
|
+
delivery TEXT,
|
|
31
|
+
state TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_cron_jobs_next_run ON cron_jobs(id);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled);
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
export interface SqliteSchedulerStoreOptions {
|
|
39
|
+
dbPath?: string
|
|
40
|
+
onSave?: (data: Uint8Array) => void | Promise<void>
|
|
41
|
+
loadOnInit?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type SqlJsStatic = Awaited<ReturnType<typeof initSqlJs>>
|
|
45
|
+
type SqlJsDatabase = InstanceType<SqlJsStatic["Database"]>
|
|
46
|
+
|
|
47
|
+
export class SqliteSchedulerStore {
|
|
48
|
+
private db: SqlJsDatabase | null = null
|
|
49
|
+
private dbPath: string
|
|
50
|
+
private onSave?: (data: Uint8Array) => void | Promise<void>
|
|
51
|
+
private loadOnInit: boolean
|
|
52
|
+
private initPromise: Promise<void> | null = null
|
|
53
|
+
private dirty = false
|
|
54
|
+
private saveTimer?: ReturnType<typeof setTimeout>
|
|
55
|
+
private jobsCache: Map<string, CronJob> | null = null
|
|
56
|
+
|
|
57
|
+
constructor(options: SqliteSchedulerStoreOptions = {}) {
|
|
58
|
+
this.dbPath = options.dbPath ?? ":memory:"
|
|
59
|
+
this.onSave = options.onSave
|
|
60
|
+
this.loadOnInit = options.loadOnInit ?? true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async init(): Promise<void> {
|
|
64
|
+
if (this.initPromise) {
|
|
65
|
+
return this.initPromise
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.initPromise = this._init()
|
|
69
|
+
return this.initPromise
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async _init(): Promise<void> {
|
|
73
|
+
let SQL: SqlJsStatic
|
|
74
|
+
try {
|
|
75
|
+
const req = createRequire(process.cwd())
|
|
76
|
+
const sqlJsPath = dirname(req.resolve("sql.js"))
|
|
77
|
+
const wasmPath = join(sqlJsPath, "sql-wasm.wasm")
|
|
78
|
+
const wasmBinary = readFileSync(wasmPath)
|
|
79
|
+
SQL = await initSqlJs({ wasmBinary } as any)
|
|
80
|
+
} catch {
|
|
81
|
+
SQL = await initSqlJs()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.dbPath === ":memory:" || !this.loadOnInit) {
|
|
85
|
+
this.db = new SQL.Database()
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
const buffer = readFileSync(this.dbPath)
|
|
89
|
+
this.db = new SQL.Database(new Uint8Array(buffer))
|
|
90
|
+
} catch {
|
|
91
|
+
this.db = new SQL.Database()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.db.run(SCHEMA)
|
|
96
|
+
this.loadCache()
|
|
97
|
+
this.scheduleSave()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private loadCache(): void {
|
|
101
|
+
if (!this.db) return
|
|
102
|
+
const cache = new Map<string, CronJob>()
|
|
103
|
+
|
|
104
|
+
const result = this.db.exec(
|
|
105
|
+
"SELECT id, agent_id, session_key, name, description, enabled, delete_after_run, created_at_ms, updated_at_ms, schedule, session_target, wake_mode, payload, delivery, state FROM cron_jobs"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const firstResult = result[0]
|
|
109
|
+
if (firstResult) {
|
|
110
|
+
for (const row of firstResult.values) {
|
|
111
|
+
const job = this.rowToJob(row as unknown[])
|
|
112
|
+
if (job) {
|
|
113
|
+
cache.set(job.id, job)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.jobsCache = cache
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private scheduleSave(): void {
|
|
122
|
+
if (this.saveTimer) {
|
|
123
|
+
clearTimeout(this.saveTimer)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.saveTimer = setTimeout(() => {
|
|
127
|
+
void this.save()
|
|
128
|
+
}, 1000)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async save(): Promise<void> {
|
|
132
|
+
if (!this.db || !this.dirty) return
|
|
133
|
+
|
|
134
|
+
const data = this.db.export()
|
|
135
|
+
this.dirty = false
|
|
136
|
+
|
|
137
|
+
if (this.onSave) {
|
|
138
|
+
await this.onSave(data)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async close(): Promise<void> {
|
|
143
|
+
if (this.saveTimer) {
|
|
144
|
+
clearTimeout(this.saveTimer)
|
|
145
|
+
}
|
|
146
|
+
await this.save()
|
|
147
|
+
this.db?.close()
|
|
148
|
+
this.db = null
|
|
149
|
+
this.jobsCache = null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private rowToJob(row: unknown[]): CronJob | null {
|
|
153
|
+
try {
|
|
154
|
+
const schedule = JSON.parse(String(row[9])) as CronSchedule
|
|
155
|
+
const payload = JSON.parse(String(row[12])) as CronPayload
|
|
156
|
+
const delivery = row[13] ? JSON.parse(String(row[13])) : undefined
|
|
157
|
+
const state = JSON.parse(String(row[14])) as CronJobState
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
id: String(row[0]),
|
|
161
|
+
agentId: row[1] ? String(row[1]) : undefined,
|
|
162
|
+
sessionKey: row[2] ? String(row[2]) : undefined,
|
|
163
|
+
name: String(row[3]),
|
|
164
|
+
description: row[4] ? String(row[4]) : undefined,
|
|
165
|
+
enabled: Boolean(row[5]),
|
|
166
|
+
deleteAfterRun: row[6] ? Boolean(row[6]) : undefined,
|
|
167
|
+
createdAtMs: Number(row[7]),
|
|
168
|
+
updatedAtMs: Number(row[8]),
|
|
169
|
+
schedule,
|
|
170
|
+
sessionTarget: (row[10] as SessionTarget) ?? "isolated",
|
|
171
|
+
wakeMode: (row[11] as WakeMode) ?? "now",
|
|
172
|
+
payload,
|
|
173
|
+
delivery,
|
|
174
|
+
state
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private jobToRow(job: CronJob): unknown[] {
|
|
182
|
+
return [
|
|
183
|
+
job.id,
|
|
184
|
+
job.agentId ?? null,
|
|
185
|
+
job.sessionKey ?? null,
|
|
186
|
+
job.name,
|
|
187
|
+
job.description ?? null,
|
|
188
|
+
job.enabled ? 1 : 0,
|
|
189
|
+
job.deleteAfterRun ? 1 : null,
|
|
190
|
+
job.createdAtMs,
|
|
191
|
+
job.updatedAtMs,
|
|
192
|
+
JSON.stringify(job.schedule),
|
|
193
|
+
job.sessionTarget,
|
|
194
|
+
job.wakeMode,
|
|
195
|
+
JSON.stringify(job.payload),
|
|
196
|
+
job.delivery ? JSON.stringify(job.delivery) : null,
|
|
197
|
+
JSON.stringify(job.state)
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private ensureDb(): SqlJsDatabase {
|
|
202
|
+
if (!this.db) {
|
|
203
|
+
throw new Error("Store not initialized. Call init() first.")
|
|
204
|
+
}
|
|
205
|
+
return this.db
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private ensureCache(): Map<string, CronJob> {
|
|
209
|
+
if (!this.jobsCache) {
|
|
210
|
+
throw new Error("Cache not initialized. Call init() first.")
|
|
211
|
+
}
|
|
212
|
+
return this.jobsCache
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getJobSync(id: string): CronJob | undefined {
|
|
216
|
+
const cache = this.ensureCache()
|
|
217
|
+
return cache.get(id)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getAllJobsSync(): CronJob[] {
|
|
221
|
+
const cache = this.ensureCache()
|
|
222
|
+
return Array.from(cache.values())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
saveJobSync(job: CronJob): void {
|
|
226
|
+
const db = this.ensureDb()
|
|
227
|
+
const cache = this.ensureCache()
|
|
228
|
+
|
|
229
|
+
db.run(
|
|
230
|
+
`INSERT OR REPLACE INTO cron_jobs
|
|
231
|
+
(id, agent_id, session_key, name, description, enabled, delete_after_run, created_at_ms, updated_at_ms, schedule, session_target, wake_mode, payload, delivery, state)
|
|
232
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
233
|
+
this.jobToRow(job)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
cache.set(job.id, job)
|
|
237
|
+
this.dirty = true
|
|
238
|
+
this.scheduleSave()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
saveAllJobsSync(): void {
|
|
242
|
+
const db = this.ensureDb()
|
|
243
|
+
const cache = this.ensureCache()
|
|
244
|
+
|
|
245
|
+
for (const job of cache.values()) {
|
|
246
|
+
db.run(
|
|
247
|
+
`INSERT OR REPLACE INTO cron_jobs
|
|
248
|
+
(id, agent_id, session_key, name, description, enabled, delete_after_run, created_at_ms, updated_at_ms, schedule, session_target, wake_mode, payload, delivery, state)
|
|
249
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
250
|
+
this.jobToRow(job)
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.dirty = true
|
|
255
|
+
this.scheduleSave()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
deleteJobSync(id: string): void {
|
|
259
|
+
const db = this.ensureDb()
|
|
260
|
+
const cache = this.ensureCache()
|
|
261
|
+
|
|
262
|
+
db.run("DELETE FROM cron_jobs WHERE id = ?", [id])
|
|
263
|
+
cache.delete(id)
|
|
264
|
+
this.dirty = true
|
|
265
|
+
this.scheduleSave()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async getTask(id: string): Promise<CronJob | undefined> {
|
|
269
|
+
return this.getJobSync(id)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getAllTasks(): Promise<CronJob[]> {
|
|
273
|
+
return this.getAllJobsSync()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async saveTask(task: CronJob): Promise<void> {
|
|
277
|
+
this.saveJobSync(task)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async deleteTask(id: string): Promise<void> {
|
|
281
|
+
this.deleteJobSync(id)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function createSqliteStore(
|
|
286
|
+
options?: SqliteSchedulerStoreOptions
|
|
287
|
+
): Promise<SqliteSchedulerStore> {
|
|
288
|
+
const resolvedOptions = { ...options }
|
|
289
|
+
|
|
290
|
+
// If a real dbPath is provided but no onSave callback, add a default
|
|
291
|
+
// file-write callback so that changes are actually persisted to disk.
|
|
292
|
+
if (
|
|
293
|
+
resolvedOptions.dbPath &&
|
|
294
|
+
resolvedOptions.dbPath !== ":memory:" &&
|
|
295
|
+
!resolvedOptions.onSave
|
|
296
|
+
) {
|
|
297
|
+
const { writeFileSync } = await import("node:fs")
|
|
298
|
+
const { dirname } = await import("node:path")
|
|
299
|
+
const { mkdirSync, existsSync } = await import("node:fs")
|
|
300
|
+
const dbPath = resolvedOptions.dbPath
|
|
301
|
+
|
|
302
|
+
// Ensure the parent directory exists
|
|
303
|
+
const dir = dirname(dbPath)
|
|
304
|
+
if (!existsSync(dir)) {
|
|
305
|
+
mkdirSync(dir, { recursive: true })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
resolvedOptions.onSave = (data: Uint8Array) => {
|
|
309
|
+
writeFileSync(dbPath, data)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const store = new SqliteSchedulerStore(resolvedOptions)
|
|
314
|
+
await store.init()
|
|
315
|
+
return store
|
|
316
|
+
}
|