@cronwatch/sdk 0.3.0 → 0.4.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 +8 -7
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/bugsnag.cjs +108 -0
- package/dist/bugsnag.cjs.map +1 -0
- package/dist/bugsnag.d.cts +26 -0
- package/dist/bugsnag.d.ts +26 -0
- package/dist/bugsnag.js +106 -0
- package/dist/bugsnag.js.map +1 -0
- package/dist/client-B7bqnkI5.d.cts +450 -0
- package/dist/client-pmqH3apS.d.ts +450 -0
- package/dist/d1.cjs +214 -0
- package/dist/d1.cjs.map +1 -0
- package/dist/d1.d.cts +42 -0
- package/dist/d1.d.ts +42 -0
- package/dist/d1.js +212 -0
- package/dist/d1.js.map +1 -0
- package/dist/datadog.cjs +95 -0
- package/dist/datadog.cjs.map +1 -0
- package/dist/datadog.d.cts +24 -0
- package/dist/datadog.d.ts +24 -0
- package/dist/datadog.js +93 -0
- package/dist/datadog.js.map +1 -0
- package/dist/discord.cjs +2 -0
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +1 -1
- package/dist/discord.d.ts +1 -1
- package/dist/discord.js +2 -0
- package/dist/discord.js.map +1 -1
- package/dist/email-BJ-z_epD.d.cts +21 -0
- package/dist/email-_H0faCh3.d.ts +21 -0
- package/dist/honeybadger.cjs +101 -0
- package/dist/honeybadger.cjs.map +1 -0
- package/dist/honeybadger.d.cts +23 -0
- package/dist/honeybadger.d.ts +23 -0
- package/dist/honeybadger.js +99 -0
- package/dist/honeybadger.js.map +1 -0
- package/dist/index.cjs +692 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -228
- package/dist/index.d.ts +5 -228
- package/dist/index.js +692 -124
- package/dist/index.js.map +1 -1
- package/dist/mailgun.cjs +115 -0
- package/dist/mailgun.cjs.map +1 -0
- package/dist/mailgun.d.cts +21 -0
- package/dist/mailgun.d.ts +21 -0
- package/dist/mailgun.js +113 -0
- package/dist/mailgun.js.map +1 -0
- package/dist/newrelic.cjs +88 -0
- package/dist/newrelic.cjs.map +1 -0
- package/dist/newrelic.d.cts +25 -0
- package/dist/newrelic.d.ts +25 -0
- package/dist/newrelic.js +86 -0
- package/dist/newrelic.js.map +1 -0
- package/dist/node.cjs +191 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +97 -0
- package/dist/node.d.ts +97 -0
- package/dist/node.js +186 -0
- package/dist/node.js.map +1 -0
- package/dist/pg-cron.cjs +286 -0
- package/dist/pg-cron.cjs.map +1 -0
- package/dist/pg-cron.d.cts +93 -0
- package/dist/pg-cron.d.ts +93 -0
- package/dist/pg-cron.js +280 -0
- package/dist/pg-cron.js.map +1 -0
- package/dist/postgres.cjs +33 -1
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.js +33 -1
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.cjs +106 -0
- package/dist/postmark.cjs.map +1 -0
- package/dist/postmark.d.cts +18 -0
- package/dist/postmark.d.ts +18 -0
- package/dist/postmark.js +104 -0
- package/dist/postmark.js.map +1 -0
- package/dist/resend.cjs +114 -0
- package/dist/resend.cjs.map +1 -0
- package/dist/resend.d.cts +16 -0
- package/dist/resend.d.ts +16 -0
- package/dist/resend.js +112 -0
- package/dist/resend.js.map +1 -0
- package/dist/rollbar.cjs +108 -0
- package/dist/rollbar.cjs.map +1 -0
- package/dist/rollbar.d.cts +20 -0
- package/dist/rollbar.d.ts +20 -0
- package/dist/rollbar.js +106 -0
- package/dist/rollbar.js.map +1 -0
- package/dist/sendgrid.cjs +114 -0
- package/dist/sendgrid.cjs.map +1 -0
- package/dist/sendgrid.d.cts +19 -0
- package/dist/sendgrid.d.ts +19 -0
- package/dist/sendgrid.js +112 -0
- package/dist/sendgrid.js.map +1 -0
- package/dist/sentry.cjs +130 -0
- package/dist/sentry.cjs.map +1 -0
- package/dist/sentry.d.cts +28 -0
- package/dist/sentry.d.ts +28 -0
- package/dist/sentry.js +127 -0
- package/dist/sentry.js.map +1 -0
- package/dist/ses.cjs +170 -0
- package/dist/ses.cjs.map +1 -0
- package/dist/ses.d.cts +25 -0
- package/dist/ses.d.ts +25 -0
- package/dist/ses.js +168 -0
- package/dist/ses.js.map +1 -0
- package/dist/slack.cjs +2 -0
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +2 -0
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs +62 -5
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.js +62 -5
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs +168 -0
- package/dist/twilio.cjs.map +1 -0
- package/dist/twilio.d.cts +52 -0
- package/dist/twilio.d.ts +52 -0
- package/dist/twilio.js +162 -0
- package/dist/twilio.js.map +1 -0
- package/dist/{types-C1PyRjI5.d.cts → types-Ddq1MUPL.d.cts} +50 -4
- package/dist/{types-C1PyRjI5.d.ts → types-Ddq1MUPL.d.ts} +50 -4
- package/dist/webhook.cjs +11 -5
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +6 -3
- package/dist/webhook.d.ts +6 -3
- package/dist/webhook.js +11 -6
- package/dist/webhook.js.map +1 -1
- package/package.json +211 -5
package/dist/sqlite.cjs
CHANGED
|
@@ -11,6 +11,27 @@ var path__default = /*#__PURE__*/_interopDefault(path);
|
|
|
11
11
|
|
|
12
12
|
// src/stores/sqlite.ts
|
|
13
13
|
|
|
14
|
+
// src/stores/busy.ts
|
|
15
|
+
var BUSY_RETRY_MS = 2e3;
|
|
16
|
+
function sleepSync(ms) {
|
|
17
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
18
|
+
}
|
|
19
|
+
function retryBusy(fn, budgetMs = BUSY_RETRY_MS, sleep = sleepSync) {
|
|
20
|
+
let waited = 0;
|
|
21
|
+
for (let attempt = 0; ; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return fn();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
const code = e?.code;
|
|
26
|
+
const busy = typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
|
|
27
|
+
if (!busy || waited >= budgetMs) throw e;
|
|
28
|
+
const pause = Math.min(10 * 2 ** attempt, 200, budgetMs - waited);
|
|
29
|
+
sleep(pause);
|
|
30
|
+
waited += pause;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
// src/stores/sql.ts
|
|
15
36
|
var DEFAULT_PREFIX = "cronwatch_";
|
|
16
37
|
var MAX_PREFIX = 63 - "runs_job_started".length;
|
|
@@ -55,6 +76,7 @@ function schema(dialect, p) {
|
|
|
55
76
|
function statements(dialect, p) {
|
|
56
77
|
const seq = "rowid";
|
|
57
78
|
const byName = "name";
|
|
79
|
+
const version = (column) => `COALESCE(json_extract(${column}, '$.version'), 0)`;
|
|
58
80
|
const sql = {
|
|
59
81
|
upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
60
82
|
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,
|
|
@@ -71,6 +93,11 @@ function statements(dialect, p) {
|
|
|
71
93
|
runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,
|
|
72
94
|
getState: `SELECT state FROM ${p}state WHERE job = ?`,
|
|
73
95
|
setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,
|
|
96
|
+
// compareAndSetState. Expecting version 0 also matches a missing row, so
|
|
97
|
+
// that case inserts; any other version must find its row.
|
|
98
|
+
casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)
|
|
99
|
+
ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,
|
|
100
|
+
casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version("state")} = ?`,
|
|
74
101
|
// Each job's newest run is kept whatever its age: without it, a job that
|
|
75
102
|
// runs less often than the retention looks like it never ran.
|
|
76
103
|
prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?
|
|
@@ -78,6 +105,10 @@ function statements(dialect, p) {
|
|
|
78
105
|
};
|
|
79
106
|
return sql;
|
|
80
107
|
}
|
|
108
|
+
function updateRunIfSql(dialect, p, count) {
|
|
109
|
+
const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => "?").join(", ")})`;
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
81
112
|
var params = {
|
|
82
113
|
upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
|
|
83
114
|
insertRun: (run) => [
|
|
@@ -93,7 +124,19 @@ var params = {
|
|
|
93
124
|
run.trigger
|
|
94
125
|
],
|
|
95
126
|
updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
|
|
96
|
-
|
|
127
|
+
updateRunIf: (run, fromStatuses) => [
|
|
128
|
+
run.status,
|
|
129
|
+
run.finishedAt,
|
|
130
|
+
run.durationMs,
|
|
131
|
+
run.error,
|
|
132
|
+
run.output,
|
|
133
|
+
JSON.stringify(run.metrics),
|
|
134
|
+
run.id,
|
|
135
|
+
...fromStatuses
|
|
136
|
+
],
|
|
137
|
+
setState: (state) => [state.job, JSON.stringify(state)],
|
|
138
|
+
casInsert: (state) => [state.job, JSON.stringify(state)],
|
|
139
|
+
casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
|
|
97
140
|
};
|
|
98
141
|
var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
|
|
99
142
|
var num = (v) => v === null ? null : Number(v);
|
|
@@ -141,10 +184,16 @@ function sqlite(options = {}) {
|
|
|
141
184
|
} catch {
|
|
142
185
|
}
|
|
143
186
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
const opened = new Database__default.default(file, { timeout: 0 });
|
|
188
|
+
try {
|
|
189
|
+
retryBusy(() => opened.pragma("journal_mode = WAL"));
|
|
190
|
+
opened.pragma("busy_timeout = 5000");
|
|
191
|
+
opened.pragma("synchronous = NORMAL");
|
|
192
|
+
} catch (e) {
|
|
193
|
+
opened.close();
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
db = opened;
|
|
148
197
|
return db;
|
|
149
198
|
};
|
|
150
199
|
const stmt = (text) => {
|
|
@@ -183,6 +232,10 @@ function sqlite(options = {}) {
|
|
|
183
232
|
async updateRun(run) {
|
|
184
233
|
stmt(sql.updateRun).run(...params.updateRun(run));
|
|
185
234
|
},
|
|
235
|
+
async updateRunIf(run, fromStatuses) {
|
|
236
|
+
if (fromStatuses.length === 0) return false;
|
|
237
|
+
return stmt(updateRunIfSql("sqlite", p, fromStatuses.length)).run(...params.updateRunIf(run, fromStatuses)).changes > 0;
|
|
238
|
+
},
|
|
186
239
|
async getRun(id) {
|
|
187
240
|
const r = stmt(sql.getRun).get(id);
|
|
188
241
|
return r ? rowToRun(r) : null;
|
|
@@ -204,6 +257,10 @@ function sqlite(options = {}) {
|
|
|
204
257
|
async setState(state) {
|
|
205
258
|
stmt(sql.setState).run(...params.setState(state));
|
|
206
259
|
},
|
|
260
|
+
async compareAndSetState(state, expectedVersion) {
|
|
261
|
+
const result = expectedVersion === 0 ? stmt(sql.casInsert).run(...params.casInsert(state)) : stmt(sql.casUpdate).run(...params.casUpdate(state, expectedVersion));
|
|
262
|
+
return result.changes > 0;
|
|
263
|
+
},
|
|
207
264
|
async prune(before) {
|
|
208
265
|
return stmt(sql.prune).run(before).changes;
|
|
209
266
|
},
|
package/dist/sqlite.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stores/sql.ts","../src/stores/sqlite.ts"],"names":["json","mkdirSync","path","closeSync","openSync","chmodSync","Database"],"mappings":"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","file":"sqlite.cjs","sourcesContent":["import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import Database from \"better-sqlite3\";\nimport { chmodSync, closeSync, mkdirSync, openSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Store, StoredJob, StoredJobDefinition } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface SqliteOptions {\n /** File path. The directory is created if missing. \":memory:\" works too. */\n path?: string;\n /** Bring your own open better-sqlite3 database instead. */\n database?: Database.Database;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in one SQLite file through better-sqlite3. The right\n * choice for a single server. WAL mode, so the app's reads never block on a\n * run being written.\n */\nexport function sqlite(options: SqliteOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"sqlite\", p);\n let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n const cache = new Map<string, Database.Statement>();\n\n const open = (): Database.Database => {\n if (db) return db;\n const onDisk = file !== \":memory:\" && file !== \"\";\n if (onDisk) {\n mkdirSync(path.dirname(file), { recursive: true });\n // Create the file private before SQLite opens it. SQLite gives the -wal and -shm files the\n // main file's mode, so the whole set stays 0600; the chmods cover files left by an earlier open.\n try {\n closeSync(openSync(file, \"a\", 0o600));\n for (const f of [file, `${file}-wal`, `${file}-shm`]) {\n try { chmodSync(f, 0o600); } catch { /* the sidecar may not exist yet */ }\n }\n } catch { /* best effort: not every filesystem cares */ }\n }\n db = new Database(file);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.pragma(\"synchronous = NORMAL\");\n return db;\n };\n\n // Each statement is prepared once per open database.\n const stmt = (text: string): Database.Statement => {\n let s = cache.get(text);\n if (!s) {\n s = open().prepare(text);\n cache.set(text, s);\n }\n return s;\n };\n\n const forget = (name: string) => {\n stmt(sql.deleteRuns).run(name);\n stmt(sql.deleteState).run(name);\n stmt(sql.deleteJob).run(name);\n };\n\n return {\n async init() {\n open().exec(schema(\"sqlite\", p));\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n stmt(sql.upsertJob).run(...params.upsertJob(definition, now));\n },\n async getJob(name) {\n const r = stmt(sql.getJob).get(name) as JobRow | undefined;\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (stmt(sql.listJobs).all() as JobRow[]).map(rowToJob);\n },\n async deleteJob(name) {\n open().transaction(forget)(name);\n },\n async insertRun(run) {\n stmt(sql.insertRun).run(...params.insertRun(run));\n },\n async updateRun(run) {\n stmt(sql.updateRun).run(...params.updateRun(run));\n },\n async getRun(id) {\n const r = stmt(sql.getRun).get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (stmt(sql.listRuns).all(job, limit) as RunRow[]).map(rowToRun);\n },\n async lastRun(job) {\n const r = stmt(sql.listRuns).get(job, 1) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (stmt(sql.runningRuns).all() as RunRow[]).map(rowToRun);\n },\n async getState(job) {\n const r = stmt(sql.getState).get(job) as StateRow | undefined;\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n stmt(sql.setState).run(...params.setState(state));\n },\n async prune(before) {\n return stmt(sql.prune).run(before).changes;\n },\n async close() {\n cache.clear();\n if (db && !options.database) {\n db.close();\n db = null;\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/stores/busy.ts","../src/stores/sql.ts","../src/stores/sqlite.ts"],"names":["json","mkdirSync","path","closeSync","openSync","chmodSync","Database"],"mappings":"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","file":"sqlite.cjs","sourcesContent":["/** How long opening SQLite keeps retrying a busy database before it gives up. */\nexport const BUSY_RETRY_MS = 2_000;\n\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/**\n * Runs `fn`, retrying while SQLite answers SQLITE_BUSY (or SQLITE_LOCKED),\n * with a short growing pause, for up to `budgetMs` in all. Opening a\n * better-sqlite3 database is synchronous, so the pause is too.\n */\nexport function retryBusy<T>(fn: () => T, budgetMs = BUSY_RETRY_MS, sleep: (ms: number) => void = sleepSync): T {\n let waited = 0;\n for (let attempt = 0; ; attempt++) {\n try {\n return fn();\n } catch (e) {\n const code = (e as { code?: unknown } | null)?.code;\n const busy = typeof code === \"string\" && (code.startsWith(\"SQLITE_BUSY\") || code.startsWith(\"SQLITE_LOCKED\"));\n if (!busy || waited >= budgetMs) throw e;\n const pause = Math.min(10 * 2 ** attempt, 200, budgetMs - waited);\n sleep(pause);\n waited += pause;\n }\n }\n}\n","import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, 0 when it has none.\n const version = (column: string) => (pg ? `COALESCE((${column}->>'version')::bigint, 0)` : `COALESCE(json_extract(${column}, '$.version'), 0)`);\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n casInsert: (state: JobState) => [state.job, JSON.stringify(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [JSON.stringify(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import Database from \"better-sqlite3\";\nimport { chmodSync, closeSync, mkdirSync, openSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Store, StoredJob, StoredJobDefinition } from \"../types.js\";\nimport { retryBusy } from \"./busy.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface SqliteOptions {\n /** File path. The directory is created if missing. \":memory:\" works too. */\n path?: string;\n /** Bring your own open better-sqlite3 database instead. */\n database?: Database.Database;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in one SQLite file through better-sqlite3. The right\n * choice for a single server. WAL mode, so the app's reads never block on a\n * run being written.\n */\nexport function sqlite(options: SqliteOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"sqlite\", p);\n let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n const cache = new Map<string, Database.Statement>();\n\n const open = (): Database.Database => {\n if (db) return db;\n const onDisk = file !== \":memory:\" && file !== \"\";\n if (onDisk) {\n mkdirSync(path.dirname(file), { recursive: true });\n // Create the file private before SQLite opens it. SQLite gives the -wal and -shm files the\n // main file's mode, so the whole set stays 0600; the chmods cover files left by an earlier open.\n try {\n closeSync(openSync(file, \"a\", 0o600));\n for (const f of [file, `${file}-wal`, `${file}-shm`]) {\n try { chmodSync(f, 0o600); } catch { /* the sidecar may not exist yet */ }\n }\n } catch { /* best effort: not every filesystem cares */ }\n }\n // No busy handler until WAL is on: switching journal mode can answer\n // SQLITE_BUSY at once while another process is doing the same on a new\n // file, so that is retried here. The database is kept only once every\n // pragma has gone through; a failed open is tried afresh next time.\n const opened = new Database(file, { timeout: 0 });\n try {\n retryBusy(() => opened.pragma(\"journal_mode = WAL\"));\n opened.pragma(\"busy_timeout = 5000\");\n opened.pragma(\"synchronous = NORMAL\");\n } catch (e) {\n opened.close();\n throw e;\n }\n db = opened;\n return db;\n };\n\n // Each statement is prepared once per open database.\n const stmt = (text: string): Database.Statement => {\n let s = cache.get(text);\n if (!s) {\n s = open().prepare(text);\n cache.set(text, s);\n }\n return s;\n };\n\n const forget = (name: string) => {\n stmt(sql.deleteRuns).run(name);\n stmt(sql.deleteState).run(name);\n stmt(sql.deleteJob).run(name);\n };\n\n return {\n async init() {\n open().exec(schema(\"sqlite\", p));\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n stmt(sql.upsertJob).run(...params.upsertJob(definition, now));\n },\n async getJob(name) {\n const r = stmt(sql.getJob).get(name) as JobRow | undefined;\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (stmt(sql.listJobs).all() as JobRow[]).map(rowToJob);\n },\n async deleteJob(name) {\n open().transaction(forget)(name);\n },\n async insertRun(run) {\n stmt(sql.insertRun).run(...params.insertRun(run));\n },\n async updateRun(run) {\n stmt(sql.updateRun).run(...params.updateRun(run));\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n return stmt(updateRunIfSql(\"sqlite\", p, fromStatuses.length)).run(...params.updateRunIf(run, fromStatuses)).changes > 0;\n },\n async getRun(id) {\n const r = stmt(sql.getRun).get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (stmt(sql.listRuns).all(job, limit) as RunRow[]).map(rowToRun);\n },\n async lastRun(job) {\n const r = stmt(sql.listRuns).get(job, 1) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (stmt(sql.runningRuns).all() as RunRow[]).map(rowToRun);\n },\n async getState(job) {\n const r = stmt(sql.getState).get(job) as StateRow | undefined;\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n stmt(sql.setState).run(...params.setState(state));\n },\n async compareAndSetState(state, expectedVersion) {\n const result = expectedVersion === 0\n ? stmt(sql.casInsert).run(...params.casInsert(state))\n : stmt(sql.casUpdate).run(...params.casUpdate(state, expectedVersion));\n return result.changes > 0;\n },\n async prune(before) {\n return stmt(sql.prune).run(before).changes;\n },\n async close() {\n cache.clear();\n if (db && !options.database) {\n db.close();\n db = null;\n }\n },\n };\n}\n"]}
|
package/dist/sqlite.d.cts
CHANGED
package/dist/sqlite.d.ts
CHANGED
package/dist/sqlite.js
CHANGED
|
@@ -4,6 +4,27 @@ import path from 'path';
|
|
|
4
4
|
|
|
5
5
|
// src/stores/sqlite.ts
|
|
6
6
|
|
|
7
|
+
// src/stores/busy.ts
|
|
8
|
+
var BUSY_RETRY_MS = 2e3;
|
|
9
|
+
function sleepSync(ms) {
|
|
10
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11
|
+
}
|
|
12
|
+
function retryBusy(fn, budgetMs = BUSY_RETRY_MS, sleep = sleepSync) {
|
|
13
|
+
let waited = 0;
|
|
14
|
+
for (let attempt = 0; ; attempt++) {
|
|
15
|
+
try {
|
|
16
|
+
return fn();
|
|
17
|
+
} catch (e) {
|
|
18
|
+
const code = e?.code;
|
|
19
|
+
const busy = typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
|
|
20
|
+
if (!busy || waited >= budgetMs) throw e;
|
|
21
|
+
const pause = Math.min(10 * 2 ** attempt, 200, budgetMs - waited);
|
|
22
|
+
sleep(pause);
|
|
23
|
+
waited += pause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
// src/stores/sql.ts
|
|
8
29
|
var DEFAULT_PREFIX = "cronwatch_";
|
|
9
30
|
var MAX_PREFIX = 63 - "runs_job_started".length;
|
|
@@ -48,6 +69,7 @@ function schema(dialect, p) {
|
|
|
48
69
|
function statements(dialect, p) {
|
|
49
70
|
const seq = "rowid";
|
|
50
71
|
const byName = "name";
|
|
72
|
+
const version = (column) => `COALESCE(json_extract(${column}, '$.version'), 0)`;
|
|
51
73
|
const sql = {
|
|
52
74
|
upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
53
75
|
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,
|
|
@@ -64,6 +86,11 @@ function statements(dialect, p) {
|
|
|
64
86
|
runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,
|
|
65
87
|
getState: `SELECT state FROM ${p}state WHERE job = ?`,
|
|
66
88
|
setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,
|
|
89
|
+
// compareAndSetState. Expecting version 0 also matches a missing row, so
|
|
90
|
+
// that case inserts; any other version must find its row.
|
|
91
|
+
casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)
|
|
92
|
+
ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,
|
|
93
|
+
casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version("state")} = ?`,
|
|
67
94
|
// Each job's newest run is kept whatever its age: without it, a job that
|
|
68
95
|
// runs less often than the retention looks like it never ran.
|
|
69
96
|
prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?
|
|
@@ -71,6 +98,10 @@ function statements(dialect, p) {
|
|
|
71
98
|
};
|
|
72
99
|
return sql;
|
|
73
100
|
}
|
|
101
|
+
function updateRunIfSql(dialect, p, count) {
|
|
102
|
+
const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => "?").join(", ")})`;
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
74
105
|
var params = {
|
|
75
106
|
upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
|
|
76
107
|
insertRun: (run) => [
|
|
@@ -86,7 +117,19 @@ var params = {
|
|
|
86
117
|
run.trigger
|
|
87
118
|
],
|
|
88
119
|
updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
|
|
89
|
-
|
|
120
|
+
updateRunIf: (run, fromStatuses) => [
|
|
121
|
+
run.status,
|
|
122
|
+
run.finishedAt,
|
|
123
|
+
run.durationMs,
|
|
124
|
+
run.error,
|
|
125
|
+
run.output,
|
|
126
|
+
JSON.stringify(run.metrics),
|
|
127
|
+
run.id,
|
|
128
|
+
...fromStatuses
|
|
129
|
+
],
|
|
130
|
+
setState: (state) => [state.job, JSON.stringify(state)],
|
|
131
|
+
casInsert: (state) => [state.job, JSON.stringify(state)],
|
|
132
|
+
casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
|
|
90
133
|
};
|
|
91
134
|
var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
|
|
92
135
|
var num = (v) => v === null ? null : Number(v);
|
|
@@ -134,10 +177,16 @@ function sqlite(options = {}) {
|
|
|
134
177
|
} catch {
|
|
135
178
|
}
|
|
136
179
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
180
|
+
const opened = new Database(file, { timeout: 0 });
|
|
181
|
+
try {
|
|
182
|
+
retryBusy(() => opened.pragma("journal_mode = WAL"));
|
|
183
|
+
opened.pragma("busy_timeout = 5000");
|
|
184
|
+
opened.pragma("synchronous = NORMAL");
|
|
185
|
+
} catch (e) {
|
|
186
|
+
opened.close();
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
db = opened;
|
|
141
190
|
return db;
|
|
142
191
|
};
|
|
143
192
|
const stmt = (text) => {
|
|
@@ -176,6 +225,10 @@ function sqlite(options = {}) {
|
|
|
176
225
|
async updateRun(run) {
|
|
177
226
|
stmt(sql.updateRun).run(...params.updateRun(run));
|
|
178
227
|
},
|
|
228
|
+
async updateRunIf(run, fromStatuses) {
|
|
229
|
+
if (fromStatuses.length === 0) return false;
|
|
230
|
+
return stmt(updateRunIfSql("sqlite", p, fromStatuses.length)).run(...params.updateRunIf(run, fromStatuses)).changes > 0;
|
|
231
|
+
},
|
|
179
232
|
async getRun(id) {
|
|
180
233
|
const r = stmt(sql.getRun).get(id);
|
|
181
234
|
return r ? rowToRun(r) : null;
|
|
@@ -197,6 +250,10 @@ function sqlite(options = {}) {
|
|
|
197
250
|
async setState(state) {
|
|
198
251
|
stmt(sql.setState).run(...params.setState(state));
|
|
199
252
|
},
|
|
253
|
+
async compareAndSetState(state, expectedVersion) {
|
|
254
|
+
const result = expectedVersion === 0 ? stmt(sql.casInsert).run(...params.casInsert(state)) : stmt(sql.casUpdate).run(...params.casUpdate(state, expectedVersion));
|
|
255
|
+
return result.changes > 0;
|
|
256
|
+
},
|
|
200
257
|
async prune(before) {
|
|
201
258
|
return stmt(sql.prune).run(before).changes;
|
|
202
259
|
},
|
package/dist/sqlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stores/sql.ts","../src/stores/sqlite.ts"],"names":["json"],"mappings":"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","file":"sqlite.js","sourcesContent":["import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import Database from \"better-sqlite3\";\nimport { chmodSync, closeSync, mkdirSync, openSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Store, StoredJob, StoredJobDefinition } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface SqliteOptions {\n /** File path. The directory is created if missing. \":memory:\" works too. */\n path?: string;\n /** Bring your own open better-sqlite3 database instead. */\n database?: Database.Database;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in one SQLite file through better-sqlite3. The right\n * choice for a single server. WAL mode, so the app's reads never block on a\n * run being written.\n */\nexport function sqlite(options: SqliteOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"sqlite\", p);\n let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n const cache = new Map<string, Database.Statement>();\n\n const open = (): Database.Database => {\n if (db) return db;\n const onDisk = file !== \":memory:\" && file !== \"\";\n if (onDisk) {\n mkdirSync(path.dirname(file), { recursive: true });\n // Create the file private before SQLite opens it. SQLite gives the -wal and -shm files the\n // main file's mode, so the whole set stays 0600; the chmods cover files left by an earlier open.\n try {\n closeSync(openSync(file, \"a\", 0o600));\n for (const f of [file, `${file}-wal`, `${file}-shm`]) {\n try { chmodSync(f, 0o600); } catch { /* the sidecar may not exist yet */ }\n }\n } catch { /* best effort: not every filesystem cares */ }\n }\n db = new Database(file);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.pragma(\"synchronous = NORMAL\");\n return db;\n };\n\n // Each statement is prepared once per open database.\n const stmt = (text: string): Database.Statement => {\n let s = cache.get(text);\n if (!s) {\n s = open().prepare(text);\n cache.set(text, s);\n }\n return s;\n };\n\n const forget = (name: string) => {\n stmt(sql.deleteRuns).run(name);\n stmt(sql.deleteState).run(name);\n stmt(sql.deleteJob).run(name);\n };\n\n return {\n async init() {\n open().exec(schema(\"sqlite\", p));\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n stmt(sql.upsertJob).run(...params.upsertJob(definition, now));\n },\n async getJob(name) {\n const r = stmt(sql.getJob).get(name) as JobRow | undefined;\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (stmt(sql.listJobs).all() as JobRow[]).map(rowToJob);\n },\n async deleteJob(name) {\n open().transaction(forget)(name);\n },\n async insertRun(run) {\n stmt(sql.insertRun).run(...params.insertRun(run));\n },\n async updateRun(run) {\n stmt(sql.updateRun).run(...params.updateRun(run));\n },\n async getRun(id) {\n const r = stmt(sql.getRun).get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (stmt(sql.listRuns).all(job, limit) as RunRow[]).map(rowToRun);\n },\n async lastRun(job) {\n const r = stmt(sql.listRuns).get(job, 1) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (stmt(sql.runningRuns).all() as RunRow[]).map(rowToRun);\n },\n async getState(job) {\n const r = stmt(sql.getState).get(job) as StateRow | undefined;\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n stmt(sql.setState).run(...params.setState(state));\n },\n async prune(before) {\n return stmt(sql.prune).run(before).changes;\n },\n async close() {\n cache.clear();\n if (db && !options.database) {\n db.close();\n db = null;\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/stores/busy.ts","../src/stores/sql.ts","../src/stores/sqlite.ts"],"names":["json"],"mappings":"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","file":"sqlite.js","sourcesContent":["/** How long opening SQLite keeps retrying a busy database before it gives up. */\nexport const BUSY_RETRY_MS = 2_000;\n\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/**\n * Runs `fn`, retrying while SQLite answers SQLITE_BUSY (or SQLITE_LOCKED),\n * with a short growing pause, for up to `budgetMs` in all. Opening a\n * better-sqlite3 database is synchronous, so the pause is too.\n */\nexport function retryBusy<T>(fn: () => T, budgetMs = BUSY_RETRY_MS, sleep: (ms: number) => void = sleepSync): T {\n let waited = 0;\n for (let attempt = 0; ; attempt++) {\n try {\n return fn();\n } catch (e) {\n const code = (e as { code?: unknown } | null)?.code;\n const busy = typeof code === \"string\" && (code.startsWith(\"SQLITE_BUSY\") || code.startsWith(\"SQLITE_LOCKED\"));\n if (!busy || waited >= budgetMs) throw e;\n const pause = Math.min(10 * 2 ** attempt, 200, budgetMs - waited);\n sleep(pause);\n waited += pause;\n }\n }\n}\n","import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, 0 when it has none.\n const version = (column: string) => (pg ? `COALESCE((${column}->>'version')::bigint, 0)` : `COALESCE(json_extract(${column}, '$.version'), 0)`);\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n casInsert: (state: JobState) => [state.job, JSON.stringify(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [JSON.stringify(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import Database from \"better-sqlite3\";\nimport { chmodSync, closeSync, mkdirSync, openSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Store, StoredJob, StoredJobDefinition } from \"../types.js\";\nimport { retryBusy } from \"./busy.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface SqliteOptions {\n /** File path. The directory is created if missing. \":memory:\" works too. */\n path?: string;\n /** Bring your own open better-sqlite3 database instead. */\n database?: Database.Database;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in one SQLite file through better-sqlite3. The right\n * choice for a single server. WAL mode, so the app's reads never block on a\n * run being written.\n */\nexport function sqlite(options: SqliteOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"sqlite\", p);\n let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n const cache = new Map<string, Database.Statement>();\n\n const open = (): Database.Database => {\n if (db) return db;\n const onDisk = file !== \":memory:\" && file !== \"\";\n if (onDisk) {\n mkdirSync(path.dirname(file), { recursive: true });\n // Create the file private before SQLite opens it. SQLite gives the -wal and -shm files the\n // main file's mode, so the whole set stays 0600; the chmods cover files left by an earlier open.\n try {\n closeSync(openSync(file, \"a\", 0o600));\n for (const f of [file, `${file}-wal`, `${file}-shm`]) {\n try { chmodSync(f, 0o600); } catch { /* the sidecar may not exist yet */ }\n }\n } catch { /* best effort: not every filesystem cares */ }\n }\n // No busy handler until WAL is on: switching journal mode can answer\n // SQLITE_BUSY at once while another process is doing the same on a new\n // file, so that is retried here. The database is kept only once every\n // pragma has gone through; a failed open is tried afresh next time.\n const opened = new Database(file, { timeout: 0 });\n try {\n retryBusy(() => opened.pragma(\"journal_mode = WAL\"));\n opened.pragma(\"busy_timeout = 5000\");\n opened.pragma(\"synchronous = NORMAL\");\n } catch (e) {\n opened.close();\n throw e;\n }\n db = opened;\n return db;\n };\n\n // Each statement is prepared once per open database.\n const stmt = (text: string): Database.Statement => {\n let s = cache.get(text);\n if (!s) {\n s = open().prepare(text);\n cache.set(text, s);\n }\n return s;\n };\n\n const forget = (name: string) => {\n stmt(sql.deleteRuns).run(name);\n stmt(sql.deleteState).run(name);\n stmt(sql.deleteJob).run(name);\n };\n\n return {\n async init() {\n open().exec(schema(\"sqlite\", p));\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n stmt(sql.upsertJob).run(...params.upsertJob(definition, now));\n },\n async getJob(name) {\n const r = stmt(sql.getJob).get(name) as JobRow | undefined;\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (stmt(sql.listJobs).all() as JobRow[]).map(rowToJob);\n },\n async deleteJob(name) {\n open().transaction(forget)(name);\n },\n async insertRun(run) {\n stmt(sql.insertRun).run(...params.insertRun(run));\n },\n async updateRun(run) {\n stmt(sql.updateRun).run(...params.updateRun(run));\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n return stmt(updateRunIfSql(\"sqlite\", p, fromStatuses.length)).run(...params.updateRunIf(run, fromStatuses)).changes > 0;\n },\n async getRun(id) {\n const r = stmt(sql.getRun).get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (stmt(sql.listRuns).all(job, limit) as RunRow[]).map(rowToRun);\n },\n async lastRun(job) {\n const r = stmt(sql.listRuns).get(job, 1) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (stmt(sql.runningRuns).all() as RunRow[]).map(rowToRun);\n },\n async getState(job) {\n const r = stmt(sql.getState).get(job) as StateRow | undefined;\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n stmt(sql.setState).run(...params.setState(state));\n },\n async compareAndSetState(state, expectedVersion) {\n const result = expectedVersion === 0\n ? stmt(sql.casInsert).run(...params.casInsert(state))\n : stmt(sql.casUpdate).run(...params.casUpdate(state, expectedVersion));\n return result.changes > 0;\n },\n async prune(before) {\n return stmt(sql.prune).run(before).changes;\n },\n async close() {\n cache.clear();\n if (db && !options.database) {\n db.close();\n db = null;\n }\n },\n };\n}\n"]}
|
package/dist/twilio.cjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/alerts/shared.ts
|
|
4
|
+
var TIMEOUT_MS = 1e4;
|
|
5
|
+
function origin(url) {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(url).origin;
|
|
8
|
+
} catch {
|
|
9
|
+
return "(invalid URL)";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
var ERROR_BODY_MAX = 200;
|
|
13
|
+
async function post(provider, url, init, secrets = []) {
|
|
14
|
+
const response = await fetch(url, { method: "POST", headers: init.headers, body: init.body, redirect: "error", signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
let text = "";
|
|
17
|
+
try {
|
|
18
|
+
text = await response.text();
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : ""}`);
|
|
22
|
+
}
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
function errorBody(text, secrets = []) {
|
|
26
|
+
const kept = secrets.filter((s) => typeof s === "string" && s.length >= 4);
|
|
27
|
+
const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);
|
|
28
|
+
let head = cut(text, ERROR_BODY_MAX + longest);
|
|
29
|
+
for (const secret of kept) head = head.split(secret).join("[redacted]");
|
|
30
|
+
return cut(head, ERROR_BODY_MAX);
|
|
31
|
+
}
|
|
32
|
+
function trimmed(value) {
|
|
33
|
+
return typeof value === "string" ? value.trim() : "";
|
|
34
|
+
}
|
|
35
|
+
function base64(text) {
|
|
36
|
+
let binary = "";
|
|
37
|
+
for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
|
|
38
|
+
return btoa(binary);
|
|
39
|
+
}
|
|
40
|
+
function basicAuth(user, password) {
|
|
41
|
+
return `Basic ${base64(`${user}:${password}`)}`;
|
|
42
|
+
}
|
|
43
|
+
function cut(text, max) {
|
|
44
|
+
if (text.length <= max) return text;
|
|
45
|
+
let end = max;
|
|
46
|
+
const code = text.charCodeAt(end - 1);
|
|
47
|
+
if (code >= 55296 && code <= 56319) end -= 1;
|
|
48
|
+
return text.slice(0, end);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/alerts/twilio.ts
|
|
52
|
+
var MAX_SEGMENTS = 10;
|
|
53
|
+
var MAX_BODY = 1600;
|
|
54
|
+
function twilio(options) {
|
|
55
|
+
const accountSid = trimmed(options.accountSid);
|
|
56
|
+
if (!accountSid) throw new Error("twilio() needs an accountSid");
|
|
57
|
+
const apiKeySid = trimmed(options.apiKeySid);
|
|
58
|
+
const user = apiKeySid || accountSid;
|
|
59
|
+
const password = apiKeySid ? trimmed(options.apiKeySecret) : trimmed(options.authToken);
|
|
60
|
+
if (!password) throw new Error("twilio() needs an authToken, or an apiKeySid and apiKeySecret");
|
|
61
|
+
if (!options.from && !options.messagingServiceSid) throw new Error("twilio() needs a from number or a messagingServiceSid");
|
|
62
|
+
const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((n) => typeof n === "string" && n.trim() !== "").map((n) => n.trim());
|
|
63
|
+
if (to.length === 0) throw new Error("twilio() needs at least one to number");
|
|
64
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(accountSid)}/Messages.json`;
|
|
65
|
+
const authorization = basicAuth(user, password);
|
|
66
|
+
const segments = segmentBudget(options.segments);
|
|
67
|
+
return {
|
|
68
|
+
name: "twilio",
|
|
69
|
+
async send(alert, context) {
|
|
70
|
+
if (alert.type === "recovered" && !options.recovered) return;
|
|
71
|
+
const body = smsBody(alert, options.link?.(alert), segments);
|
|
72
|
+
const results = await Promise.allSettled(to.map((number) => {
|
|
73
|
+
const form = new URLSearchParams();
|
|
74
|
+
form.append("To", number);
|
|
75
|
+
if (options.messagingServiceSid) form.append("MessagingServiceSid", options.messagingServiceSid);
|
|
76
|
+
else form.append("From", options.from);
|
|
77
|
+
form.append("Body", body);
|
|
78
|
+
return post("Twilio", url, { headers: { "content-type": "application/x-www-form-urlencoded", authorization }, body: form.toString() }, [password]);
|
|
79
|
+
}));
|
|
80
|
+
const failed = results.flatMap((r, i) => r.status === "rejected" ? [{ number: to[i], error: r.reason }] : []);
|
|
81
|
+
if (failed.length === 0) return;
|
|
82
|
+
if (failed.length === to.length) {
|
|
83
|
+
const message = failed[0].error.message;
|
|
84
|
+
throw new Error(to.length > 1 ? `${message} (${failed.length} of ${to.length} numbers failed)` : message);
|
|
85
|
+
}
|
|
86
|
+
for (const { number, error } of failed) {
|
|
87
|
+
const report = new Error(`${error.message} (to ${maskNumber(number)}; ${to.length - failed.length} of ${to.length} numbers took the alert)`);
|
|
88
|
+
if (context) context.onError(report);
|
|
89
|
+
else console.error("[cronwatch] alert channel twilio:", report);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function maskNumber(number) {
|
|
95
|
+
return number.length <= 4 ? number : `${"*".repeat(Math.min(number.length - 4, 8))}${number.slice(-4)}`;
|
|
96
|
+
}
|
|
97
|
+
var GSM = `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
|
|
98
|
+
\xD8\xF8\r\xC5\xE5\u0394_\u03A6\u0393\u039B\u03A9\u03A0\u03A8\u03A3\u0398\u039E\xC6\xE6\xDF\xC9 !"#\xA4%&'()*+,-./0123456789:;<=>?\xA1ABCDEFGHIJKLMNOPQRSTUVWXYZ\xC4\xD6\xD1\xDC\xA7\xBFabcdefghijklmnopqrstuvwxyz\xE4\xF6\xF1\xFC\xE0`;
|
|
99
|
+
var GSM_EXTENDED = "^{}\\[~]|\u20AC\f";
|
|
100
|
+
function smsSegments(text) {
|
|
101
|
+
const units = [];
|
|
102
|
+
let gsm = true;
|
|
103
|
+
for (const ch of text) {
|
|
104
|
+
if (GSM.includes(ch)) units.push(1);
|
|
105
|
+
else if (GSM_EXTENDED.includes(ch)) units.push(2);
|
|
106
|
+
else {
|
|
107
|
+
gsm = false;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const [single, per, sizes] = gsm ? [160, 153, units] : [70, 67, Array.from(text, (ch) => ch.length)];
|
|
112
|
+
const total = sizes.reduce((n, u) => n + u, 0);
|
|
113
|
+
if (total <= single) return 1;
|
|
114
|
+
let count = 1;
|
|
115
|
+
let used = 0;
|
|
116
|
+
for (const u of sizes) {
|
|
117
|
+
if (used + u > per) {
|
|
118
|
+
count += 1;
|
|
119
|
+
used = 0;
|
|
120
|
+
}
|
|
121
|
+
used += u;
|
|
122
|
+
}
|
|
123
|
+
return count;
|
|
124
|
+
}
|
|
125
|
+
function fits(text, segments) {
|
|
126
|
+
return text.length <= MAX_BODY && smsSegments(text) <= segments;
|
|
127
|
+
}
|
|
128
|
+
function smsBody(alert, link, segments = 3) {
|
|
129
|
+
const budget = segmentBudget(segments);
|
|
130
|
+
const tail = link ? `
|
|
131
|
+
${link}` : "";
|
|
132
|
+
const lines = [alert.title, ...alert.message.split("\n").filter((l) => l.trim() !== ""), ...alert.triage ? [`Triage: ${alert.triage}`] : []];
|
|
133
|
+
let text = "";
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const next = text ? `${text}
|
|
136
|
+
${line}` : line;
|
|
137
|
+
if (fits(next + tail, budget)) {
|
|
138
|
+
text = next;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const chars = Array.from(line);
|
|
142
|
+
let lo = 0;
|
|
143
|
+
let hi = chars.length;
|
|
144
|
+
while (lo < hi) {
|
|
145
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
146
|
+
const candidate = (text ? `${text}
|
|
147
|
+
` : "") + chars.slice(0, mid).join("") + "...";
|
|
148
|
+
if (fits(candidate + tail, budget)) lo = mid;
|
|
149
|
+
else hi = mid - 1;
|
|
150
|
+
}
|
|
151
|
+
if (lo > 0) text = (text ? `${text}
|
|
152
|
+
` : "") + chars.slice(0, lo).join("") + "...";
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
return cut(text + tail, MAX_BODY);
|
|
156
|
+
}
|
|
157
|
+
function segmentBudget(segments) {
|
|
158
|
+
const n = typeof segments === "number" && Number.isFinite(segments) ? Math.floor(segments) : 3;
|
|
159
|
+
return Math.min(MAX_SEGMENTS, Math.max(1, n));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
exports.MAX_BODY = MAX_BODY;
|
|
163
|
+
exports.MAX_SEGMENTS = MAX_SEGMENTS;
|
|
164
|
+
exports.smsBody = smsBody;
|
|
165
|
+
exports.smsSegments = smsSegments;
|
|
166
|
+
exports.twilio = twilio;
|
|
167
|
+
//# sourceMappingURL=twilio.cjs.map
|
|
168
|
+
//# sourceMappingURL=twilio.cjs.map
|