import { serve } from "bun";
import { Expo } from "expo-server-sdk";
const expo = new Expo();
const events = [];
const html = `
Timed Event Scheduler
Schedule Timed Event
`;
serve({
port: 7860,
fetch: async (req) => {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
// POST /events - Create Event
if (req.method === "POST" && url.pathname === "/events") {
try {
const body = await req.json();
const { id, delay, message, type, repeat, expoTokens, webhookUrl, payload } = body;
if (!id || !delay || !type) {
return new Response("Missing 'id', 'delay', or 'type'", { status: 400 });
}
if (events.find(e => e.id === id)) {
return new Response("Event ID already exists", { status: 409 });
}
events.push({
id,
timestamp: Date.now() + delay * 1000,
type,
repeat: type === "interval" && repeat ? repeat * 1000 : undefined,
data: {
message,
expoTokens: Array.isArray(expoTokens) ? expoTokens : [],
webhookUrl,
payload
}
});
return new Response("Event scheduled", { status: 200 });
} catch (err) {
return new Response("Invalid JSON", { status: 400 });
}
}
// DELETE /events/:id - Remove Event
if (req.method === "DELETE" && url.pathname.startsWith("/events/")) {
const id = url.pathname.split("/")[2];
const index = events.findIndex(e => e.id === id);
if (index === -1) return new Response("Event not found", { status: 404 });
events.splice(index, 1);
return new Response("Event deleted", { status: 200 });
}
return new Response("Not Found", { status: 404 });
}
});
// Event Loop
setInterval(async () => {
const now = Date.now();
const due = events.filter(e => e.timestamp <= now);
for (const e of due) {
console.log(`⏰ Executing event: ${e.id} (${e.type})`);
// Expo Notifications
if (e.data.expoTokens?.length && e.data.message) {
console.log("found a notifications events")
const messages = e.data.expoTokens
.filter(t => Expo.isExpoPushToken(t))
.map(token => ({
to: token,
sound: 'default',
title: '⏰ Scheduled Event',
body: e.data.message,
data: {}
}));
for (const chunk of expo.chunkPushNotifications(messages)) {
try {
await expo.sendPushNotificationsAsync(chunk);
} catch (err) {
console.error("🔥 Expo error:", err);
}
}
}
// Webhook Trigger
if (e.type === "webhook" && e.data.webhookUrl) {
console.log("found a webhook event")
try {
await fetch(e.data.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(e.data.payload || {})
});
console.log(`📡 Webhook fired for ${e.id}`);
} catch (err) {
console.error(`❌ Webhook failed for ${e.id}`, err);
}
}
// Repeat or remove
if (e.type === "interval" && e.repeat) {
e.timestamp = now + e.repeat;
} else {
const i = events.findIndex(ev => ev.id === e.id);
if (i !== -1) events.splice(i, 1);
}
}
}, 1000);