feat: automatické sledování doručení objednávky přes Bolt Food
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 36s
CI / Playwright E2E tests (push) Successful in 1m25s
CI / Build and push Docker image (push) Successful in 43s
CI / Notify (push) Successful in 2s

Zakladatel skupiny může na stránce objednání vložit sdílecí odkaz
Bolt Food. Server pak každou minutu dotazuje veřejné Bolt API
a automaticky aktualizuje čas doručení skupiny (deliveryAt).
Sledování se samo ukončí po doručení, zrušení objednávky nebo
opakovaných chybách. Leader lease vytažena do znovupoužitelného
modulu leaderLease.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:22:07 +02:00
parent 1df21edc1a
commit 491ec25b52
12 changed files with 559 additions and 52 deletions
+4 -45
View File
@@ -1,15 +1,14 @@
import webpush from 'web-push';
import crypto from 'crypto';
import getStorage from './storage';
import { getRedisClient } from './storage/redis';
import { createLeaderLease } from './leaderLease';
import { getClientData, getToday } from './service';
import { getIsWeekend } from './utils';
import { LunchChoices } from '../../types';
const storage = getStorage();
const REGISTRY_KEY = 'push_reminder_registry';
const LEADER_LEASE_KEY = 'luncher:reminder:leader';
const LEASE_TTL_SECONDS = 90;
const lease = createLeaderLease('luncher:reminder:leader');
const POD_ID = process.env.POD_ID ?? `local-${process.pid}`;
@@ -43,49 +42,9 @@ function userHasChoice(choices: LunchChoices, login: string): boolean {
return false;
}
/**
* Pokusí se získat nebo obnovit leader lease pro scheduler připomínek.
* Vrátí true pokud tato instance smí spustit připomínky.
* Při non-Redis storage vždy vrací true (single-process, leader election není potřeba).
*/
async function tryAcquireOrRenewLease(): Promise<boolean> {
if (process.env.STORAGE?.toLowerCase() !== 'redis') return true;
try {
const c = getRedisClient();
if (!c) return true;
// Zkusíme získat lease atomicky (SET NX EX)
const acquired = await c.set(LEADER_LEASE_KEY, POD_ID, { NX: true, EX: LEASE_TTL_SECONDS });
if (acquired !== null) return true; // lease čerstvě získána
// Pokud jsme ji nedostali, ověříme zda ji držíme my
const currentHolder = await c.get(LEADER_LEASE_KEY);
if (currentHolder === POD_ID) {
// Naše lease — obnovíme TTL
await c.set(LEADER_LEASE_KEY, POD_ID, { EX: LEASE_TTL_SECONDS });
return true;
}
return false; // lease drží jiná instance
} catch (e) {
console.error('Push reminder: chyba při získávání lease, připomínky budou odeslány', e);
return true; // při chybě raději spustíme, než vynecháme
}
}
/** Uvolní leader lease při graceful shutdown. */
export async function releaseReminderLease(): Promise<void> {
if (process.env.STORAGE?.toLowerCase() !== 'redis') return;
try {
const c = getRedisClient();
if (!c) return;
const currentHolder = await c.get(LEADER_LEASE_KEY);
if (currentHolder === POD_ID) {
await c.del(LEADER_LEASE_KEY);
console.log('Push reminder: lease uvolněna');
}
} catch (e) {
console.error('Push reminder: chyba při uvolňování lease', e);
}
await lease.release();
}
/** Stopne scheduler připomínek. Volá se při graceful shutdown. */
@@ -140,7 +99,7 @@ async function checkAndSendReminders(): Promise<void> {
if (getIsWeekend(getToday())) return;
// Leader election — pouze jeden pod spouští připomínky
const isLeader = await tryAcquireOrRenewLease();
const isLeader = await lease.tryAcquireOrRenew();
if (!isLeader) return;
const registry = await storage.getData<Registry>(REGISTRY_KEY) ?? {};