everydaytok commited on
Commit
f5eeb4a
Β·
verified Β·
1 Parent(s): 7faa37c

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +50 -27
app.js CHANGED
@@ -11,7 +11,7 @@ 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 = true; // false;
15
 
16
  if (!SUPABASE_URL || !SUPABASE_KEY) {
17
  console.error("❌ Missing Supabase Credentials");
@@ -27,13 +27,12 @@ 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
 
@@ -58,27 +57,37 @@ async function executeJob(jobId) {
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);
@@ -92,38 +101,35 @@ function startJobInternal(jobId, intervalMs, url, payload, initialDelay, isHydra
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...");
@@ -135,12 +141,29 @@ async function hydrateJobs() {
135
  }
136
 
137
  let count = 0;
 
 
138
  for (const job of data) {
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.`);
@@ -153,20 +176,20 @@ app.post('/register', async (req, res) => {
153
 
154
  if (secret !== CRON_SECRET) return res.status(403).json({ error: "Unauthorized" });
155
 
 
156
  const { error } = await supabase.from('system_jobs').upsert({
157
  id: jobId,
158
  lead_id: leadId,
159
  interval_ms: intervalMs,
160
  webhook_url: webhookUrl,
161
  payload: payload,
162
- updated_at: new Date()
163
  });
164
 
165
  if (error) return res.status(500).json({ error: error.message });
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}`);
 
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 = true;
15
 
16
  if (!SUPABASE_URL || !SUPABASE_KEY) {
17
  console.error("❌ Missing Supabase Credentials");
 
27
  app.use(cors());
28
 
29
  // ==========================================
30
+ // ⏱️ YOUR EXACT ORIGINAL MATH
31
  // ==========================================
32
  function getMsUntilNextFiveAM(offset = 0) {
33
  const now = new Date();
34
  const target = new Date(now.getTime());
35
 
 
36
  const targetUtcMinutes = (5 * 60) - (offset * 60);
37
  target.setUTCHours(0, targetUtcMinutes, 0, 0);
38
 
 
57
  headers: { 'Content-Type': 'application/json' },
58
  body: JSON.stringify(job.payload)
59
  });
60
+
61
  console.log(` └─ Response: ${res.status}`);
62
+
63
+ // βœ… UPDATE DATABASE ON SUCCESS
64
+ // If the webhook succeeds, we log the exact time so future restarts know it ran.
65
+ if (res.ok) {
66
+ const { error } = await supabase
67
+ .from('system_jobs')
68
+ .update({ updated_at: new Date().toISOString() })
69
+ .eq('id', jobId);
70
+
71
+ if (!error) {
72
+ console.log(` └─ πŸ’Ύ Database 'updated_at' successfully synced.`);
73
+ } else {
74
+ console.error(` └─ ❌ Failed to update DB:`, error.message);
75
+ }
76
+ }
77
+
78
  } catch (e) {
79
  console.error(`❌ Job ${jobId} HTTP Failed:`, e.message);
80
  }
81
  }
82
 
83
+ function startJobInternal(jobId, intervalMs, url, payload, initialDelay, runImmediately = false) {
84
  let delay = initialDelay;
 
85
 
86
+ // Failsafe: Reset crazy massive delays to next 5AM
87
  if (delay > 31536000000) {
88
  delay = getMsUntilNextFiveAM(payload?.timezoneOffset || 0);
89
  }
90
 
 
 
 
 
 
 
91
  const nextRunAt = Date.now() + delay;
92
  const hours = (delay / 1000 / 60 / 60).toFixed(2);
93
  const minutes = (delay / 1000 / 60).toFixed(2);
 
101
  });
102
 
103
  if (runImmediately) {
104
+ console.log(`⚑ [Startup] Missed event detected! Firing immediately: ${jobId}`);
105
+ executeJob(jobId);
106
  console.log(`⏳ Job ${jobId} re-scheduled. Next 5AM run in ${hours} hours (${minutes} mins).`);
107
  } else {
108
+ console.log(`⏳ Job ${jobId} scheduled. Next 5AM run in ${hours} hours (${minutes} mins).`);
109
  }
110
  }
111
 
112
  // ==========================================
113
+ // βš™οΈ THE TICK ENGINE (WATCHDOG)
114
  // ==========================================
 
 
115
  setInterval(() => {
116
  const now = Date.now();
117
 
118
  for (const [jobId, job] of activeJobs.entries()) {
119
  if (now >= job.nextRunAt) {
120
+ executeJob(jobId);
121
 
 
122
  const nextDelay = getMsUntilNextFiveAM(job.offset);
123
  job.nextRunAt = now + nextDelay;
124
 
125
  const hours = (nextDelay / 1000 / 60 / 60).toFixed(2);
126
+ console.log(`πŸ”„ Job ${jobId} entered 24h cycle. Next run in ${hours} hours.`);
127
  }
128
  }
129
+ }, 60000);
130
 
131
  // ==========================================
132
+ // πŸ’Ύ DB HYDRATION USING `updated_at`
133
  // ==========================================
134
  async function hydrateJobs() {
135
  console.log("πŸ’§ Hydrating Cron Jobs from DB...");
 
141
  }
142
 
143
  let count = 0;
144
+ const now = Date.now();
145
+
146
  for (const job of data) {
147
  const offset = job.payload?.timezoneOffset ?? 0;
148
  const newDelay = getMsUntilNextFiveAM(offset);
149
+ let runImmediately = false;
150
+
151
+ // βœ… CHECK LAST UPDATED TIME
152
+ if (job.updated_at) {
153
+ const lastUpdatedTime = new Date(job.updated_at).getTime();
154
+ const timeSinceLastRunMs = now - lastUpdatedTime;
155
+
156
+ // If it's been more than 24 hours (86,400,000 ms) since the last successful run,
157
+ // AND the variable allows it, we fire it instantly to catch up.
158
+ if (RUN_PAST_EVENTS_ON_STARTUP && timeSinceLastRunMs >= 86400000) {
159
+ runImmediately = true;
160
+ }
161
+ } else if (RUN_PAST_EVENTS_ON_STARTUP) {
162
+ // If it has NEVER run before (no updated_at), run it now.
163
+ runImmediately = true;
164
+ }
165
 
166
+ startJobInternal(job.id, job.interval_ms, job.webhook_url, job.payload, newDelay, runImmediately);
 
167
  count++;
168
  }
169
  console.log(`βœ… Successfully hydrated and scheduled ${count} jobs.`);
 
176
 
177
  if (secret !== CRON_SECRET) return res.status(403).json({ error: "Unauthorized" });
178
 
179
+ // Creating a fresh job (updated_at is null so it gets processed naturally)
180
  const { error } = await supabase.from('system_jobs').upsert({
181
  id: jobId,
182
  lead_id: leadId,
183
  interval_ms: intervalMs,
184
  webhook_url: webhookUrl,
185
  payload: payload,
186
+ updated_at: new Date().toISOString()
187
  });
188
 
189
  if (error) return res.status(500).json({ error: error.message });
190
 
191
  const delay = (initialDelay !== undefined) ? initialDelay : getMsUntilNextFiveAM(payload?.timezoneOffset || 0);
192
 
 
193
  startJobInternal(jobId, intervalMs, webhookUrl, payload, delay, false);
194
 
195
  console.log(`βž• Registered & Scheduled Job (5AM Anchor): ${jobId}`);