UNPKG

partywhen

Version:

A library for scheduling and running tasks in Cloudflare Workers

380 lines 11.3 kB
// src/index.ts import cronParser from "cron-parser"; import { Server } from "partyserver"; var Scheduler = class extends Server { constructor(state, env) { super(state, env); void this.ctx.blockConcurrencyWhile(async () => { this.ctx.storage.sql.exec( ` CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, description TEXT, payload TEXT, callback TEXT, type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed', 'cron', 'no-schedule')), time INTEGER, delayInSeconds INTEGER, cron TEXT, created_at INTEGER DEFAULT (unixepoch()) ) ` ); await this.alarm(); }); } status() { return { status: "reachable", timestamp: Date.now(), diskUsage: this.ctx.storage.sql.databaseSize }; } async fetch(_request) { return new Response("Hello World!"); } getAllTasks() { return this.querySql([{ sql: "SELECT * FROM tasks" }]); } async scheduleNextAlarm() { const query = ` SELECT time FROM tasks WHERE time > ? AND type != 'no-schedule' ORDER BY time ASC LIMIT 1 `; const { result } = this.querySql([ { sql: query, params: [Math.floor(Date.now() / 1e3)] } ]); if (!result) return; if (result.length > 0 && "time" in result[0]) { const nextTime = result[0].time * 1e3; await this.ctx.storage.setAlarm(nextTime); } } async scheduleTask(task) { const { id } = task; console.log("scheduling task", task); if ("time" in task && task.time) { const timestamp = Math.floor(task.time.getTime() / 1e3); const query = ` INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, time) VALUES (?, ?, ?, ?, 'scheduled', ?) `; this.querySql([ { sql: query, params: [ id, task.description || null, JSON.stringify(task.payload || null), JSON.stringify(task.callback || null), timestamp ] } ]); await this.scheduleNextAlarm(); return { id, description: task.description, payload: task.payload, callback: task.callback, time: task.time, type: "scheduled" }; } else if ("delayInSeconds" in task && task.delayInSeconds) { const time = new Date(Date.now() + task.delayInSeconds * 1e3); const timestamp = Math.floor(time.getTime() / 1e3); const query = ` INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, delayInSeconds, time) VALUES (?, ?, ?, ?, 'delayed', ?, ?) `; this.querySql([ { sql: query, params: [ id, task.description || null, JSON.stringify(task.payload || null), JSON.stringify(task.callback || null), task.delayInSeconds, timestamp ] } ]); await this.scheduleNextAlarm(); return { id, description: task.description, payload: task.payload, callback: task.callback, delayInSeconds: task.delayInSeconds, time, type: "delayed" }; } else if ("cron" in task && task.cron) { const nextExecutionTime = this.getNextCronTime(task.cron); const timestamp = Math.floor(nextExecutionTime.getTime() / 1e3); const query = ` INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, cron, time) VALUES (?, ?, ?, ?, 'cron', ?, ?) `; this.querySql([ { sql: query, params: [ id, task.description || null, JSON.stringify(task.payload || null), JSON.stringify(task.callback || null), task.cron, timestamp ] } ]); await this.scheduleNextAlarm(); return { id, description: task.description, payload: task.payload, callback: task.callback, cron: task.cron, time: nextExecutionTime, type: "cron" }; } else { const time = /* @__PURE__ */ new Date(864e13); const timestamp = Math.floor(time.getTime() / 1e3); const query = ` INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, time) VALUES (?, ?, ?, ?, 'no-schedule', ?) `; this.querySql([ { sql: query, params: [ id, task.description || null, JSON.stringify(task.payload || null), JSON.stringify(task.callback || null), timestamp ] } ]); return { id, description: task.description, payload: task.payload, callback: task.callback, time, type: "no-schedule" }; } } async alarm() { const now = Math.floor(Date.now() / 1e3); const { result: tasks } = this.querySql([ { sql: "SELECT * FROM tasks WHERE time <= ?", params: [now] } ]); for (const row of tasks || []) { const task = this.rowToTask(row); await this.executeTask(task); if (task.type === "cron") { const nextExecutionTime = this.getNextCronTime(task.cron); const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3); this.querySql([ { sql: "UPDATE tasks SET time = ? WHERE id = ?", params: [nextTimestamp, task.id] } ]); } else { this.querySql([ { sql: "DELETE FROM tasks WHERE id = ?", params: [task.id] } ]); } } await this.scheduleNextAlarm(); } rowToTask(row) { const base = { id: row.id, description: row.description, payload: row.payload ? JSON.parse(row.payload) : void 0, callback: row.callback ? JSON.parse(row.callback) : void 0 }; switch (row.type) { case "scheduled": return { ...base, time: new Date(row.time * 1e3), type: "scheduled" }; case "delayed": return { ...base, delayInSeconds: row.delayInSeconds, time: new Date(row.time * 1e3), type: "delayed" }; case "cron": return { ...base, cron: row.cron, time: new Date(row.time * 1e3), type: "cron" }; case "no-schedule": return { ...base, time: new Date(row.time * 1e3), type: "no-schedule" }; default: throw new Error(`Unknown task type: ${row.type}`); } } async executeTask(task) { console.log(`Executing task ${task.id}:`, task); if ("callback" in task && task.callback) { const { type } = task.callback; if (type === "webhook") { const response = await fetch(task.callback.url, { method: "POST", body: JSON.stringify(task), headers: { "Content-Type": "application/json" } }); if (!response.ok) { const text = await response.text(); throw new Error( `Webhook failed with status ${response.status} and body ${text}` ); } const responseBody = await response.text(); console.log(`Webhook response body: ${responseBody}`); } else if (type === "durable-object") { const id = this.env[task.callback.namespace].idFromName( task.callback.name ); const stub = this.env[task.callback.namespace].get(id); stub[task.callback.function](task).catch((e) => { console.error("Error executing durable object function:", e); }); } else if (type === "service") { this.env[task.callback.service][task.callback.function](task); } else if (type === "self") { this[task.callback.function](task); } else { console.error("unknown callback type", task); } } else { console.error("missing callback", task); } } getNextCronTime(cronExpression) { const interval = cronParser.parseExpression(cronExpression); return interval.next().toDate(); } async query(criteria = {}) { let query = "SELECT * FROM tasks WHERE 1=1"; const params = []; if (criteria.id) { query += " AND id = ?"; params.push(criteria.id); } if (criteria.description) { query += " AND description = ?"; params.push(criteria.description); } if (criteria.type) { query += " AND type = ?"; params.push(criteria.type); } if (criteria.timeRange) { query += " AND time >= ? AND time <= ?"; const start = criteria.timeRange.start || /* @__PURE__ */ new Date(0); const end = criteria.timeRange.end || /* @__PURE__ */ new Date(999999999999999); params.push( Math.floor(start.getTime() / 1e3), Math.floor(end.getTime() / 1e3) ); } const { result } = this.querySql([{ sql: query, params }]); return (result == null ? void 0 : result.map((row) => this.rowToTask(row))) || []; } async cancelTask(id) { const query = "DELETE FROM tasks WHERE id = ?"; this.querySql([{ sql: query, params: [id] }]); await this.scheduleNextAlarm(); return true; } querySql(qs, isRaw = false) { try { if (!qs.length) { throw new Error("No query found to run"); } const queries = (qs == null ? void 0 : qs.map((item) => { const { sql, params } = item; if (!(sql == null ? void 0 : sql.trim())) { throw new Error("Empty 'sql' field in transaction"); } return { sql, params }; })) || []; let result; if (queries.length > 1) { result = this.executeTransaction(queries, isRaw); } else { const [query] = queries; result = this.executeQuery(query.sql, query.params, isRaw); } return { error: null, status: 200, result }; } catch (error) { return { result: null, error: error.message ?? "Operation failed.", status: 500 }; } } executeTransaction(queries, isRaw) { return this.ctx.storage.transactionSync(() => { const results = []; for (const queryObj of queries) { const { sql, params } = queryObj; const result = this.executeQuery(sql, params, isRaw); results.push(result); } return results; }); } executeQuery(sql, params, isRaw) { const cursor = (params == null ? void 0 : params.length) ? this.ctx.storage.sql.exec(sql, ...params) : this.ctx.storage.sql.exec(sql); let result; if (isRaw) { result = { columns: cursor.columnNames, // @ts-expect-error TODO fix this!!! // eslint-disable-next-line @typescript-eslint/no-unsafe-call rows: cursor.raw().toArray(), meta: { rows_read: cursor.rowsRead, rows_written: cursor.rowsWritten } }; } else { result = cursor.toArray(); } return result; } }; export { Scheduler }; //# sourceMappingURL=index.js.map