@dcrays/scheduled-task 0.1.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.
@@ -0,0 +1,94 @@
1
+ import { assertMinimumCronInterval, CronExpressionError, validTimeZone } from "./cron.js";
2
+ export const CRON_CREATE_TYPE = "dsh/cron.create";
3
+ export const CRON_LIST_TYPE = "dsh/cron.list";
4
+ export class CronAutomationInputError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "CronAutomationInputError";
10
+ }
11
+ }
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function exactKeys(value, keys) {
16
+ const actual = Object.keys(value).sort();
17
+ const expected = [...keys].sort();
18
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
19
+ }
20
+ export function decodeCronCreateEnvelope(value) {
21
+ if (!isRecord(value) || value.type !== CRON_CREATE_TYPE) {
22
+ throw new CronAutomationInputError("invalid_envelope", `type must be ${CRON_CREATE_TYPE}`);
23
+ }
24
+ if (!exactKeys(value, ["type", "requestId", "name", "prompt", "cron", "timezone"])) {
25
+ throw new CronAutomationInputError("invalid_envelope", "cron envelope contains unknown or missing fields");
26
+ }
27
+ if (typeof value.requestId !== "string" || value.requestId.trim() !== value.requestId || value.requestId.length < 1 || value.requestId.length > 128) {
28
+ throw new CronAutomationInputError("invalid_request_id", "requestId must contain 1-128 characters without surrounding whitespace");
29
+ }
30
+ if (typeof value.name !== "string" || value.name.trim() !== value.name || value.name.length < 1 || value.name.length > 120) {
31
+ throw new CronAutomationInputError("invalid_name", "name must contain 1-120 characters without surrounding whitespace");
32
+ }
33
+ if (typeof value.prompt !== "string" || value.prompt.trim() !== value.prompt || value.prompt.length === 0) {
34
+ throw new CronAutomationInputError("invalid_prompt", "prompt must be a non-empty string without surrounding whitespace");
35
+ }
36
+ if (typeof value.cron !== "string") {
37
+ throw new CronAutomationInputError("invalid_cron", "cron must be a standard five-field expression");
38
+ }
39
+ try {
40
+ assertMinimumCronInterval(value.cron);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof CronExpressionError) {
44
+ throw new CronAutomationInputError("invalid_cron", error.message);
45
+ }
46
+ throw error;
47
+ }
48
+ if (typeof value.timezone !== "string" || !validTimeZone(value.timezone)) {
49
+ throw new CronAutomationInputError("invalid_timezone", "timezone must be a valid IANA time zone");
50
+ }
51
+ return {
52
+ type: CRON_CREATE_TYPE,
53
+ requestId: value.requestId,
54
+ name: value.name,
55
+ prompt: value.prompt,
56
+ cron: value.cron,
57
+ timezone: value.timezone
58
+ };
59
+ }
60
+ /** Parse a cron control envelope carried as the sole text block of a direct user message. */
61
+ export function parseCronAutomationMessage(message) {
62
+ if (message.source.kind !== "user" || message.content.length !== 1)
63
+ return undefined;
64
+ const block = message.content[0];
65
+ if (block?.type !== "text")
66
+ return undefined;
67
+ const text = block.text.trim();
68
+ if (!text.startsWith("{"))
69
+ return undefined;
70
+ let value;
71
+ try {
72
+ value = JSON.parse(text);
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ if (!isRecord(value) || value.type !== CRON_CREATE_TYPE)
78
+ return undefined;
79
+ return decodeCronCreateEnvelope(value);
80
+ }
81
+ /** Recognize the stable prompt prefix emitted by the Mobook scheduled-task editor. */
82
+ export function parseCronEditorRequest(message) {
83
+ if (message.source.kind !== "user" || message.content.length !== 1)
84
+ return undefined;
85
+ const block = message.content[0];
86
+ if (block?.type !== "text")
87
+ return undefined;
88
+ const text = block.text.trim();
89
+ const match = /^编辑定时任务[((]\s*任务\s*ID\s+([^))]+?)\s*[))][::]/u.exec(text);
90
+ const id = match?.[1]?.trim();
91
+ if (!id || id.length > 128 || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(id))
92
+ return undefined;
93
+ return { id, text };
94
+ }
@@ -0,0 +1,85 @@
1
+ export type OpenClawCronSchedule = {
2
+ kind: 'at';
3
+ at?: string;
4
+ atMs?: number;
5
+ [key: string]: unknown;
6
+ } | {
7
+ kind: 'every';
8
+ everyMs: number;
9
+ anchorMs?: number;
10
+ [key: string]: unknown;
11
+ } | {
12
+ kind: 'cron';
13
+ expr: string;
14
+ tz?: string;
15
+ [key: string]: unknown;
16
+ };
17
+ export interface OpenClawCronPayload {
18
+ kind: 'agentTurn';
19
+ message: string;
20
+ timeoutSeconds?: number;
21
+ [key: string]: unknown;
22
+ }
23
+ export interface OpenClawCronDelivery {
24
+ mode: 'announce' | 'none' | 'webhook';
25
+ channel?: string;
26
+ to?: string;
27
+ accountId?: string;
28
+ bestEffort?: boolean;
29
+ [key: string]: unknown;
30
+ }
31
+ /** OpenClaw-compatible job. Unknown fields survive migration unchanged. */
32
+ export interface StoredCronAutomation {
33
+ id: string;
34
+ name: string;
35
+ description?: string;
36
+ enabled: boolean;
37
+ createdAtMs: number;
38
+ updatedAtMs: number;
39
+ schedule: OpenClawCronSchedule;
40
+ payload: OpenClawCronPayload;
41
+ agentId: string;
42
+ /** DSH-only execution binding; OpenClaw ignores this extension. */
43
+ dshSessionId?: string;
44
+ sessionKey?: string;
45
+ sessionTarget: 'isolated';
46
+ wakeMode: 'now';
47
+ deleteAfterRun?: boolean;
48
+ delivery: OpenClawCronDelivery;
49
+ state: {
50
+ nextRunAtMs?: number;
51
+ runningAtMs?: number;
52
+ lastRunAtMs?: number;
53
+ lastRunStatus?: 'ok' | 'error' | 'skipped';
54
+ [key: string]: unknown;
55
+ };
56
+ [key: string]: unknown;
57
+ }
58
+ export interface CronImportResult {
59
+ added: StoredCronAutomation[];
60
+ skippedIds: string[];
61
+ }
62
+ export interface CronAutomationRepositoryOptions {
63
+ readOnly?: boolean;
64
+ }
65
+ export declare function decodeJob(value: unknown): StoredCronAutomation;
66
+ export declare class CronAutomationRepository {
67
+ readonly file: string;
68
+ private readonly database;
69
+ private readonly insertJobStatement;
70
+ private readonly readOnly;
71
+ private closed;
72
+ constructor(file: string, options?: CronAutomationRepositoryOptions);
73
+ load(): Promise<StoredCronAutomation[]>;
74
+ upsert(jobs: readonly StoredCronAutomation[]): Promise<void>;
75
+ delete(jobId: string): Promise<boolean>;
76
+ previewImport(source: string, incoming: readonly StoredCronAutomation[]): Promise<CronImportResult>;
77
+ import(source: string, incoming: readonly StoredCronAutomation[]): Promise<CronImportResult>;
78
+ close(): void;
79
+ private planImport;
80
+ private nextSortOrder;
81
+ private insertJob;
82
+ private assertOpen;
83
+ private assertWritable;
84
+ private writeTransaction;
85
+ }
@@ -0,0 +1,393 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { parseCronExpression, validTimeZone } from './cron.js';
5
+ const STORE_KEY = 'dsh-cron-automation';
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+ function finiteTimestamp(value) {
10
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
11
+ }
12
+ function decodeSchedule(value) {
13
+ if (!isRecord(value) || typeof value.kind !== 'string')
14
+ throw new Error('cron job has an invalid schedule');
15
+ if (value.kind === 'cron') {
16
+ const expr = typeof value.expr === 'string' ? value.expr : value.cron;
17
+ if (typeof expr !== 'string')
18
+ throw new Error('cron job has an invalid cron expression');
19
+ parseCronExpression(expr);
20
+ if (value.tz !== undefined && (typeof value.tz !== 'string' || !validTimeZone(value.tz))) {
21
+ throw new Error('cron job has an invalid time zone');
22
+ }
23
+ const { cron: _legacyCron, ...rest } = value;
24
+ return { ...rest, kind: 'cron', expr };
25
+ }
26
+ if (value.kind === 'every') {
27
+ if (!finiteTimestamp(value.everyMs) || value.everyMs < 60_000) {
28
+ throw new Error('cron job has an invalid interval');
29
+ }
30
+ if (value.anchorMs !== undefined && !finiteTimestamp(value.anchorMs)) {
31
+ throw new Error('cron job has an invalid interval anchor');
32
+ }
33
+ return value;
34
+ }
35
+ if (value.kind === 'at') {
36
+ const atMs = finiteTimestamp(value.atMs) ? value.atMs : typeof value.at === 'string' ? Date.parse(value.at) : Number.NaN;
37
+ if (!Number.isFinite(atMs))
38
+ throw new Error('cron job has an invalid one-shot time');
39
+ return value;
40
+ }
41
+ throw new Error('cron job uses an unsupported schedule kind');
42
+ }
43
+ export function decodeJob(value) {
44
+ if (!isRecord(value))
45
+ throw new Error('cron store contains a non-object job');
46
+ if (typeof value.id !== 'string' || !value.id || typeof value.name !== 'string' || !value.name) {
47
+ throw new Error('cron job has an invalid id or name');
48
+ }
49
+ if (!isRecord(value.payload) ||
50
+ value.payload.kind !== 'agentTurn' ||
51
+ typeof value.payload.message !== 'string' ||
52
+ !value.payload.message) {
53
+ throw new Error('cron job has an invalid agentTurn payload');
54
+ }
55
+ const agentId = typeof value.agentId === 'string' && value.agentId
56
+ ? value.agentId
57
+ : typeof value.sessionKey === 'string'
58
+ ? (value.sessionKey.split(':').filter(Boolean).at(-2) ?? 'main')
59
+ : 'main';
60
+ if (value.dshSessionId !== undefined && (typeof value.dshSessionId !== 'string' || !value.dshSessionId)) {
61
+ throw new Error('cron job has an invalid DSH session binding');
62
+ }
63
+ if (!finiteTimestamp(value.createdAtMs) || !finiteTimestamp(value.updatedAtMs)) {
64
+ throw new Error('cron job has invalid timestamps');
65
+ }
66
+ if (typeof value.enabled !== 'boolean')
67
+ throw new Error('cron job has an invalid enabled flag');
68
+ if (value.sessionTarget !== undefined && value.sessionTarget !== 'isolated') {
69
+ throw new Error('agentTurn cron jobs must use an isolated session');
70
+ }
71
+ if (value.delivery !== undefined &&
72
+ (!isRecord(value.delivery) || !['announce', 'none', 'webhook'].includes(String(value.delivery.mode)))) {
73
+ throw new Error('cron job has invalid delivery settings');
74
+ }
75
+ const state = isRecord(value.state) ? value.state : {};
76
+ for (const key of ['nextRunAtMs', 'runningAtMs', 'lastRunAtMs']) {
77
+ if (state[key] !== undefined && !finiteTimestamp(state[key]))
78
+ throw new Error(`cron job has invalid ${key}`);
79
+ }
80
+ return {
81
+ ...value,
82
+ id: value.id,
83
+ name: value.name,
84
+ enabled: value.enabled,
85
+ createdAtMs: value.createdAtMs,
86
+ updatedAtMs: value.updatedAtMs,
87
+ schedule: decodeSchedule(value.schedule),
88
+ payload: value.payload,
89
+ agentId,
90
+ sessionTarget: 'isolated',
91
+ wakeMode: 'now',
92
+ delivery: isRecord(value.delivery) ? value.delivery : { mode: 'none' },
93
+ state: state
94
+ };
95
+ }
96
+ function booleanInteger(value) {
97
+ return value === undefined ? null : value ? 1 : 0;
98
+ }
99
+ function atValue(schedule) {
100
+ if (schedule.kind !== 'at')
101
+ return null;
102
+ if (schedule.at)
103
+ return schedule.at;
104
+ return schedule.atMs === undefined ? null : new Date(schedule.atMs).toISOString();
105
+ }
106
+ function uniqueJobs(jobs) {
107
+ const unique = new Map();
108
+ for (const job of jobs) {
109
+ const decoded = decodeJob(job);
110
+ if (unique.has(decoded.id))
111
+ throw new Error(`cron store contains duplicate job id: ${decoded.id}`);
112
+ unique.set(decoded.id, decoded);
113
+ }
114
+ return [...unique.values()];
115
+ }
116
+ export class CronAutomationRepository {
117
+ file;
118
+ database;
119
+ insertJobStatement;
120
+ readOnly;
121
+ closed = false;
122
+ constructor(file, options = {}) {
123
+ this.file = file;
124
+ this.readOnly = options.readOnly === true;
125
+ if (!this.readOnly)
126
+ fs.mkdirSync(path.dirname(file), { recursive: true });
127
+ this.database = new DatabaseSync(file, { readOnly: this.readOnly, timeout: 5_000 });
128
+ if (this.readOnly)
129
+ return;
130
+ this.database.exec('PRAGMA journal_mode = WAL');
131
+ this.database.exec('PRAGMA synchronous = FULL');
132
+ this.database.exec('PRAGMA busy_timeout = 5000');
133
+ this.database.exec(`
134
+ CREATE TABLE IF NOT EXISTS cron_jobs (
135
+ store_key TEXT NOT NULL,
136
+ job_id TEXT NOT NULL,
137
+ name TEXT NOT NULL,
138
+ description TEXT,
139
+ enabled INTEGER NOT NULL,
140
+ delete_after_run INTEGER,
141
+ created_at_ms INTEGER NOT NULL,
142
+ agent_id TEXT,
143
+ session_key TEXT,
144
+ schedule_kind TEXT NOT NULL,
145
+ schedule_expr TEXT,
146
+ schedule_tz TEXT,
147
+ every_ms INTEGER,
148
+ anchor_ms INTEGER,
149
+ at TEXT,
150
+ stagger_ms INTEGER,
151
+ session_target TEXT NOT NULL,
152
+ wake_mode TEXT NOT NULL,
153
+ payload_kind TEXT NOT NULL,
154
+ payload_message TEXT,
155
+ payload_model TEXT,
156
+ payload_fallbacks_json TEXT,
157
+ payload_thinking TEXT,
158
+ payload_timeout_seconds INTEGER,
159
+ payload_allow_unsafe_external_content INTEGER,
160
+ payload_external_content_source_json TEXT,
161
+ payload_light_context INTEGER,
162
+ payload_tools_allow_json TEXT,
163
+ delivery_mode TEXT,
164
+ delivery_channel TEXT,
165
+ delivery_to TEXT,
166
+ delivery_thread_id TEXT,
167
+ delivery_account_id TEXT,
168
+ delivery_best_effort INTEGER,
169
+ delivery_completion_mode TEXT,
170
+ delivery_completion_to TEXT,
171
+ failure_delivery_mode TEXT,
172
+ failure_delivery_channel TEXT,
173
+ failure_delivery_to TEXT,
174
+ failure_delivery_account_id TEXT,
175
+ failure_alert_disabled INTEGER,
176
+ failure_alert_after INTEGER,
177
+ failure_alert_channel TEXT,
178
+ failure_alert_to TEXT,
179
+ failure_alert_cooldown_ms INTEGER,
180
+ failure_alert_include_skipped INTEGER,
181
+ failure_alert_mode TEXT,
182
+ failure_alert_account_id TEXT,
183
+ next_run_at_ms INTEGER,
184
+ running_at_ms INTEGER,
185
+ last_run_at_ms INTEGER,
186
+ last_run_status TEXT,
187
+ last_error TEXT,
188
+ last_duration_ms INTEGER,
189
+ consecutive_errors INTEGER,
190
+ consecutive_skipped INTEGER,
191
+ schedule_error_count INTEGER,
192
+ last_delivery_status TEXT,
193
+ last_delivery_error TEXT,
194
+ last_delivered INTEGER,
195
+ last_failure_alert_at_ms INTEGER,
196
+ job_json TEXT NOT NULL,
197
+ state_json TEXT NOT NULL DEFAULT '{}',
198
+ runtime_updated_at_ms INTEGER,
199
+ schedule_identity TEXT,
200
+ sort_order INTEGER NOT NULL DEFAULT 0,
201
+ updated_at INTEGER NOT NULL,
202
+ PRIMARY KEY (store_key, job_id)
203
+ );
204
+
205
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_store_updated
206
+ ON cron_jobs(store_key, sort_order ASC, updated_at DESC, job_id);
207
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_store_order
208
+ ON cron_jobs(store_key, sort_order ASC, updated_at ASC, job_id);
209
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled_next_run
210
+ ON cron_jobs(store_key, enabled, next_run_at_ms, job_id)
211
+ WHERE next_run_at_ms IS NOT NULL;
212
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_agent_session
213
+ ON cron_jobs(agent_id, session_key, updated_at DESC, job_id)
214
+ WHERE agent_id IS NOT NULL OR session_key IS NOT NULL;
215
+
216
+ CREATE TABLE IF NOT EXISTS cron_imported_jobs (
217
+ source TEXT NOT NULL,
218
+ job_id TEXT NOT NULL,
219
+ imported_at INTEGER NOT NULL,
220
+ PRIMARY KEY (source, job_id)
221
+ );
222
+ `);
223
+ this.insertJobStatement = this.database.prepare(`
224
+ INSERT INTO cron_jobs (
225
+ store_key, job_id, name, description, enabled, delete_after_run,
226
+ created_at_ms, agent_id, session_key,
227
+ schedule_kind, schedule_expr, schedule_tz, every_ms, anchor_ms, at,
228
+ session_target, wake_mode,
229
+ payload_kind, payload_message, payload_timeout_seconds,
230
+ delivery_mode, delivery_channel, delivery_to, delivery_account_id, delivery_best_effort,
231
+ next_run_at_ms, running_at_ms, last_run_at_ms, last_run_status,
232
+ job_json, state_json, runtime_updated_at_ms, sort_order, updated_at
233
+ ) VALUES (
234
+ ?, ?, ?, ?, ?, ?,
235
+ ?, ?, ?,
236
+ ?, ?, ?, ?, ?, ?,
237
+ ?, ?,
238
+ ?, ?, ?,
239
+ ?, ?, ?, ?, ?,
240
+ ?, ?, ?, ?,
241
+ ?, ?, ?, ?, ?
242
+ )
243
+ ON CONFLICT(store_key, job_id) DO UPDATE SET
244
+ name = excluded.name,
245
+ description = excluded.description,
246
+ enabled = excluded.enabled,
247
+ delete_after_run = excluded.delete_after_run,
248
+ created_at_ms = excluded.created_at_ms,
249
+ agent_id = excluded.agent_id,
250
+ session_key = excluded.session_key,
251
+ schedule_kind = excluded.schedule_kind,
252
+ schedule_expr = excluded.schedule_expr,
253
+ schedule_tz = excluded.schedule_tz,
254
+ every_ms = excluded.every_ms,
255
+ anchor_ms = excluded.anchor_ms,
256
+ at = excluded.at,
257
+ session_target = excluded.session_target,
258
+ wake_mode = excluded.wake_mode,
259
+ payload_kind = excluded.payload_kind,
260
+ payload_message = excluded.payload_message,
261
+ payload_timeout_seconds = excluded.payload_timeout_seconds,
262
+ delivery_mode = excluded.delivery_mode,
263
+ delivery_channel = excluded.delivery_channel,
264
+ delivery_to = excluded.delivery_to,
265
+ delivery_account_id = excluded.delivery_account_id,
266
+ delivery_best_effort = excluded.delivery_best_effort,
267
+ next_run_at_ms = excluded.next_run_at_ms,
268
+ running_at_ms = excluded.running_at_ms,
269
+ last_run_at_ms = excluded.last_run_at_ms,
270
+ last_run_status = excluded.last_run_status,
271
+ job_json = excluded.job_json,
272
+ state_json = excluded.state_json,
273
+ runtime_updated_at_ms = excluded.runtime_updated_at_ms,
274
+ updated_at = excluded.updated_at
275
+ `);
276
+ try {
277
+ fs.chmodSync(file, 0o600);
278
+ }
279
+ catch {
280
+ // Best effort on filesystems without POSIX modes.
281
+ }
282
+ }
283
+ async load() {
284
+ this.assertOpen();
285
+ const rows = this.database
286
+ .prepare(`
287
+ SELECT job_json
288
+ FROM cron_jobs
289
+ WHERE store_key = ?
290
+ ORDER BY sort_order ASC, updated_at ASC, job_id ASC
291
+ `)
292
+ .all(STORE_KEY);
293
+ return rows.map((row) => decodeJob(JSON.parse(row.job_json)));
294
+ }
295
+ async upsert(jobs) {
296
+ this.assertWritable();
297
+ const decoded = uniqueJobs(jobs);
298
+ if (decoded.length === 0)
299
+ return;
300
+ this.writeTransaction(() => {
301
+ const nextOrder = this.nextSortOrder();
302
+ decoded.forEach((job, index) => this.insertJob(job, nextOrder + index));
303
+ });
304
+ }
305
+ async delete(jobId) {
306
+ this.assertWritable();
307
+ const result = this.database.prepare('DELETE FROM cron_jobs WHERE store_key = ? AND job_id = ?').run(STORE_KEY, jobId);
308
+ return Number(result.changes) > 0;
309
+ }
310
+ async previewImport(source, incoming) {
311
+ this.assertOpen();
312
+ return this.planImport(source, uniqueJobs(incoming));
313
+ }
314
+ async import(source, incoming) {
315
+ this.assertWritable();
316
+ if (!source.trim())
317
+ throw new Error('cron import source must be non-empty');
318
+ const decoded = uniqueJobs(incoming);
319
+ return this.writeTransaction(() => {
320
+ const result = this.planImport(source, decoded);
321
+ const nextOrder = this.nextSortOrder();
322
+ result.added.forEach((job, index) => this.insertJob(job, nextOrder + index));
323
+ const mark = this.database.prepare(`
324
+ INSERT OR IGNORE INTO cron_imported_jobs (source, job_id, imported_at)
325
+ VALUES (?, ?, ?)
326
+ `);
327
+ const importedAt = Date.now();
328
+ for (const job of decoded)
329
+ mark.run(source, job.id, importedAt);
330
+ return result;
331
+ });
332
+ }
333
+ close() {
334
+ if (this.closed)
335
+ return;
336
+ this.closed = true;
337
+ this.database.close();
338
+ }
339
+ planImport(source, incoming) {
340
+ const existing = new Set(this.database.prepare('SELECT job_id FROM cron_jobs WHERE store_key = ?').all(STORE_KEY).map((row) => row.job_id));
341
+ const imported = new Set(this.database.prepare('SELECT job_id FROM cron_imported_jobs WHERE source = ?').all(source).map((row) => row.job_id));
342
+ const added = [];
343
+ const skippedIds = [];
344
+ for (const job of incoming) {
345
+ if (existing.has(job.id) || imported.has(job.id)) {
346
+ skippedIds.push(job.id);
347
+ continue;
348
+ }
349
+ existing.add(job.id);
350
+ added.push(job);
351
+ }
352
+ return { added, skippedIds };
353
+ }
354
+ nextSortOrder() {
355
+ const row = this.database
356
+ .prepare(`
357
+ SELECT COALESCE(MAX(sort_order), -1) + 1 AS next_order
358
+ FROM cron_jobs
359
+ WHERE store_key = ?
360
+ `)
361
+ .get(STORE_KEY);
362
+ return row.next_order;
363
+ }
364
+ insertJob(job, sortOrder) {
365
+ const statement = this.insertJobStatement;
366
+ if (!statement)
367
+ throw new Error('cron sqlite repository is read-only');
368
+ const schedule = job.schedule;
369
+ const state = job.state;
370
+ statement.run(STORE_KEY, job.id, job.name, job.description ?? null, booleanInteger(job.enabled), booleanInteger(job.deleteAfterRun), job.createdAtMs, job.agentId, job.sessionKey ?? null, schedule.kind, schedule.kind === 'cron' ? schedule.expr : null, schedule.kind === 'cron' ? (schedule.tz ?? null) : null, schedule.kind === 'every' ? schedule.everyMs : null, schedule.kind === 'every' ? (schedule.anchorMs ?? null) : null, atValue(schedule), job.sessionTarget, job.wakeMode, job.payload.kind, job.payload.message, typeof job.payload.timeoutSeconds === 'number' ? job.payload.timeoutSeconds : null, job.delivery.mode, job.delivery.channel ?? null, job.delivery.to ?? null, job.delivery.accountId ?? null, booleanInteger(job.delivery.bestEffort), state.nextRunAtMs ?? null, state.runningAtMs ?? null, state.lastRunAtMs ?? null, state.lastRunStatus ?? null, JSON.stringify(job), JSON.stringify(state), job.updatedAtMs, sortOrder, job.updatedAtMs);
371
+ }
372
+ assertOpen() {
373
+ if (this.closed)
374
+ throw new Error('cron sqlite repository is closed');
375
+ }
376
+ assertWritable() {
377
+ this.assertOpen();
378
+ if (this.readOnly)
379
+ throw new Error('cron sqlite repository is read-only');
380
+ }
381
+ writeTransaction(operation) {
382
+ this.database.exec('BEGIN IMMEDIATE');
383
+ try {
384
+ const result = operation();
385
+ this.database.exec('COMMIT');
386
+ return result;
387
+ }
388
+ catch (error) {
389
+ this.database.exec('ROLLBACK');
390
+ throw error;
391
+ }
392
+ }
393
+ }