@cronwatch/sdk 0.1.0 → 0.3.1
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/LICENSE +21 -0
- package/README.md +27 -12
- package/dist/anthropic.cjs +19 -5
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/anthropic.js +19 -5
- package/dist/anthropic.js.map +1 -1
- package/dist/discord.cjs +14 -2
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +6 -2
- package/dist/discord.d.ts +6 -2
- package/dist/discord.js +13 -3
- package/dist/discord.js.map +1 -1
- package/dist/index.cjs +835 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -45
- package/dist/index.d.ts +133 -45
- package/dist/index.js +836 -291
- package/dist/index.js.map +1 -1
- package/dist/postgres.cjs +152 -73
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +2 -2
- package/dist/postgres.d.ts +2 -2
- package/dist/postgres.js +152 -73
- package/dist/postgres.js.map +1 -1
- package/dist/slack.cjs +12 -6
- 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 +12 -6
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs +184 -74
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +3 -1
- package/dist/sqlite.d.ts +3 -1
- package/dist/sqlite.js +185 -75
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-BQ1P8z55.d.cts → types-DUt2apl0.d.cts} +84 -7
- package/dist/{types-BQ1P8z55.d.ts → types-DUt2apl0.d.ts} +84 -7
- package/dist/webhook.cjs +10 -2
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +10 -2
- package/dist/webhook.js.map +1 -1
- package/package.json +97 -34
package/dist/postgres.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stores/postgres.ts"],"names":[],"mappings":";;;AAkBA,IAAM,MAAM,CAAC,CAAA,KAA8C,MAAM,IAAA,GAAO,IAAA,GAAO,OAAO,CAAC,CAAA;AAEvF,SAAS,SAAS,CAAA,EAAgB;AAChC,EAAA,OAAO;AAAA,IACL,IAAI,CAAA,CAAE,EAAA;AAAA,IAAI,KAAK,CAAA,CAAE,GAAA;AAAA,IAAK,QAAQ,CAAA,CAAE,MAAA;AAAA,IAAQ,SAAA,EAAW,MAAA,CAAO,CAAA,CAAE,UAAU,CAAA;AAAA,IAAG,UAAA,EAAY,GAAA,CAAI,CAAA,CAAE,WAAW,CAAA;AAAA,IACtG,UAAA,EAAY,GAAA,CAAI,CAAA,CAAE,WAAW,CAAA;AAAA,IAAG,OAAO,CAAA,CAAE,KAAA;AAAA,IAAO,QAAQ,CAAA,CAAE,MAAA;AAAA,IAAQ,OAAA,EAAS,CAAA,CAAE,OAAA,IAAW,EAAC;AAAA,IAAG,SAAS,CAAA,CAAE;AAAA,GACzG;AACF;AAOO,SAAS,QAAA,CAAS,OAAA,GAA2B,EAAC,EAAU;AAC7D,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,CAAQ,IAAA;AACvB,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,IAAQ,IAAI,EAAA,CAAG,IAAA,CAAK,EAAE,gBAAA,EAAkB,OAAA,CAAQ,gBAAA,IAAoB,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA;AACnH,EAAA,MAAM,KAAK,OAAA,CAAQ,MAAA,IAAU,YAAA,EAAc,OAAA,CAAQ,gBAAgB,EAAE,CAAA;AACrE,EAAA,MAAM,IAAA,GAAO,CAAA,EAAG,CAAC,CAAA,IAAA,CAAA,EAAQ,IAAA,GAAO,GAAG,CAAC,CAAA,IAAA,CAAA,EAAQ,KAAA,GAAQ,CAAA,EAAG,CAAC,CAAA,KAAA,CAAA;AAExD,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAO;AACX,MAAA,MAAM,KAAK,KAAA,CAAM;AAAA,mCAAA,EACc,IAAI,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAAA,EAMJ,IAAI,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAAA,EAaJ,IAAI,mBAAmB,IAAI,CAAA;AAAA,mCAAA,EAC3B,IAAI,eAAe,IAAI,CAAA;AAAA,mCAAA,EACvB,KAAK,CAAA;AAAA;AAAA;AAAA;AAAA,MAAA,CAInC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,MAAM,SAAA,CAAU,UAAA,EAAY,GAAA,EAAK;AAC/B,MAAA,MAAM,IAAA,CAAK,KAAA;AAAA,QACT,eAAe,IAAI,CAAA;AAAA,4GAAA,CAAA;AAAA,QAEnB,CAAC,UAAA,CAAW,IAAA,EAAM,KAAK,SAAA,CAAU,UAAU,GAAG,GAAG;AAAA,OACnD;AAAA,IACF,CAAA;AAAA,IACA,MAAM,OAAO,IAAA,EAAM;AACjB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,CAAA,cAAA,EAAiB,IAAI,CAAA,gBAAA,CAAA,EAAoB,CAAC,IAAI,CAAC,CAAA;AACzF,MAAA,MAAM,CAAA,GAAI,KAAK,CAAC,CAAA;AAChB,MAAA,OAAO,IAAI,EAAE,IAAA,EAAM,EAAE,IAAA,EAAM,UAAA,EAAY,EAAE,UAAA,EAAY,SAAA,EAAW,MAAA,CAAO,CAAA,CAAE,UAAU,CAAA,EAAG,SAAA,EAAW,OAAO,CAAA,CAAE,UAAU,GAAE,GAAI,IAAA;AAAA,IAC5H,CAAA;AAAA,IACA,MAAM,QAAA,GAAiC;AACrC,MAAA,MAAM,EAAE,MAAK,GAAI,MAAM,KAAK,KAAA,CAAc,CAAA,cAAA,EAAiB,IAAI,CAAA,cAAA,CAAgB,CAAA;AAC/E,MAAA,OAAO,IAAA,CAAK,IAAI,CAAC,CAAA,MAAO,EAAE,IAAA,EAAM,CAAA,CAAE,MAAM,UAAA,EAAY,CAAA,CAAE,YAAY,SAAA,EAAW,MAAA,CAAO,EAAE,UAAU,CAAA,EAAG,WAAW,MAAA,CAAO,CAAA,CAAE,UAAU,CAAA,EAAE,CAAE,CAAA;AAAA,IACvI,CAAA;AAAA,IACA,MAAM,UAAU,IAAA,EAAM;AACpB,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA,EAAQ;AAClC,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,CAAO,MAAM,OAAO,CAAA;AAC1B,QAAA,MAAM,OAAO,KAAA,CAAM,CAAA,YAAA,EAAe,IAAI,CAAA,eAAA,CAAA,EAAmB,CAAC,IAAI,CAAC,CAAA;AAC/D,QAAA,MAAM,OAAO,KAAA,CAAM,CAAA,YAAA,EAAe,KAAK,CAAA,eAAA,CAAA,EAAmB,CAAC,IAAI,CAAC,CAAA;AAChE,QAAA,MAAM,OAAO,KAAA,CAAM,CAAA,YAAA,EAAe,IAAI,CAAA,gBAAA,CAAA,EAAoB,CAAC,IAAI,CAAC,CAAA;AAChE,QAAA,MAAM,MAAA,CAAO,MAAM,QAAQ,CAAA;AAAA,MAC7B,SAAS,CAAA,EAAG;AACV,QAAA,MAAM,MAAA,CAAO,MAAM,UAAU,CAAA;AAC7B,QAAA,MAAM,CAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,MAAA,CAAO,OAAA,EAAQ;AAAA,MACjB;AAAA,IACF,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,MAAM,IAAA,CAAK,KAAA;AAAA,QACT,eAAe,IAAI,CAAA;AAAA,yDAAA,CAAA;AAAA,QAEnB,CAAC,IAAI,EAAA,EAAI,GAAA,CAAI,KAAK,GAAA,CAAI,MAAA,EAAQ,GAAA,CAAI,SAAA,EAAW,GAAA,CAAI,UAAA,EAAY,IAAI,UAAA,EAAY,GAAA,CAAI,KAAA,EAAO,GAAA,CAAI,MAAA,EAAQ,IAAA,CAAK,UAAU,GAAA,CAAI,OAAO,CAAA,EAAG,GAAA,CAAI,OAAO;AAAA,OAC9I;AAAA,IACF,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,MAAM,IAAA,CAAK,KAAA;AAAA,QACT,UAAU,IAAI,CAAA,yGAAA,CAAA;AAAA,QACd,CAAC,GAAA,CAAI,MAAA,EAAQ,GAAA,CAAI,UAAA,EAAY,IAAI,UAAA,EAAY,GAAA,CAAI,KAAA,EAAO,GAAA,CAAI,QAAQ,IAAA,CAAK,SAAA,CAAU,IAAI,OAAO,CAAA,EAAG,IAAI,EAAE;AAAA,OACzG;AAAA,IACF,CAAA;AAAA,IACA,MAAM,OAAO,EAAA,EAAI;AACf,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,CAAA,cAAA,EAAiB,IAAI,CAAA,cAAA,CAAA,EAAkB,CAAC,EAAE,CAAC,CAAA;AACrF,MAAA,OAAO,KAAK,CAAC,CAAA,GAAI,SAAS,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,IACvC,CAAA;AAAA,IACA,MAAM,QAAA,CAAS,GAAA,EAAK,KAAA,EAAO;AACzB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,CAAA,cAAA,EAAiB,IAAI,CAAA,2DAAA,CAAA,EAA+D,CAAC,GAAA,EAAK,KAAK,CAAC,CAAA;AAC1I,MAAA,OAAO,IAAA,CAAK,IAAI,QAAQ,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,MAAM,QAAQ,GAAA,EAAK;AACjB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAAc,CAAA,cAAA,EAAiB,IAAI,CAAA,0DAAA,CAAA,EAA8D,CAAC,GAAG,CAAC,CAAA;AAClI,MAAA,OAAO,KAAK,CAAC,CAAA,GAAI,SAAS,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,IACvC,CAAA;AAAA,IACA,MAAM,WAAA,GAAc;AAClB,MAAA,MAAM,EAAE,MAAK,GAAI,MAAM,KAAK,KAAA,CAAc,CAAA,cAAA,EAAiB,IAAI,CAAA,6CAAA,CAA+C,CAAA;AAC9G,MAAA,OAAO,IAAA,CAAK,IAAI,QAAQ,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,MAAM,SAAS,GAAA,EAAK;AAClB,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,KAAA,CAA2B,CAAA,kBAAA,EAAqB,KAAK,CAAA,eAAA,CAAA,EAAmB,CAAC,GAAG,CAAC,CAAA;AACzG,MAAA,OAAO,IAAA,CAAK,CAAC,CAAA,EAAG,KAAA,IAAS,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,SAAS,KAAA,EAAO;AACpB,MAAA,MAAM,IAAA,CAAK,KAAA;AAAA,QACT,eAAe,KAAK,CAAA,oFAAA,CAAA;AAAA,QACpB,CAAC,KAAA,CAAM,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC;AAAA,OACnC;AAAA,IACF,CAAA;AAAA,IACA,MAAM,MAAM,MAAA,EAAQ;AAClB,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA,CAAM,eAAe,IAAI,CAAA,8CAAA,CAAA,EAAkD,CAAC,MAAM,CAAC,CAAA;AAC7G,MAAA,OAAO,OAAO,QAAA,IAAY,CAAA;AAAA,IAC5B,CAAA;AAAA,IACA,MAAM,KAAA,GAAQ;AACZ,MAAA,IAAI,KAAA,EAAO,MAAM,IAAA,CAAK,GAAA,EAAI;AAAA,IAC5B;AAAA,GACF;AACF","file":"postgres.js","sourcesContent":["import pg from \"pg\";\nimport type { JobState, Run, Store, StoredJob, StoredJobDefinition } from \"../types.js\";\n\nexport interface PostgresOptions {\n /** postgres://... Defaults to process.env.DATABASE_URL. */\n connectionString?: string;\n /** Bring your own pool instead. It is not closed by close(). */\n pool?: pg.Pool;\n /** Table name prefix. Default \"cronwatch_\". */\n prefix?: string;\n}\n\ninterface JobRow { name: string; definition: StoredJobDefinition; created_at: string | number; updated_at: string | number }\ninterface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: string | number; finished_at: string | number | null;\n duration_ms: string | number | null; error: string | null; output: string | null; metrics: Record<string, number>; trigger: string;\n}\n\nconst num = (v: string | number | null): number | null => (v === null ? null : Number(v));\n\nfunction 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 ?? {}, trigger: r.trigger,\n };\n}\n\n/**\n * Keeps everything in Postgres through the `pg` driver. For apps on Vercel,\n * Neon, Supabase, Railway and the like, where there is no disk to keep a\n * SQLite file on. Times are stored as BIGINT epoch milliseconds.\n */\nexport function postgres(options: PostgresOptions = {}): Store {\n const owned = !options.pool;\n const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });\n const p = (options.prefix ?? \"cronwatch_\").replace(/[^a-z0-9_]/gi, \"\");\n const JOBS = `${p}jobs`, RUNS = `${p}runs`, STATE = `${p}state`;\n\n return {\n async init() {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS ${JOBS} (\n name TEXT PRIMARY KEY,\n definition JSONB NOT NULL,\n created_at BIGINT NOT NULL,\n updated_at BIGINT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${RUNS} (\n seq BIGSERIAL,\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at BIGINT NOT NULL,\n finished_at BIGINT,\n duration_ms BIGINT,\n error TEXT,\n output TEXT,\n metrics JSONB NOT NULL DEFAULT '{}'::jsonb,\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${RUNS}_job_started ON ${RUNS} (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${RUNS}_running ON ${RUNS} (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${STATE} (\n job TEXT PRIMARY KEY,\n state JSONB NOT NULL\n );\n `);\n },\n async upsertJob(definition, now) {\n await pool.query(\n `INSERT INTO ${JOBS} (name, definition, created_at, updated_at) VALUES ($1, $2, $3, $3)\n ON CONFLICT (name) DO UPDATE SET definition = EXCLUDED.definition, updated_at = EXCLUDED.updated_at`,\n [definition.name, JSON.stringify(definition), now],\n );\n },\n async getJob(name) {\n const { rows } = await pool.query<JobRow>(`SELECT * FROM ${JOBS} WHERE name = $1`, [name]);\n const r = rows[0];\n return r ? { name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) } : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const { rows } = await pool.query<JobRow>(`SELECT * FROM ${JOBS} ORDER BY name`);\n return rows.map((r) => ({ name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) }));\n },\n async deleteJob(name) {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(`DELETE FROM ${RUNS} WHERE job = $1`, [name]);\n await client.query(`DELETE FROM ${STATE} WHERE job = $1`, [name]);\n await client.query(`DELETE FROM ${JOBS} WHERE name = $1`, [name]);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n },\n async insertRun(run) {\n await pool.query(\n `INSERT INTO ${RUNS} (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,\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 },\n async updateRun(run) {\n await pool.query(\n `UPDATE ${RUNS} SET status = $1, finished_at = $2, duration_ms = $3, error = $4, output = $5, metrics = $6 WHERE id = $7`,\n [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n );\n },\n async getRun(id) {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE id = $1`, [id]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async listRuns(job, limit) {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT $2`, [job, limit]);\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT 1`, [job]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async runningRuns() {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE status = 'running' ORDER BY started_at`);\n return rows.map(rowToRun);\n },\n async getState(job) {\n const { rows } = await pool.query<{ state: JobState }>(`SELECT state FROM ${STATE} WHERE job = $1`, [job]);\n return rows[0]?.state ?? null;\n },\n async setState(state) {\n await pool.query(\n `INSERT INTO ${STATE} (job, state) VALUES ($1, $2) ON CONFLICT (job) DO UPDATE SET state = EXCLUDED.state`,\n [state.job, JSON.stringify(state)],\n );\n },\n async prune(before) {\n const result = await pool.query(`DELETE FROM ${RUNS} WHERE status <> 'running' AND started_at < $1`, [before]);\n return result.rowCount ?? 0;\n },\n async close() {\n if (owned) await pool.end();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/stores/sql.ts","../src/stores/postgres.ts"],"names":["json"],"mappings":"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","file":"postgres.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 // 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// 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 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 pg from \"pg\";\nimport type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface PostgresOptions {\n /** postgres://... Defaults to process.env.DATABASE_URL. */\n connectionString?: string;\n /** Bring your own pool instead. It is not closed by close(). */\n pool?: pg.Pool;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in Postgres through the `pg` driver. For apps on Vercel,\n * Neon, Supabase, Railway and the like, where there is no disk to keep a\n * SQLite file on. Times are stored as BIGINT epoch milliseconds.\n */\nexport function postgres(options: PostgresOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"postgres\", p);\n const owned = !options.pool;\n const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });\n\n const transaction = async (work: (client: pg.PoolClient) => Promise<void>) => {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n await work(client);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n };\n\n return {\n async init() {\n // Many instances starting at once would race CREATE TABLE IF NOT EXISTS, which Postgres can\n // reject with a unique violation on pg_type. A lock per prefix makes them take turns.\n await transaction(async (client) => {\n await client.query(\"SELECT pg_advisory_xact_lock(hashtext($1))\", [`cronwatch:${p}`]);\n await client.query(schema(\"postgres\", p));\n });\n },\n async upsertJob(definition, now) {\n await pool.query(sql.upsertJob, params.upsertJob(definition, now));\n },\n async getJob(name) {\n const { rows } = await pool.query<JobRow>(sql.getJob, [name]);\n return rows[0] ? rowToJob(rows[0]) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const { rows } = await pool.query<JobRow>(sql.listJobs);\n return rows.map(rowToJob);\n },\n async deleteJob(name) {\n await transaction(async (client) => {\n await client.query(sql.deleteRuns, [name]);\n await client.query(sql.deleteState, [name]);\n await client.query(sql.deleteJob, [name]);\n });\n },\n async insertRun(run) {\n await pool.query(sql.insertRun, params.insertRun(run));\n },\n async updateRun(run) {\n await pool.query(sql.updateRun, params.updateRun(run));\n },\n async getRun(id) {\n const { rows } = await pool.query<RunRow>(sql.getRun, [id]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async listRuns(job, limit) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, limit]);\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, 1]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async runningRuns() {\n const { rows } = await pool.query<RunRow>(sql.runningRuns);\n return rows.map(rowToRun);\n },\n async getState(job) {\n const { rows } = await pool.query<StateRow>(sql.getState, [job]);\n return rows[0] ? rowToState(rows[0]) : null;\n },\n async setState(state) {\n await pool.query(sql.setState, params.setState(state));\n },\n async compareAndSetState(state, expectedVersion) {\n const result = expectedVersion === 0\n ? await pool.query(sql.casInsert, params.casInsert(state))\n : await pool.query(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return (result.rowCount ?? 0) > 0;\n },\n async prune(before) {\n const result = await pool.query(sql.prune, [before]);\n return result.rowCount ?? 0;\n },\n async close() {\n if (owned) await pool.end();\n },\n };\n}\n"]}
|
package/dist/slack.cjs
CHANGED
|
@@ -9,6 +9,7 @@ var EMOJI = {
|
|
|
9
9
|
over_budget: ":moneybag:",
|
|
10
10
|
recovered: ":white_check_mark:"
|
|
11
11
|
};
|
|
12
|
+
var TIMEOUT_MS = 1e4;
|
|
12
13
|
function slack(options) {
|
|
13
14
|
if (!options.webhookUrl) throw new Error("slack() needs a webhookUrl");
|
|
14
15
|
return {
|
|
@@ -16,18 +17,20 @@ function slack(options) {
|
|
|
16
17
|
async send(alert) {
|
|
17
18
|
const url = options.link?.(alert);
|
|
18
19
|
const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : ""}`;
|
|
19
|
-
const body = escape(alert.message);
|
|
20
|
-
const triage = alert.triage ? `
|
|
21
|
-
_Triage:_ ${escape(alert.triage)}` : "";
|
|
20
|
+
const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);
|
|
22
21
|
const response = await fetch(options.webhookUrl, {
|
|
23
22
|
method: "POST",
|
|
24
23
|
headers: { "content-type": "application/json" },
|
|
24
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
25
25
|
body: JSON.stringify({
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.
|
|
27
|
+
text: escape(`${alert.title}
|
|
28
|
+
${alert.message}`),
|
|
28
29
|
blocks: [
|
|
29
30
|
{ type: "section", text: { type: "mrkdwn", text: title } },
|
|
30
|
-
{ type: "section", text: { type: "mrkdwn", text: "```" + body
|
|
31
|
+
{ type: "section", text: { type: "mrkdwn", text: "```" + body + "```" } },
|
|
32
|
+
// Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.
|
|
33
|
+
...alert.triage ? [{ type: "section", text: { type: "mrkdwn", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3e3) } }] : []
|
|
31
34
|
]
|
|
32
35
|
})
|
|
33
36
|
});
|
|
@@ -38,6 +41,9 @@ ${alert.message}`,
|
|
|
38
41
|
function escape(text) {
|
|
39
42
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
40
43
|
}
|
|
44
|
+
function codeBlockSafe(text) {
|
|
45
|
+
return text.replace(/```/g, "`\u200B`\u200B`");
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
exports.slack = slack;
|
|
43
49
|
//# sourceMappingURL=slack.cjs.map
|
package/dist/slack.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/slack.ts"],"names":[],"mappings":";;;AASA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,0BAAA;AAAA,EACR,MAAA,EAAQ,KAAA;AAAA,EACR,KAAA,EAAO,YAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;
|
|
1
|
+
{"version":3,"sources":["../src/alerts/slack.ts"],"names":[],"mappings":";;;AASA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,0BAAA;AAAA,EACR,MAAA,EAAQ,KAAA;AAAA,EACR,KAAA,EAAO,YAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;AAEA,IAAM,UAAA,GAAa,GAAA;AAGZ,SAAS,MAAM,OAAA,EAAqC;AACzD,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,EAAY,MAAM,IAAI,MAAM,4BAA4B,CAAA;AACrE,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AAChC,MAAA,MAAM,QAAQ,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,IAAI,CAAC,CAAA,EAAA,EAAK,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA,CAAA,EAAI,GAAA,GAAM,CAAA,GAAA,EAAM,GAAG,YAAY,EAAE,CAAA,CAAA;AAC3F,MAAA,MAAM,IAAA,GAAO,cAAc,MAAA,CAAO,KAAA,CAAM,OAAO,CAAC,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA;AAC/D,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY;AAAA,QAC/C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,UAAU,CAAA;AAAA,QACtC,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA;AAAA,UAEnB,IAAA,EAAM,MAAA,CAAO,CAAA,EAAG,KAAA,CAAM,KAAK;AAAA,EAAK,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,UAC/C,MAAA,EAAQ;AAAA,YACN,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,EAAM,EAAE;AAAA,YACzD,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,GAAQ,IAAA,GAAO,KAAA,EAAM,EAAE;AAAA;AAAA,YAExE,GAAI,KAAA,CAAM,MAAA,GAAS,CAAC,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,aAAa,MAAA,CAAO,KAAA,CAAM,MAAM,CAAC,CAAA,CAAA,CAAG,KAAA,CAAM,CAAA,EAAG,GAAI,CAAA,EAAE,EAAG,CAAA,GAAI;AAAC;AAClI,SACD;AAAA,OACF,CAAA;AACD,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,QAAU,IAAI,KAAA,CAAM,0BAA0B,QAAA,CAAS,MAAM,CAAA,EAAA,EAAA,CAAM,MAAM,SAAS,IAAA,EAAK,EAAG,MAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IACzH;AAAA,GACF;AACF;AAGA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA;AAC/E;AAGA,SAAS,cAAc,IAAA,EAAsB;AAC3C,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,iBAAO,CAAA;AACrC","file":"slack.cjs","sourcesContent":["import type { Alert, AlertChannel } from \"../types.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n recovered: \":white_check_mark:\",\n};\n\nconst TIMEOUT_MS = 10_000;\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);\n const response = await fetch(options.webhookUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n signal: AbortSignal.timeout(TIMEOUT_MS),\n body: JSON.stringify({\n // The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.\n text: escape(`${alert.title}\\n${alert.message}`),\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body + \"```\" } },\n // Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.\n ...(alert.triage ? [{ type: \"section\", text: { type: \"mrkdwn\", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3000) } }] : []),\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\n/** Slack's three control characters. Escaping < and > also stops <!channel> and <url|links>. */\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Breaks up ``` so text inside a code block cannot close it. */\nfunction codeBlockSafe(text: string): string {\n return text.replace(/```/g, \"```\");\n}\n"]}
|
package/dist/slack.d.cts
CHANGED
package/dist/slack.d.ts
CHANGED
package/dist/slack.js
CHANGED
|
@@ -7,6 +7,7 @@ var EMOJI = {
|
|
|
7
7
|
over_budget: ":moneybag:",
|
|
8
8
|
recovered: ":white_check_mark:"
|
|
9
9
|
};
|
|
10
|
+
var TIMEOUT_MS = 1e4;
|
|
10
11
|
function slack(options) {
|
|
11
12
|
if (!options.webhookUrl) throw new Error("slack() needs a webhookUrl");
|
|
12
13
|
return {
|
|
@@ -14,18 +15,20 @@ function slack(options) {
|
|
|
14
15
|
async send(alert) {
|
|
15
16
|
const url = options.link?.(alert);
|
|
16
17
|
const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : ""}`;
|
|
17
|
-
const body = escape(alert.message);
|
|
18
|
-
const triage = alert.triage ? `
|
|
19
|
-
_Triage:_ ${escape(alert.triage)}` : "";
|
|
18
|
+
const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);
|
|
20
19
|
const response = await fetch(options.webhookUrl, {
|
|
21
20
|
method: "POST",
|
|
22
21
|
headers: { "content-type": "application/json" },
|
|
22
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
23
23
|
body: JSON.stringify({
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.
|
|
25
|
+
text: escape(`${alert.title}
|
|
26
|
+
${alert.message}`),
|
|
26
27
|
blocks: [
|
|
27
28
|
{ type: "section", text: { type: "mrkdwn", text: title } },
|
|
28
|
-
{ type: "section", text: { type: "mrkdwn", text: "```" + body
|
|
29
|
+
{ type: "section", text: { type: "mrkdwn", text: "```" + body + "```" } },
|
|
30
|
+
// Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.
|
|
31
|
+
...alert.triage ? [{ type: "section", text: { type: "mrkdwn", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3e3) } }] : []
|
|
29
32
|
]
|
|
30
33
|
})
|
|
31
34
|
});
|
|
@@ -36,6 +39,9 @@ ${alert.message}`,
|
|
|
36
39
|
function escape(text) {
|
|
37
40
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
38
41
|
}
|
|
42
|
+
function codeBlockSafe(text) {
|
|
43
|
+
return text.replace(/```/g, "`\u200B`\u200B`");
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
export { slack };
|
|
41
47
|
//# sourceMappingURL=slack.js.map
|
package/dist/slack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/slack.ts"],"names":[],"mappings":";AASA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,0BAAA;AAAA,EACR,MAAA,EAAQ,KAAA;AAAA,EACR,KAAA,EAAO,YAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;
|
|
1
|
+
{"version":3,"sources":["../src/alerts/slack.ts"],"names":[],"mappings":";AASA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,0BAAA;AAAA,EACR,MAAA,EAAQ,KAAA;AAAA,EACR,KAAA,EAAO,YAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;AAEA,IAAM,UAAA,GAAa,GAAA;AAGZ,SAAS,MAAM,OAAA,EAAqC;AACzD,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,EAAY,MAAM,IAAI,MAAM,4BAA4B,CAAA;AACrE,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AAChC,MAAA,MAAM,QAAQ,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,IAAI,CAAC,CAAA,EAAA,EAAK,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA,CAAA,EAAI,GAAA,GAAM,CAAA,GAAA,EAAM,GAAG,YAAY,EAAE,CAAA,CAAA;AAC3F,MAAA,MAAM,IAAA,GAAO,cAAc,MAAA,CAAO,KAAA,CAAM,OAAO,CAAC,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA;AAC/D,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY;AAAA,QAC/C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,UAAU,CAAA;AAAA,QACtC,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA;AAAA,UAEnB,IAAA,EAAM,MAAA,CAAO,CAAA,EAAG,KAAA,CAAM,KAAK;AAAA,EAAK,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,UAC/C,MAAA,EAAQ;AAAA,YACN,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,EAAM,EAAE;AAAA,YACzD,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,GAAQ,IAAA,GAAO,KAAA,EAAM,EAAE;AAAA;AAAA,YAExE,GAAI,KAAA,CAAM,MAAA,GAAS,CAAC,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,aAAa,MAAA,CAAO,KAAA,CAAM,MAAM,CAAC,CAAA,CAAA,CAAG,KAAA,CAAM,CAAA,EAAG,GAAI,CAAA,EAAE,EAAG,CAAA,GAAI;AAAC;AAClI,SACD;AAAA,OACF,CAAA;AACD,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,QAAU,IAAI,KAAA,CAAM,0BAA0B,QAAA,CAAS,MAAM,CAAA,EAAA,EAAA,CAAM,MAAM,SAAS,IAAA,EAAK,EAAG,MAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IACzH;AAAA,GACF;AACF;AAGA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA;AAC/E;AAGA,SAAS,cAAc,IAAA,EAAsB;AAC3C,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,iBAAO,CAAA;AACrC","file":"slack.js","sourcesContent":["import type { Alert, AlertChannel } from \"../types.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n recovered: \":white_check_mark:\",\n};\n\nconst TIMEOUT_MS = 10_000;\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);\n const response = await fetch(options.webhookUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n signal: AbortSignal.timeout(TIMEOUT_MS),\n body: JSON.stringify({\n // The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.\n text: escape(`${alert.title}\\n${alert.message}`),\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body + \"```\" } },\n // Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.\n ...(alert.triage ? [{ type: \"section\", text: { type: \"mrkdwn\", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3000) } }] : []),\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\n/** Slack's three control characters. Escaping < and > also stops <!channel> and <url|links>. */\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Breaks up ``` so text inside a code block cannot close it. */\nfunction codeBlockSafe(text: string): string {\n return text.replace(/```/g, \"```\");\n}\n"]}
|
package/dist/sqlite.cjs
CHANGED
|
@@ -10,134 +10,244 @@ var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
|
10
10
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
11
|
|
|
12
12
|
// src/stores/sqlite.ts
|
|
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
|
+
|
|
35
|
+
// src/stores/sql.ts
|
|
36
|
+
var DEFAULT_PREFIX = "cronwatch_";
|
|
37
|
+
var MAX_PREFIX = 63 - "runs_job_started".length;
|
|
38
|
+
function tablePrefix(prefix = DEFAULT_PREFIX) {
|
|
39
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, not starting with a digit, at most ${MAX_PREFIX} characters.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return prefix;
|
|
45
|
+
}
|
|
46
|
+
function schema(dialect, p) {
|
|
47
|
+
const int = "INTEGER";
|
|
48
|
+
const json2 = "TEXT";
|
|
49
|
+
return `
|
|
50
|
+
CREATE TABLE IF NOT EXISTS ${p}jobs (
|
|
51
|
+
name TEXT PRIMARY KEY,
|
|
52
|
+
definition ${json2} NOT NULL,
|
|
53
|
+
created_at ${int} NOT NULL,
|
|
54
|
+
updated_at ${int} NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS ${p}runs (${""}
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
job TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL,
|
|
60
|
+
started_at ${int} NOT NULL,
|
|
61
|
+
finished_at ${int},
|
|
62
|
+
duration_ms ${int},
|
|
63
|
+
error TEXT,
|
|
64
|
+
output TEXT,
|
|
65
|
+
metrics ${json2} NOT NULL DEFAULT '{}',
|
|
66
|
+
trigger TEXT NOT NULL DEFAULT 'run'
|
|
67
|
+
);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';
|
|
70
|
+
CREATE TABLE IF NOT EXISTS ${p}state (
|
|
71
|
+
job TEXT PRIMARY KEY,
|
|
72
|
+
state ${json2} NOT NULL
|
|
73
|
+
);
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
function statements(dialect, p) {
|
|
77
|
+
const seq = "rowid";
|
|
78
|
+
const byName = "name";
|
|
79
|
+
const version = (column) => `COALESCE(json_extract(${column}, '$.version'), 0)`;
|
|
80
|
+
const sql = {
|
|
81
|
+
upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
82
|
+
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,
|
|
83
|
+
getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,
|
|
84
|
+
listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,
|
|
85
|
+
deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,
|
|
86
|
+
deleteState: `DELETE FROM ${p}state WHERE job = ?`,
|
|
87
|
+
deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,
|
|
88
|
+
insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
90
|
+
updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,
|
|
91
|
+
getRun: `SELECT * FROM ${p}runs WHERE id = ?`,
|
|
92
|
+
listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,
|
|
93
|
+
runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,
|
|
94
|
+
getState: `SELECT state FROM ${p}state WHERE job = ?`,
|
|
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")} = ?`,
|
|
101
|
+
// Each job's newest run is kept whatever its age: without it, a job that
|
|
102
|
+
// runs less often than the retention looks like it never ran.
|
|
103
|
+
prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?
|
|
104
|
+
AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`
|
|
105
|
+
};
|
|
106
|
+
return sql;
|
|
107
|
+
}
|
|
108
|
+
var params = {
|
|
109
|
+
upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
|
|
110
|
+
insertRun: (run) => [
|
|
111
|
+
run.id,
|
|
112
|
+
run.job,
|
|
113
|
+
run.status,
|
|
114
|
+
run.startedAt,
|
|
115
|
+
run.finishedAt,
|
|
116
|
+
run.durationMs,
|
|
117
|
+
run.error,
|
|
118
|
+
run.output,
|
|
119
|
+
JSON.stringify(run.metrics),
|
|
120
|
+
run.trigger
|
|
121
|
+
],
|
|
122
|
+
updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
|
|
123
|
+
setState: (state) => [state.job, JSON.stringify(state)],
|
|
124
|
+
casInsert: (state) => [state.job, JSON.stringify(state)],
|
|
125
|
+
casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
|
|
126
|
+
};
|
|
127
|
+
var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
|
|
128
|
+
var num = (v) => v === null ? null : Number(v);
|
|
129
|
+
function rowToJob(r) {
|
|
130
|
+
return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
|
|
131
|
+
}
|
|
13
132
|
function rowToRun(r) {
|
|
14
133
|
return {
|
|
15
134
|
id: r.id,
|
|
16
135
|
job: r.job,
|
|
17
136
|
status: r.status,
|
|
18
|
-
startedAt: r.started_at,
|
|
19
|
-
finishedAt: r.finished_at,
|
|
20
|
-
durationMs: r.duration_ms,
|
|
137
|
+
startedAt: Number(r.started_at),
|
|
138
|
+
finishedAt: num(r.finished_at),
|
|
139
|
+
durationMs: num(r.duration_ms),
|
|
21
140
|
error: r.error,
|
|
22
141
|
output: r.output,
|
|
23
|
-
metrics:
|
|
142
|
+
metrics: r.metrics === null ? {} : json(r.metrics),
|
|
24
143
|
trigger: r.trigger
|
|
25
144
|
};
|
|
26
145
|
}
|
|
146
|
+
function rowToState(r) {
|
|
147
|
+
return json(r.state);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/stores/sqlite.ts
|
|
27
151
|
function sqlite(options = {}) {
|
|
152
|
+
const p = tablePrefix(options.prefix);
|
|
153
|
+
const sql = statements("sqlite", p);
|
|
28
154
|
let db = options.database ?? null;
|
|
29
155
|
const file = options.path ?? "./data/cronwatch.db";
|
|
156
|
+
const cache = /* @__PURE__ */ new Map();
|
|
30
157
|
const open = () => {
|
|
31
158
|
if (db) return db;
|
|
32
|
-
|
|
159
|
+
const onDisk = file !== ":memory:" && file !== "";
|
|
160
|
+
if (onDisk) {
|
|
33
161
|
fs.mkdirSync(path__default.default.dirname(file), { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
db = new Database__default.default(file);
|
|
36
|
-
if (file !== ":memory:") {
|
|
37
162
|
try {
|
|
38
|
-
fs.
|
|
163
|
+
fs.closeSync(fs.openSync(file, "a", 384));
|
|
164
|
+
for (const f of [file, `${file}-wal`, `${file}-shm`]) {
|
|
165
|
+
try {
|
|
166
|
+
fs.chmodSync(f, 384);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
39
170
|
} catch {
|
|
40
171
|
}
|
|
41
172
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
173
|
+
const opened = new Database__default.default(file, { timeout: 0 });
|
|
174
|
+
try {
|
|
175
|
+
retryBusy(() => opened.pragma("journal_mode = WAL"));
|
|
176
|
+
opened.pragma("busy_timeout = 5000");
|
|
177
|
+
opened.pragma("synchronous = NORMAL");
|
|
178
|
+
} catch (e) {
|
|
179
|
+
opened.close();
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
db = opened;
|
|
45
183
|
return db;
|
|
46
184
|
};
|
|
185
|
+
const stmt = (text) => {
|
|
186
|
+
let s = cache.get(text);
|
|
187
|
+
if (!s) {
|
|
188
|
+
s = open().prepare(text);
|
|
189
|
+
cache.set(text, s);
|
|
190
|
+
}
|
|
191
|
+
return s;
|
|
192
|
+
};
|
|
193
|
+
const forget = (name) => {
|
|
194
|
+
stmt(sql.deleteRuns).run(name);
|
|
195
|
+
stmt(sql.deleteState).run(name);
|
|
196
|
+
stmt(sql.deleteJob).run(name);
|
|
197
|
+
};
|
|
47
198
|
return {
|
|
48
199
|
async init() {
|
|
49
|
-
|
|
50
|
-
d.exec(`
|
|
51
|
-
CREATE TABLE IF NOT EXISTS cronwatch_jobs (
|
|
52
|
-
name TEXT PRIMARY KEY,
|
|
53
|
-
definition TEXT NOT NULL,
|
|
54
|
-
created_at INTEGER NOT NULL,
|
|
55
|
-
updated_at INTEGER NOT NULL
|
|
56
|
-
);
|
|
57
|
-
CREATE TABLE IF NOT EXISTS cronwatch_runs (
|
|
58
|
-
id TEXT PRIMARY KEY,
|
|
59
|
-
job TEXT NOT NULL,
|
|
60
|
-
status TEXT NOT NULL,
|
|
61
|
-
started_at INTEGER NOT NULL,
|
|
62
|
-
finished_at INTEGER,
|
|
63
|
-
duration_ms INTEGER,
|
|
64
|
-
error TEXT,
|
|
65
|
-
output TEXT,
|
|
66
|
-
metrics TEXT NOT NULL DEFAULT '{}',
|
|
67
|
-
trigger TEXT NOT NULL DEFAULT 'run'
|
|
68
|
-
);
|
|
69
|
-
CREATE INDEX IF NOT EXISTS cronwatch_runs_job_started ON cronwatch_runs (job, started_at DESC);
|
|
70
|
-
CREATE INDEX IF NOT EXISTS cronwatch_runs_running ON cronwatch_runs (status) WHERE status = 'running';
|
|
71
|
-
CREATE TABLE IF NOT EXISTS cronwatch_state (
|
|
72
|
-
job TEXT PRIMARY KEY,
|
|
73
|
-
state TEXT NOT NULL
|
|
74
|
-
);
|
|
75
|
-
`);
|
|
200
|
+
open().exec(schema("sqlite", p));
|
|
76
201
|
},
|
|
77
202
|
async upsertJob(definition, now) {
|
|
78
|
-
|
|
79
|
-
INSERT INTO cronwatch_jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
80
|
-
ON CONFLICT(name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at
|
|
81
|
-
`).run(definition.name, JSON.stringify(definition), now, now);
|
|
203
|
+
stmt(sql.upsertJob).run(...params.upsertJob(definition, now));
|
|
82
204
|
},
|
|
83
205
|
async getJob(name) {
|
|
84
|
-
const r =
|
|
85
|
-
return r ?
|
|
206
|
+
const r = stmt(sql.getJob).get(name);
|
|
207
|
+
return r ? rowToJob(r) : null;
|
|
86
208
|
},
|
|
87
209
|
async listJobs() {
|
|
88
|
-
|
|
89
|
-
return rows.map((r) => ({ name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at }));
|
|
210
|
+
return stmt(sql.listJobs).all().map(rowToJob);
|
|
90
211
|
},
|
|
91
212
|
async deleteJob(name) {
|
|
92
|
-
|
|
93
|
-
d.transaction(() => {
|
|
94
|
-
d.prepare("DELETE FROM cronwatch_runs WHERE job = ?").run(name);
|
|
95
|
-
d.prepare("DELETE FROM cronwatch_state WHERE job = ?").run(name);
|
|
96
|
-
d.prepare("DELETE FROM cronwatch_jobs WHERE name = ?").run(name);
|
|
97
|
-
})();
|
|
213
|
+
open().transaction(forget)(name);
|
|
98
214
|
},
|
|
99
215
|
async insertRun(run) {
|
|
100
|
-
|
|
101
|
-
INSERT INTO cronwatch_runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
|
|
102
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
103
|
-
`).run(run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger);
|
|
216
|
+
stmt(sql.insertRun).run(...params.insertRun(run));
|
|
104
217
|
},
|
|
105
218
|
async updateRun(run) {
|
|
106
|
-
|
|
107
|
-
UPDATE cronwatch_runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?
|
|
108
|
-
`).run(run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id);
|
|
219
|
+
stmt(sql.updateRun).run(...params.updateRun(run));
|
|
109
220
|
},
|
|
110
221
|
async getRun(id) {
|
|
111
|
-
const r =
|
|
222
|
+
const r = stmt(sql.getRun).get(id);
|
|
112
223
|
return r ? rowToRun(r) : null;
|
|
113
224
|
},
|
|
114
225
|
async listRuns(job, limit) {
|
|
115
|
-
|
|
116
|
-
return rows.map(rowToRun);
|
|
226
|
+
return stmt(sql.listRuns).all(job, limit).map(rowToRun);
|
|
117
227
|
},
|
|
118
228
|
async lastRun(job) {
|
|
119
|
-
const r =
|
|
229
|
+
const r = stmt(sql.listRuns).get(job, 1);
|
|
120
230
|
return r ? rowToRun(r) : null;
|
|
121
231
|
},
|
|
122
232
|
async runningRuns() {
|
|
123
|
-
|
|
124
|
-
return rows.map(rowToRun);
|
|
233
|
+
return stmt(sql.runningRuns).all().map(rowToRun);
|
|
125
234
|
},
|
|
126
235
|
async getState(job) {
|
|
127
|
-
const r =
|
|
128
|
-
return r ?
|
|
236
|
+
const r = stmt(sql.getState).get(job);
|
|
237
|
+
return r ? rowToState(r) : null;
|
|
129
238
|
},
|
|
130
239
|
async setState(state) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
240
|
+
stmt(sql.setState).run(...params.setState(state));
|
|
241
|
+
},
|
|
242
|
+
async compareAndSetState(state, expectedVersion) {
|
|
243
|
+
const result = expectedVersion === 0 ? stmt(sql.casInsert).run(...params.casInsert(state)) : stmt(sql.casUpdate).run(...params.casUpdate(state, expectedVersion));
|
|
244
|
+
return result.changes > 0;
|
|
135
245
|
},
|
|
136
246
|
async prune(before) {
|
|
137
|
-
|
|
138
|
-
return info.changes;
|
|
247
|
+
return stmt(sql.prune).run(before).changes;
|
|
139
248
|
},
|
|
140
249
|
async close() {
|
|
250
|
+
cache.clear();
|
|
141
251
|
if (db && !options.database) {
|
|
142
252
|
db.close();
|
|
143
253
|
db = null;
|
package/dist/sqlite.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stores/sqlite.ts"],"names":["mkdirSync","path","Database","chmodSync"],"mappings":"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","file":"sqlite.cjs","sourcesContent":["import Database from \"better-sqlite3\";\nimport { chmodSync, mkdirSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { JobState, Run, Store, StoredJob, StoredJobDefinition } from \"../types.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}\n\ninterface JobRow { name: string; definition: string; created_at: number; updated_at: number }\ninterface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: number; finished_at: number | null;\n duration_ms: number | null; error: string | null; output: string | null; metrics: string; trigger: string;\n}\n\nfunction rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: r.started_at, finishedAt: r.finished_at,\n durationMs: r.duration_ms, error: r.error, output: r.output, metrics: JSON.parse(r.metrics), trigger: r.trigger,\n };\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 let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n\n const open = (): Database.Database => {\n if (db) return db;\n if (file !== \":memory:\") {\n mkdirSync(path.dirname(file), { recursive: true });\n }\n db = new Database(file);\n if (file !== \":memory:\") {\n try { chmodSync(file, 0o600); } catch { /* best effort: not every filesystem cares */ }\n }\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.pragma(\"synchronous = NORMAL\");\n return db;\n };\n\n return {\n async init() {\n const d = open();\n d.exec(`\n CREATE TABLE IF NOT EXISTS cronwatch_jobs (\n name TEXT PRIMARY KEY,\n definition TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS cronwatch_runs (\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at INTEGER NOT NULL,\n finished_at INTEGER,\n duration_ms INTEGER,\n error TEXT,\n output TEXT,\n metrics TEXT NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS cronwatch_runs_job_started ON cronwatch_runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS cronwatch_runs_running ON cronwatch_runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS cronwatch_state (\n job TEXT PRIMARY KEY,\n state TEXT NOT NULL\n );\n `);\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n open().prepare(`\n INSERT INTO cronwatch_jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at\n `).run(definition.name, JSON.stringify(definition), now, now);\n },\n async getJob(name) {\n const r = open().prepare(\"SELECT * FROM cronwatch_jobs WHERE name = ?\").get(name) as JobRow | undefined;\n return r ? { name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at } : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const rows = open().prepare(\"SELECT * FROM cronwatch_jobs ORDER BY name\").all() as JobRow[];\n return rows.map((r) => ({ name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at }));\n },\n async deleteJob(name) {\n const d = open();\n d.transaction(() => {\n d.prepare(\"DELETE FROM cronwatch_runs WHERE job = ?\").run(name);\n d.prepare(\"DELETE FROM cronwatch_state WHERE job = ?\").run(name);\n d.prepare(\"DELETE FROM cronwatch_jobs WHERE name = ?\").run(name);\n })();\n },\n async insertRun(run) {\n open().prepare(`\n INSERT INTO cronwatch_runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger);\n },\n async updateRun(run) {\n open().prepare(`\n UPDATE cronwatch_runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?\n `).run(run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id);\n },\n async getRun(id) {\n const r = open().prepare(\"SELECT * FROM cronwatch_runs WHERE id = ?\").get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n const rows = open().prepare(\"SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT ?\").all(job, limit) as RunRow[];\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const r = open().prepare(\"SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT 1\").get(job) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n const rows = open().prepare(\"SELECT * FROM cronwatch_runs WHERE status = 'running' ORDER BY started_at\").all() as RunRow[];\n return rows.map(rowToRun);\n },\n async getState(job) {\n const r = open().prepare(\"SELECT state FROM cronwatch_state WHERE job = ?\").get(job) as { state: string } | undefined;\n return r ? (JSON.parse(r.state) as JobState) : null;\n },\n async setState(state) {\n open().prepare(`\n INSERT INTO cronwatch_state (job, state) VALUES (?, ?)\n ON CONFLICT(job) DO UPDATE SET state = excluded.state\n `).run(state.job, JSON.stringify(state));\n },\n async prune(before) {\n const info = open().prepare(\"DELETE FROM cronwatch_runs WHERE status != 'running' AND started_at < ?\").run(before);\n return info.changes;\n },\n async close() {\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// 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 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, 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 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"]}
|