partywhen
Version:
A library for scheduling and running tasks in Cloudflare Workers
340 lines (339 loc) • 9.52 kB
JavaScript
import cronParser from "cron-parser";
import { Server } from "partyserver";
//#region src/index.ts
var Scheduler = class extends Server {
constructor(state, env) {
super(state, env);
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 { result } = this.querySql([{
sql: `
SELECT time FROM tasks
WHERE time > ?
AND type != 'no-schedule'
ORDER BY time ASC
LIMIT 1
`,
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);
this.querySql([{
sql: `
INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, time)
VALUES (?, ?, ?, ?, 'scheduled', ?)
`,
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);
this.querySql([{
sql: `
INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, delayInSeconds, time)
VALUES (?, ?, ?, ?, 'delayed', ?, ?)
`,
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);
this.querySql([{
sql: `
INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, cron, time)
VALUES (?, ?, ?, ?, 'cron', ?, ?)
`,
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);
this.querySql([{
sql: `
INSERT OR REPLACE INTO tasks (id, description, payload, callback, type, time)
VALUES (?, ?, ?, ?, 'no-schedule', ?)
`,
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: /* @__PURE__ */ new Date(row.time * 1e3),
type: "scheduled"
};
case "delayed": return {
...base,
delayInSeconds: row.delayInSeconds,
time: /* @__PURE__ */ new Date(row.time * 1e3),
type: "delayed"
};
case "cron": return {
...base,
cron: row.cron,
time: /* @__PURE__ */ new Date(row.time * 1e3),
type: "cron"
};
case "no-schedule": return {
...base,
time: /* @__PURE__ */ 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);
this.env[task.callback.namespace].get(id)[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) {
return cronParser.parse(cronExpression).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?.map((row) => this.rowToTask(row)) || [];
}
async cancelTask(id) {
this.querySql([{
sql: "DELETE FROM tasks WHERE id = ?",
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?.map((item) => {
const { sql, params } = item;
if (!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?.length ? this.ctx.storage.sql.exec(sql, ...params) : this.ctx.storage.sql.exec(sql);
let result;
if (isRaw) result = {
columns: cursor.columnNames,
rows: cursor.raw().toArray(),
meta: {
rows_read: cursor.rowsRead,
rows_written: cursor.rowsWritten
}
};
else result = cursor.toArray();
return result;
}
};
//#endregion
export { Scheduler };
//# sourceMappingURL=index.js.map