everydaytok commited on
Commit
27b60ac
·
verified ·
1 Parent(s): 9c321fd

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +74 -44
app.js CHANGED
@@ -2,12 +2,17 @@ import express from 'express';
2
  import cors from 'cors';
3
  import { createClient } from '@supabase/supabase-js';
4
 
5
- // --- CONFIG ---
6
- const PORT = 7860;
 
 
7
  const SUPABASE_URL = process.env.SUPABASE_URL;
8
  const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
9
  const CRON_SECRET = process.env.CRON_SECRET || "default_secret";
10
 
 
 
 
11
  if (!SUPABASE_URL || !SUPABASE_KEY) {
12
  console.error("❌ Missing Supabase Credentials");
13
  process.exit(1);
@@ -16,34 +21,67 @@ if (!SUPABASE_URL || !SUPABASE_KEY) {
16
  const app = express();
17
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
18
 
19
- // Map will now store job details and absolute timestamps (nextRunAt)
20
  const activeJobs = new Map();
21
 
22
  app.use(express.json());
23
  app.use(cors());
24
 
25
- // --- BULLETPROOF DELAY MATH ---
 
 
26
  function getMsUntilNextFiveAM(offset = 0) {
27
  const now = new Date();
28
- const target = new Date(now);
29
 
30
- // Calculate target UTC minutes for local 5 AM
31
  const targetUtcMinutes = (5 * 60) - (offset * 60);
32
-
33
  target.setUTCHours(0, targetUtcMinutes, 0, 0);
34
 
35
- // BUFFER: If the target is in the past OR within the next 2 minutes
36
- // (meaning we just fired it), schedule for TOMORROW.
37
- if (target.getTime() <= now.getTime() + 120000) {
38
  target.setDate(target.getDate() + 1);
39
  }
40
 
41
  return target.getTime() - now.getTime();
42
  }
43
 
44
- function startJobInternal(jobId, intervalMs, url, payload, initialDelay) {
45
- // Calculate absolute timestamp for when this should run next
46
- const nextRunAt = Date.now() + initialDelay;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  activeJobs.set(jobId, {
49
  url,
@@ -53,48 +91,40 @@ function startJobInternal(jobId, intervalMs, url, payload, initialDelay) {
53
  nextRunAt
54
  });
55
 
56
- console.log(`⏳ Job ${jobId} scheduled. Next run: ${new Date(nextRunAt).toLocaleString()}`);
 
 
 
 
 
 
57
  }
58
 
59
  // ==========================================
60
- // ⚙️ THE CRON ENGINE (HEARTBEAT)
61
  // ==========================================
62
- // This loop checks every 60 seconds if any job is ready to fire.
63
- // This survives container sleeps, CPU throttling, and timer drifts.
64
- setInterval(async () => {
65
  const now = Date.now();
66
 
67
  for (const [jobId, job] of activeJobs.entries()) {
68
  if (now >= job.nextRunAt) {
69
- console.log(`⏰ [${new Date().toLocaleTimeString()}] Executing: ${jobId}`);
70
 
71
- // 1. Immediately push the next run time forward to prevent double-firing
72
- // We lock it back to exactly the next 5 AM based on their timezone
73
  const nextDelay = getMsUntilNextFiveAM(job.offset);
74
  job.nextRunAt = now + nextDelay;
75
-
76
- // 2. Fire the webhook asynchronously
77
- try {
78
- fetch(job.url, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify(job.payload)
82
- }).then(res => {
83
- console.log(` └─ Response [${jobId}]: ${res.status}`);
84
- }).catch(err => {
85
- console.error(`❌ Fetch Error [${jobId}]:`, err.message);
86
- });
87
- } catch (e) {
88
- console.error(`❌ Job ${jobId} HTTP Failed:`, e.message);
89
- }
90
 
91
- console.log(`🔄 Job ${jobId} entered 24h cycle. Next run in ${(nextDelay/1000/60/60).toFixed(2)} hours.`);
 
92
  }
93
  }
94
  }, 60000); // Ticks every 60 seconds
95
 
96
-
97
- // --- DB HYDRATION ---
 
98
  async function hydrateJobs() {
99
  console.log("💧 Hydrating Cron Jobs from DB...");
100
  const { data, error } = await supabase.from('system_jobs').select('*');
@@ -109,13 +139,13 @@ async function hydrateJobs() {
109
  const offset = job.payload?.timezoneOffset ?? 0;
110
  const newDelay = getMsUntilNextFiveAM(offset);
111
 
112
- startJobInternal(job.id, job.interval_ms, job.webhook_url, job.payload, newDelay);
 
113
  count++;
114
  }
115
  console.log(`✅ Successfully hydrated and scheduled ${count} jobs.`);
116
  }
117
 
118
- // --- ENDPOINTS ---
119
  app.get('/', (req, res) => res.json({ status: "Cron Registry Active", active_jobs: activeJobs.size }));
120
 
121
  app.post('/register', async (req, res) => {
@@ -136,9 +166,10 @@ app.post('/register', async (req, res) => {
136
 
137
  const delay = (initialDelay !== undefined) ? initialDelay : getMsUntilNextFiveAM(payload?.timezoneOffset || 0);
138
 
139
- startJobInternal(jobId, intervalMs, webhookUrl, payload, delay);
 
140
 
141
- console.log(`➕ Registered Job: ${jobId}`);
142
  res.json({ success: true, jobId });
143
  });
144
 
@@ -147,7 +178,6 @@ app.post('/deregister', async (req, res) => {
147
  if (secret !== CRON_SECRET) return res.status(403).json({ error: "Unauthorized" });
148
 
149
  await supabase.from('system_jobs').delete().eq('id', jobId);
150
-
151
  if (activeJobs.has(jobId)) {
152
  activeJobs.delete(jobId);
153
  }
 
2
  import cors from 'cors';
3
  import { createClient } from '@supabase/supabase-js';
4
 
5
+ // ==========================================
6
+ // ⚙️ CONFIGURATION & SETTINGS
7
+ // ==========================================
8
+ const PORT = process.env.PORT || 7860;
9
  const SUPABASE_URL = process.env.SUPABASE_URL;
10
  const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
11
  const CRON_SECRET = process.env.CRON_SECRET || "default_secret";
12
 
13
+ // 🔴 VARIABLE BOOLEAN: Should past/missed jobs fire immediately on server startup?
14
+ const RUN_PAST_EVENTS_ON_STARTUP = false;
15
+
16
  if (!SUPABASE_URL || !SUPABASE_KEY) {
17
  console.error("❌ Missing Supabase Credentials");
18
  process.exit(1);
 
21
  const app = express();
22
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
23
 
 
24
  const activeJobs = new Map();
25
 
26
  app.use(express.json());
27
  app.use(cors());
28
 
29
+ // ==========================================
30
+ // ⏱️ YOUR EXACT ORIGINAL MATH (RESTORED)
31
+ // ==========================================
32
  function getMsUntilNextFiveAM(offset = 0) {
33
  const now = new Date();
34
+ const target = new Date(now.getTime());
35
 
36
+ // Using your exact original calculation
37
  const targetUtcMinutes = (5 * 60) - (offset * 60);
 
38
  target.setUTCHours(0, targetUtcMinutes, 0, 0);
39
 
40
+ if (target <= now) {
 
 
41
  target.setDate(target.getDate() + 1);
42
  }
43
 
44
  return target.getTime() - now.getTime();
45
  }
46
 
47
+ // ==========================================
48
+ // 🚀 JOB EXECUTION & SCHEDULING
49
+ // ==========================================
50
+ async function executeJob(jobId) {
51
+ const job = activeJobs.get(jobId);
52
+ if (!job) return;
53
+
54
+ console.log(`⏰ [${new Date().toLocaleTimeString()}] Executing: ${jobId}`);
55
+ try {
56
+ const res = await fetch(job.url, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify(job.payload)
60
+ });
61
+ console.log(` └─ Response: ${res.status}`);
62
+ } catch (e) {
63
+ console.error(`❌ Job ${jobId} HTTP Failed:`, e.message);
64
+ }
65
+ }
66
+
67
+ function startJobInternal(jobId, intervalMs, url, payload, initialDelay, isHydration = false) {
68
+ let delay = initialDelay;
69
+ let runImmediately = false;
70
+
71
+ // Failsafe: If delay is somehow massive (e.g. accidentally passed a timestamp), reset it
72
+ if (delay > 31536000000) {
73
+ delay = getMsUntilNextFiveAM(payload?.timezoneOffset || 0);
74
+ }
75
+
76
+ // Handle the past events boolean for DB hydration
77
+ if (isHydration && RUN_PAST_EVENTS_ON_STARTUP) {
78
+ runImmediately = true;
79
+ delay = getMsUntilNextFiveAM(payload?.timezoneOffset || 0);
80
+ }
81
+
82
+ const nextRunAt = Date.now() + delay;
83
+ const hours = (delay / 1000 / 60 / 60).toFixed(2);
84
+ const minutes = (delay / 1000 / 60).toFixed(2);
85
 
86
  activeJobs.set(jobId, {
87
  url,
 
91
  nextRunAt
92
  });
93
 
94
+ if (runImmediately) {
95
+ console.log(`⚡ [Startup] Firing past/missed event immediately: ${jobId}`);
96
+ executeJob(jobId); // Fire right now
97
+ console.log(`⏳ Job ${jobId} re-scheduled. Next 5AM run in ${hours} hours (${minutes} mins).`);
98
+ } else {
99
+ console.log(`⏳ Job ${jobId} scheduled. First 5AM run in ${hours} hours (${minutes} mins).`);
100
+ }
101
  }
102
 
103
  // ==========================================
104
+ // ⚙️ THE TICK ENGINE (REPLACES CRASHING TIMEOUTS)
105
  // ==========================================
106
+ // We keep this because native setTimeouts die after 24 hours on cloud servers.
107
+ // This 60-second tick just acts as a watchdog to fire your jobs accurately.
108
+ setInterval(() => {
109
  const now = Date.now();
110
 
111
  for (const [jobId, job] of activeJobs.entries()) {
112
  if (now >= job.nextRunAt) {
113
+ executeJob(jobId); // Fire the webhook
114
 
115
+ // Calculate exact time until NEXT 5 AM
 
116
  const nextDelay = getMsUntilNextFiveAM(job.offset);
117
  job.nextRunAt = now + nextDelay;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ const hours = (nextDelay / 1000 / 60 / 60).toFixed(2);
120
+ console.log(`🔄 Job ${jobId} entering 24h interval cycle. Next run in ${hours} hours.`);
121
  }
122
  }
123
  }, 60000); // Ticks every 60 seconds
124
 
125
+ // ==========================================
126
+ // 💾 DB HYDRATION & ROUTES
127
+ // ==========================================
128
  async function hydrateJobs() {
129
  console.log("💧 Hydrating Cron Jobs from DB...");
130
  const { data, error } = await supabase.from('system_jobs').select('*');
 
139
  const offset = job.payload?.timezoneOffset ?? 0;
140
  const newDelay = getMsUntilNextFiveAM(offset);
141
 
142
+ // Pass 'true' at the end to signify this is a startup hydration
143
+ startJobInternal(job.id, job.interval_ms, job.webhook_url, job.payload, newDelay, true);
144
  count++;
145
  }
146
  console.log(`✅ Successfully hydrated and scheduled ${count} jobs.`);
147
  }
148
 
 
149
  app.get('/', (req, res) => res.json({ status: "Cron Registry Active", active_jobs: activeJobs.size }));
150
 
151
  app.post('/register', async (req, res) => {
 
166
 
167
  const delay = (initialDelay !== undefined) ? initialDelay : getMsUntilNextFiveAM(payload?.timezoneOffset || 0);
168
 
169
+ // Pass 'false' at the end to signify this is a fresh registration
170
+ startJobInternal(jobId, intervalMs, webhookUrl, payload, delay, false);
171
 
172
+ console.log(`➕ Registered & Scheduled Job (5AM Anchor): ${jobId}`);
173
  res.json({ success: true, jobId });
174
  });
175
 
 
178
  if (secret !== CRON_SECRET) return res.status(403).json({ error: "Unauthorized" });
179
 
180
  await supabase.from('system_jobs').delete().eq('id', jobId);
 
181
  if (activeJobs.has(jobId)) {
182
  activeJobs.delete(jobId);
183
  }